246 lines
9.9 KiB
Java
246 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) + "}";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|