Files
Server/src/engine/gameManager/StaticManifest.java
T

246 lines
9.9 KiB
Java
Raw Normal View History

// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// 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) + "}";
}
}
}