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>
This commit is contained in:
1 parent
3b4a60c739
commit
b83cde8141
5 files changed
+303
-2
No files matched your search
@@ -0,0 +1,47 @@
|
||||
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
|
||||
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
|
||||
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
|
||||
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
|
||||
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
|
||||
// Magicbane Emulator Project © 2013 - 2026
|
||||
// https://www.magicbane.com
|
||||
|
||||
package engine.db.handlers;
|
||||
|
||||
import engine.gameManager.DbManager;
|
||||
import org.pmw.tinylog.Logger;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class dbStaticWorldHandler extends dbHandlerBase {
|
||||
|
||||
public dbStaticWorldHandler() {
|
||||
|
||||
}
|
||||
|
||||
// Table and column come from StaticManifest's fixed list, never from input. -1 means "none" and is skipped.
|
||||
public HashMap<Integer, Integer> GET_NEGATIVE_REFERENCES(String table, String column) {
|
||||
|
||||
HashMap<Integer, Integer> rowsByValue = new HashMap<>();
|
||||
|
||||
try (Connection connection = DbManager.getConnection();
|
||||
PreparedStatement preparedStatement = connection.prepareStatement("SELECT `" + column + "` AS `value`, COUNT(*) AS `rows` FROM `"
|
||||
+ table + "` WHERE `" + column + "` < -1 GROUP BY `" + column + "`;")) {
|
||||
|
||||
ResultSet rs = preparedStatement.executeQuery();
|
||||
|
||||
while (rs.next())
|
||||
rowsByValue.put(rs.getInt("value"), rs.getInt("rows"));
|
||||
|
||||
} catch (SQLException e) {
|
||||
Logger.error(table + "." + column + ": " + e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return rowsByValue;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ public enum DbManager {
|
||||
public static final dbBlueprintHandler BlueprintQueries = new dbBlueprintHandler();
|
||||
public static final dbShrineHandler ShrineQueries = new dbShrineHandler();
|
||||
public static final dbRunegateHandler RunegateQueries = new dbRunegateHandler();
|
||||
public static final dbStaticWorldHandler StaticWorldQueries = new dbStaticWorldHandler();
|
||||
|
||||
public static final dbPetitionHandler PetitionQueries = new dbPetitionHandler();
|
||||
private static final EnumMap<GameObjectType, ConcurrentHashMap<Integer, AbstractGameObject>> objectCache = new EnumMap<>(GameObjectType.class);
|
||||
@@ -76,13 +77,16 @@ public enum DbManager {
|
||||
private static HikariDataSource connectionPool = null;
|
||||
|
||||
// Only the static world factories may call this, in the fixed load order (W2); never renumber.
|
||||
public static int nextStaticID() {
|
||||
// The key names the object from the data files alone (findings §5, manifest format) and must be unique.
|
||||
public static int nextStaticID(GameObjectType objectType, String manifestKey) {
|
||||
|
||||
int staticID = lastStaticID.decrementAndGet();
|
||||
|
||||
if (staticID <= RUNTIME_ID_START)
|
||||
throw new IllegalStateException("Static world ID " + staticID + " reached the runtime ID range at " + RUNTIME_ID_START);
|
||||
|
||||
StaticManifest.addEntry(staticID, objectType, manifestKey);
|
||||
|
||||
return staticID;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
|
||||
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
|
||||
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
|
||||
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
|
||||
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
|
||||
// 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) + "}";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -176,7 +176,8 @@ public class Guild extends AbstractWorldObject {
|
||||
|
||||
Guild guild = new Guild();
|
||||
|
||||
guild.setObjectUUID(DbManager.nextStaticID());
|
||||
guild.setObjectUUID(DbManager.nextStaticID(GameObjectType.Guild,
|
||||
"guild:" + Integer.toUnsignedString(seedEntry.guildKeys.get(0))));
|
||||
guild.setStaticWorldObject(true);
|
||||
guild.name = seedEntry.name;
|
||||
guild.charter = seedEntry.charter;
|
||||
|
||||
@@ -578,6 +578,9 @@ public class WorldServer {
|
||||
Logger.info("Loading all Mobiles");
|
||||
DbManager.MobQueries.GET_ALL_MOBS();
|
||||
|
||||
StaticManifest.writeManifest(worldMapID);
|
||||
StaticManifest.logOrphanReferences();
|
||||
|
||||
Logger.info("time to load World Objects: " + (System.currentTimeMillis() - start) + " ms");
|
||||
|
||||
// Configure realms for serialization
|
||||
|
||||
Reference in new issue
Block a user