Skip to content

Instantly share code, notes, and snippets.

@NotArchon
Created May 3, 2023 01:30
Show Gist options
  • Save NotArchon/d0abf31457267720cbb42f1c271e690f to your computer and use it in GitHub Desktop.
Save NotArchon/d0abf31457267720cbb42f1c271e690f to your computer and use it in GitHub Desktop.
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