Files
Server/src/engine/gameManager/StaticManifest.java
T
MagicBotandClaude Opus 5.5 b83cde8141 Write the static ID manifest and check DB references at boot
nextStaticID now takes the object type and a natural manifest key, so no
static ID is handed out without a manifest entry. After the world load the
server diffs maps/<map>.manifest against the previous boot (moved, vanished,
re-targeted IDs), writes the new one (old copy kept as .manifest.prev), and
logs every negative reference in the Task 2.1 columns that no loaded static
object of the right type holds.

Value changes: none in play. Guilds are the only static IDs so far
(-2..-25, keys guild:<first seed key>).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:27:50 -04:00

247 lines
9.9 KiB
Java

// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.gameManager;
import engine.mbEnums.GameObjectType;
import engine.parsers.cache.CacheParserUtil;
import org.json.JSONArray;
import org.json.JSONObject;
import org.pmw.tinylog.Logger;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
// W2 safety net: every static ID with its natural key, in load order, written to maps/<map>.manifest at boot.
// Diffing against the previous boot's file shows IDs that moved; the orphan check shows DB references they broke.
public enum StaticManifest {
STATICMANIFEST;
private static final String mapPath = ConfigManager.DEFAULT_DATA_DIR + "maps/";
// Player data columns that can hold a static ID (findings §5, Task 2.1). A null type accepts any static object.
private static final String[][] referenceColumns = {
{"object", "parent", null},
{"obj_character", "char_bindBuilding", "Building"},
{"obj_character", "guildUID", "Guild"},
{"dyn_character_guildhistory", "GuildID", "Guild"},
{"dyn_guild_allianceenemylist", "OtherGuildID", "Guild"},
{"dyn_guild_guildkoslist", "KOSGuildID", "Guild"},
{"obj_mob", "mob_buildingID", "Building"},
{"obj_mob", "mob_guildUID", "Guild"},
{"obj_npc", "npc_buildingID", "Building"},
{"obj_npc", "npc_guildID", "Guild"},
{"obj_city", "treeOfLifeUUID", "Building"},
{"static_runegate_portals", "sourceBuilding", "Building"},
{"static_runegate_portals", "targetBuilding", "Building"},
{"obj_mine", "mine_buildingUID", "Building"}
};
private static final ArrayList<ManifestEntry> entries = new ArrayList<>();
private static final HashMap<Integer, ManifestEntry> entriesByID = new HashMap<>();
private static final HashMap<String, ManifestEntry> entriesByKey = new HashMap<>();
public static void addEntry(int staticID, GameObjectType objectType, String key) {
ManifestEntry entry = new ManifestEntry(staticID, objectType.name(), key);
if (entriesByKey.containsKey(key))
throw new IllegalStateException("Static manifest key " + key + " is used twice");
entries.add(entry);
entriesByID.put(staticID, entry);
entriesByKey.put(key, entry);
}
// Logs moved, vanished and re-targeted IDs against the previous manifest, then replaces it (old copy kept as .prev).
public static void writeManifest(int mapID) {
File manifestFile = new File(mapPath + mapID + ".manifest");
File previousFile = new File(mapPath + mapID + ".manifest.prev");
HashMap<String, ManifestEntry> previousByKey;
StringBuilder manifestText = new StringBuilder();
previousByKey = readManifest(manifestFile, mapID);
if (previousByKey != null)
diffManifest(previousByKey);
manifestText.append("{\n \"map\": ").append(mapID).append(",\n \"count\": ").append(entries.size())
.append(",\n \"statics\": [\n");
for (int i = 0; i < entries.size(); i++) {
manifestText.append(" ").append(entries.get(i).toJson());
manifestText.append(i < entries.size() - 1 ? ",\n" : "\n");
}
manifestText.append(" ]\n}\n");
try {
if (manifestFile.exists())
Files.copy(manifestFile.toPath(), previousFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
Files.write(manifestFile.toPath(), manifestText.toString().getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
Logger.error("Static manifest not written: " + manifestFile.getAbsolutePath() + ": " + e);
return;
}
Logger.info("Static manifest: " + entries.size() + " static IDs written to " + manifestFile.getName());
}
// Logs every negative DB reference (below -1, which means "none") that no loaded static object of the right type holds.
public static void logOrphanReferences() {
HashMap<Integer, Integer> negativeValues;
ManifestEntry entry;
String expectedType;
String problem;
int orphanColumns = 0;
int orphanRows = 0;
for (String[] referenceColumn : referenceColumns) {
expectedType = referenceColumn[2];
negativeValues = DbManager.StaticWorldQueries.GET_NEGATIVE_REFERENCES(referenceColumn[0], referenceColumn[1]);
if (negativeValues == null)
continue;
for (Map.Entry<Integer, Integer> negativeValue : negativeValues.entrySet()) {
entry = entriesByID.get(negativeValue.getKey());
if (entry == null)
problem = "no static object";
else if (DbManager.getFromCache(GameObjectType.valueOf(entry.type), entry.id) == null)
problem = entry.type + " " + entry.key + " is not loaded";
else if (expectedType != null && !expectedType.equals(entry.type))
problem = "points at " + entry.type + " " + entry.key + ", expected " + expectedType;
else
continue;
Logger.warn("Orphan reference: " + referenceColumn[0] + "." + referenceColumn[1] + " = "
+ negativeValue.getKey() + " on " + negativeValue.getValue() + " rows: " + problem);
orphanColumns++;
orphanRows += negativeValue.getValue();
}
}
Logger.info("Static orphan check: " + orphanColumns + " orphan values on " + orphanRows + " rows");
}
private static HashMap<String, ManifestEntry> readManifest(File manifestFile, int mapID) {
HashMap<String, ManifestEntry> previousByKey = new HashMap<>();
JSONObject manifestJson;
JSONArray staticsJson;
JSONObject entryJson;
if (!manifestFile.exists()) {
Logger.info("Static manifest: no previous " + manifestFile.getName() + ", nothing to diff");
return null;
}
try {
manifestJson = CacheParserUtil.readJsonFile(manifestFile);
if (manifestJson.getInt("map") != mapID) {
Logger.error(manifestFile.getName() + ": map " + manifestJson.getInt("map") + " does not match MB_WORLD_MAPID " + mapID);
return null;
}
staticsJson = manifestJson.getJSONArray("statics");
for (int i = 0; i < staticsJson.length(); i++) {
entryJson = staticsJson.getJSONObject(i);
previousByKey.put(entryJson.getString("key"),
new ManifestEntry(entryJson.getInt("id"), entryJson.getString("type"), entryJson.getString("key")));
}
} catch (Exception e) {
Logger.error("Previous static manifest unreadable, nothing to diff: " + manifestFile.getName() + ": " + e);
return null;
}
return previousByKey;
}
// A re-targeted ID is the dangerous case: player references to it now silently point at a different object.
private static void diffManifest(HashMap<String, ManifestEntry> previousByKey) {
HashMap<Integer, ManifestEntry> previousByID = new HashMap<>();
ManifestEntry currentEntry;
ManifestEntry previousEntry;
int moved = 0;
int vanished = 0;
int retargeted = 0;
int added = 0;
for (ManifestEntry entry : previousByKey.values())
previousByID.put(entry.id, entry);
for (ManifestEntry entry : previousByKey.values()) {
currentEntry = entriesByKey.get(entry.key);
if (currentEntry == null) {
Logger.warn("Static manifest: " + entry.type + " " + entry.key + " (was " + entry.id + ") vanished");
vanished++;
} else if (currentEntry.id != entry.id) {
Logger.warn("Static manifest: " + entry.type + " " + entry.key + " moved " + entry.id + " -> " + currentEntry.id);
moved++;
}
}
for (ManifestEntry entry : entries) {
previousEntry = previousByID.get(entry.id);
if (!previousByKey.containsKey(entry.key))
added++;
if (previousEntry != null && !previousEntry.key.equals(entry.key)) {
Logger.warn("Static manifest: ID " + entry.id + " re-targeted from " + previousEntry.type + " "
+ previousEntry.key + " to " + entry.type + " " + entry.key);
retargeted++;
}
}
Logger.info("Static manifest diff: " + moved + " moved, " + vanished + " vanished, " + retargeted
+ " re-targeted, " + added + " new");
}
private static class ManifestEntry {
private final int id;
private final String type;
private final String key;
private ManifestEntry(int id, String type, String key) {
this.id = id;
this.type = type;
this.key = key;
}
private String toJson() {
return "{\"id\": " + id + ", \"type\": " + JSONObject.quote(type) + ", \"key\": " + JSONObject.quote(key) + "}";
}
}
}