Created
May 3, 2023 01:30
-
-
Save NotArchon/d0abf31457267720cbb42f1c271e690f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.neox.web.model.game.requests; | |
import com.mongodb.MongoException; | |
import com.mongodb.client.MongoCollection; | |
import com.mongodb.client.model.*; | |
import com.neox.web.Neox; | |
import com.neox.web.model.game.GameRequest; | |
import com.neox.web.pojo.HiscoresDoc; | |
import io.archon.misc.utils.ThreadUtils; | |
import io.archon.misc.utils.TimeUtils; | |
import io.archon.webserver.network.HttpAsyncRequest; | |
import io.archon.webserver.network.messages.StatusMessage; | |
import io.netty5.handler.codec.http.HttpResponseStatus; | |
import lombok.AccessLevel; | |
import lombok.NoArgsConstructor; | |
import lombok.RequiredArgsConstructor; | |
import lombok.experimental.FieldDefaults; | |
import org.jetbrains.annotations.Nullable; | |
import java.time.*; | |
import java.time.temporal.ChronoUnit; | |
import java.util.*; | |
@FieldDefaults(level = AccessLevel.PRIVATE) | |
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) // needed for default values | |
public class HiscoresUpdate implements GameRequest { | |
// todo prune timed entries over a month old | |
final String userId; | |
final String gameMode; | |
final String xpMode; | |
final @Nullable String buildKey; | |
final List<HiscoresEntry> entries; | |
@RequiredArgsConstructor | |
private static final class HiscoresEntry { | |
final String key; | |
final long primary; | |
final long secondary; | |
} | |
@RequiredArgsConstructor | |
private static final class Gain { | |
final String baseKey; | |
final long primary; | |
final long secondary; | |
} | |
final transient long ms = System.currentTimeMillis(); | |
@Override | |
public void handle(HttpAsyncRequest httpReq) { // 2630 bytes as of 4/28/23 | |
if(userId == null || gameMode == null || xpMode == null || entries == null) { // should never happen | |
httpReq.write(new StatusMessage(HttpResponseStatus.BAD_REQUEST)); | |
return; | |
} | |
MongoCollection<HiscoresDoc> collection = Neox.getNode().getMongoDatabase() | |
.getCollection("hiscores", HiscoresDoc.class); // not sure if we should cache this ? | |
Map<String, HiscoresDoc> existingMap = new HashMap<>(); | |
collection.find(Filters.and( | |
Filters.eq("user_id", userId), | |
Filters.eq("game_mode", gameMode), | |
Filters.eq("xp_mode", xpMode) | |
/* not including build here since we always overwrite that field because it can potentially change */ | |
)).forEach(e -> existingMap.put(e.key(), e)); | |
List<WriteModel<HiscoresDoc>> updates = new ArrayList<>(entries.size()); | |
List<Gain> gains = new ArrayList<>(); | |
entries.forEach(entry -> { | |
if(entry.key == null) // should never happen | |
return; | |
HiscoresDoc existing = existingMap.get(entry.key); | |
if(entry.primary == 0 && entry.secondary == 0) { | |
/* no need to store these, delete if they exist */ | |
if(existing != null) | |
updates.add(new DeleteOneModel<>(Filters.eq("_id", existing._id()))); | |
return; | |
} | |
WriteModel<HiscoresDoc> update = makeUpdate(entry.key, entry.primary, entry.secondary, existing, null); | |
if(update != null) | |
updates.add(update); | |
if(existing != null) { | |
long primaryGain = Math.max(0, entry.primary - existing.primary()); | |
long secondaryGain = Math.max(0, entry.secondary - existing.secondary()); | |
if(primaryGain > 0 || secondaryGain > 0) | |
gains.add(new Gain(entry.key, primaryGain, secondaryGain)); | |
} | |
}); | |
if(!gains.isEmpty()) { | |
ZonedDateTime now = ZonedDateTime.ofInstant(new Date(ms).toInstant(), ZoneId.of("UTC")); | |
/* weekly gains */ | |
int daysPastMonday = now.getDayOfWeek().getValue() - DayOfWeek.MONDAY.getValue(); | |
ZonedDateTime weekStart = now.truncatedTo(ChronoUnit.DAYS).minusDays(daysPastMonday); | |
long weekStartMs = ms - TimeUtils.getMillisBetween(weekStart, now); | |
updates.addAll(makesGainsUpdates(existingMap, "weekly", gains, weekStartMs)); | |
/* monthly gains */ | |
ZonedDateTime monthStart = now.truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1); | |
long monthStartMs = ms - TimeUtils.getMillisBetween(monthStart, now); | |
updates.addAll(makesGainsUpdates(existingMap, "monthly", gains, monthStartMs)); | |
} | |
if(updates.isEmpty()) | |
return; | |
int tries = 0; | |
while(tries++ < 5) { | |
try { | |
collection.bulkWrite(updates); | |
break; | |
} catch(MongoException e) { | |
Neox.getNode().logError(e); // todo remove this just testing live | |
ThreadUtils.sleep(5000L); | |
} | |
} | |
} | |
private WriteModel<HiscoresDoc> makeUpdate(String key, long primary, long secondary, HiscoresDoc existing, Long gainStartMs) { | |
if(existing == null) { // insert required | |
return new InsertOneModel<>(HiscoresDoc.builder() | |
.userId(userId) | |
.gameMode(gameMode) | |
.xpMode(xpMode) | |
.buildKey(buildKey) | |
.key(key) | |
.primary(primary) | |
.secondary(secondary) | |
.lastUpdate(ms) | |
.build() | |
); | |
} | |
boolean increment = gainStartMs != null && existing.lastUpdate() >= gainStartMs; | |
if(increment) { | |
primary += existing.primary(); | |
secondary += existing.secondary(); | |
} | |
if(increment || existing.primary() != primary || existing.secondary() != secondary) { | |
return new UpdateOneModel<>( | |
Filters.eq("_id", existing._id()), | |
Updates.combine( | |
buildKey == null ? Updates.unset("build_key") : Updates.set("build_key", buildKey), | |
Updates.set("primary", primary), | |
Updates.set("secondary", secondary), | |
Updates.set("last_update", ms) | |
) | |
); | |
} | |
if(!Objects.equals(existing.buildKey(), buildKey)) { // update required (build key only) | |
return new UpdateOneModel<>( | |
Filters.eq("_id", existing._id()), | |
buildKey == null ? Updates.unset("build_key") : Updates.set("build_key", buildKey) | |
); | |
} | |
return null; | |
} | |
private List<WriteModel<HiscoresDoc>> makesGainsUpdates(Map<String, HiscoresDoc> existingMap, String type, List<Gain> gains, long gainStartMs) { | |
List<WriteModel<HiscoresDoc>> updates = new ArrayList<>(gains.size()); | |
gains.forEach(gain -> { | |
String key = gain.baseKey + "-" + type; | |
HiscoresDoc existing = existingMap.get(key); | |
WriteModel<HiscoresDoc> update = makeUpdate(key, gain.primary, gain.secondary, existing, gainStartMs); | |
if(update != null) | |
updates.add(update); | |
}); | |
return updates; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment