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>
360 lines
14 KiB
Java
360 lines
14 KiB
Java
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
|
|
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
|
|
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
|
|
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
|
|
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
|
|
// Magicbane Emulator Project © 2013 - 2026
|
|
// https://www.magicbane.com
|
|
|
|
package engine.gameManager;
|
|
|
|
import com.zaxxer.hikari.HikariConfig;
|
|
import com.zaxxer.hikari.HikariDataSource;
|
|
import engine.db.handlers.*;
|
|
import engine.mbEnums;
|
|
import engine.mbEnums.GameObjectType;
|
|
import engine.objects.*;
|
|
import engine.server.MBServerStatics;
|
|
import engine.util.Hasher;
|
|
import org.pmw.tinylog.Logger;
|
|
|
|
import java.sql.Connection;
|
|
import java.sql.PreparedStatement;
|
|
import java.sql.SQLException;
|
|
import java.util.EnumMap;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
public enum DbManager {
|
|
DBMANAGER;
|
|
|
|
// Static world IDs count down from -2 (-1 means "not found"); runtime negative IDs start below them.
|
|
public static final int RUNTIME_ID_START = -1_000_000;
|
|
private static final AtomicInteger lastStaticID = new AtomicInteger(-1);
|
|
|
|
public static final dbAccountHandler AccountQueries = new dbAccountHandler();
|
|
public static final dbBaneHandler BaneQueries = new dbBaneHandler();
|
|
|
|
//Local Object Caching
|
|
public static final dbBaseClassHandler BaseClassQueries = new dbBaseClassHandler();
|
|
public static final dbBuildingHandler BuildingQueries = new dbBuildingHandler();
|
|
public static final dbBuildingLocationHandler BuildingLocationQueries = new dbBuildingLocationHandler();
|
|
public static final dbCharacterPowerHandler CharacterPowerQueries = new dbCharacterPowerHandler();
|
|
public static final dbCharacterRuneHandler CharacterRuneQueries = new dbCharacterRuneHandler();
|
|
public static final dbCharacterSkillHandler CharacterSkillQueries = new dbCharacterSkillHandler();
|
|
public static final dbCityHandler CityQueries = new dbCityHandler();
|
|
public static final dbContractHandler ContractQueries = new dbContractHandler();
|
|
public static final dbWarehouseHandler WarehouseQueries = new dbWarehouseHandler();
|
|
|
|
// Omg refactor this out, somebody!
|
|
public static final dbCSSessionHandler CSSessionQueries = new dbCSSessionHandler();
|
|
public static final dbEnchantmentHandler EnchantmentQueries = new dbEnchantmentHandler();
|
|
public static final dbGuildHandler GuildQueries = new dbGuildHandler();
|
|
public static final dbItemHandler ItemQueries = new dbItemHandler();
|
|
public static final dbLootHandler LootQueries = new dbLootHandler();
|
|
public static final dbMenuHandler MenuQueries = new dbMenuHandler();
|
|
public static final dbMineHandler MineQueries = new dbMineHandler();
|
|
public static final dbMobHandler MobQueries = new dbMobHandler();
|
|
public static final dbMobBaseHandler MobBaseQueries = new dbMobBaseHandler();
|
|
public static final dbNPCHandler NPCQueries = new dbNPCHandler();
|
|
public static final dbPlayerCharacterHandler PlayerCharacterQueries = new dbPlayerCharacterHandler();
|
|
public static final dbPromotionClassHandler PromotionQueries = new dbPromotionClassHandler();
|
|
public static final dbRaceHandler RaceQueries = new dbRaceHandler();
|
|
public static final dbRuneBaseHandler RuneBaseQueries = new dbRuneBaseHandler();
|
|
public static final dbSkillBaseHandler SkillsBaseQueries = new dbSkillBaseHandler();
|
|
public static final dbSkillReqHandler SkillReqQueries = new dbSkillReqHandler();
|
|
public static final dbVendorDialogHandler VendorDialogQueries = new dbVendorDialogHandler();
|
|
public static final dbZoneHandler ZoneQueries = new dbZoneHandler();
|
|
public static final dbRealmHandler RealmQueries = new dbRealmHandler();
|
|
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);
|
|
public static Hasher hasher;
|
|
private static HikariDataSource connectionPool = null;
|
|
|
|
// Only the static world factories may call this, in the fixed load order (W2); never renumber.
|
|
// 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;
|
|
}
|
|
|
|
// W3: static world objects never reach the DB. Logs the refused handler method and its caller.
|
|
public static boolean refuseStaticWrite(AbstractWorldObject worldObject) {
|
|
|
|
StackTraceElement[] stack;
|
|
String caller;
|
|
int frame = 1;
|
|
|
|
if (worldObject == null || !worldObject.isStaticWorldObject())
|
|
return false;
|
|
|
|
stack = new Throwable().getStackTrace();
|
|
|
|
while (frame < stack.length - 1 && stack[frame].getClassName().equals(DbManager.class.getName()))
|
|
frame++;
|
|
|
|
caller = (frame + 1 < stack.length) ? stack[frame + 1].toString() : "unknown";
|
|
|
|
Logger.error("Refused DB write for static " + worldObject.getObjectType() + " " + worldObject.getObjectUUID()
|
|
+ " in " + stack[frame] + " called from " + caller);
|
|
return true;
|
|
}
|
|
|
|
public static boolean refuseStaticWrite(GameObjectType objectType, long objectUUID) {
|
|
|
|
AbstractGameObject gameObject = getFromCache(objectType, (int) objectUUID);
|
|
|
|
if (gameObject instanceof AbstractWorldObject)
|
|
return refuseStaticWrite((AbstractWorldObject) gameObject);
|
|
|
|
return false;
|
|
}
|
|
|
|
public static AbstractGameObject getObject(GameObjectType objectType, int objectUUID) {
|
|
|
|
AbstractGameObject outObject = null;
|
|
|
|
switch (objectType) {
|
|
case PlayerCharacter:
|
|
outObject = PlayerCharacter.getPlayerCharacter(objectUUID);
|
|
break;
|
|
case NPC:
|
|
outObject = NPC.getNPC(objectUUID);
|
|
break;
|
|
case Mob:
|
|
outObject = Mob.getFromCache(objectUUID);
|
|
break;
|
|
case Building:
|
|
outObject = BuildingManager.getBuilding(objectUUID);
|
|
break;
|
|
case Guild:
|
|
outObject = Guild.getGuild(objectUUID);
|
|
break;
|
|
case Item:
|
|
outObject = Item.getFromCache(objectUUID);
|
|
break;
|
|
case MobLoot:
|
|
outObject = MobLoot.getFromCache(objectUUID);
|
|
break;
|
|
case City:
|
|
outObject = City.getCity(objectUUID);
|
|
break;
|
|
default:
|
|
Logger.error("Attempt to retrieve nonexistant " + objectType +
|
|
" from object cache.");
|
|
break;
|
|
|
|
}
|
|
|
|
return outObject;
|
|
}
|
|
|
|
public static boolean inCache(GameObjectType gameObjectType, int uuid) {
|
|
|
|
if (objectCache.get(gameObjectType) == null)
|
|
return false;
|
|
|
|
return (objectCache.get(gameObjectType).containsKey(uuid));
|
|
|
|
}
|
|
|
|
public static AbstractGameObject getFromCache(GameObjectType gameObjectType, int uuid) {
|
|
|
|
if (objectCache.get(gameObjectType) == null)
|
|
return null;
|
|
|
|
return objectCache.get(gameObjectType).get(uuid);
|
|
|
|
}
|
|
|
|
public static void removeFromCache(GameObjectType gameObjectType, int uuid) {
|
|
|
|
AbstractGameObject abstractGameObject;
|
|
|
|
if (objectCache.get(gameObjectType) == null)
|
|
return;
|
|
|
|
abstractGameObject = objectCache.get(gameObjectType).get(uuid);
|
|
|
|
if (abstractGameObject == null)
|
|
return;
|
|
|
|
removeFromCache(abstractGameObject);
|
|
|
|
}
|
|
|
|
public static void removeFromCache(AbstractGameObject abstractGameObject) {
|
|
|
|
if (abstractGameObject == null)
|
|
return;
|
|
|
|
if (objectCache.get(abstractGameObject.getObjectType()) == null)
|
|
return;
|
|
|
|
// Remove object from game cache
|
|
|
|
objectCache.get(abstractGameObject.getObjectType()).remove(abstractGameObject.getObjectUUID());
|
|
|
|
// Release bounds as we're dispensing with this object.
|
|
|
|
if (abstractGameObject instanceof AbstractWorldObject) {
|
|
AbstractWorldObject abstractWorldObject = (AbstractWorldObject) abstractGameObject;
|
|
|
|
if (abstractWorldObject.getBounds() != null) {
|
|
abstractWorldObject.getBounds().release();
|
|
abstractWorldObject.setBounds(null);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public static boolean addToCache(AbstractGameObject gameObject) {
|
|
|
|
boolean isWorldServer = ConfigManager.serverType.equals(mbEnums.ServerType.WORLDSERVER);
|
|
|
|
if (!isWorldServer) {
|
|
if (MBServerStatics.SKIP_CACHE_LOGIN)
|
|
return true;
|
|
if (MBServerStatics.SKIP_CACHE_LOGIN_PLAYER
|
|
&& (gameObject.getObjectType() == GameObjectType.PlayerCharacter))
|
|
return true;
|
|
if (MBServerStatics.SKIP_CACHE_LOGIN_ITEM &&
|
|
(gameObject.getObjectType() == GameObjectType.Item))
|
|
return true;
|
|
}
|
|
|
|
// First time this object type has been cached. Create the hashmap.
|
|
|
|
if (objectCache.get(gameObject.getObjectType()) == null) {
|
|
|
|
int initialCapacity;
|
|
|
|
// Provide initial sizing hints
|
|
|
|
switch (gameObject.getObjectType()) {
|
|
case Building:
|
|
initialCapacity = 46900;
|
|
break;
|
|
case Mob:
|
|
initialCapacity = 11700;
|
|
break;
|
|
case NPC:
|
|
initialCapacity = 900;
|
|
break;
|
|
case Zone:
|
|
initialCapacity = 1070;
|
|
break;
|
|
case Account:
|
|
initialCapacity = 10000;
|
|
break;
|
|
case Guild:
|
|
initialCapacity = 100;
|
|
break;
|
|
case ItemContainer:
|
|
initialCapacity = 100;
|
|
break;
|
|
case Item:
|
|
initialCapacity = 1000;
|
|
break;
|
|
case MobLoot:
|
|
initialCapacity = 10000;
|
|
break;
|
|
case PlayerCharacter:
|
|
initialCapacity = 100;
|
|
break;
|
|
default:
|
|
initialCapacity = 100; // Lookup api default should be ok for small maps
|
|
break;
|
|
}
|
|
objectCache.put(gameObject.getObjectType(), new ConcurrentHashMap<>(initialCapacity));
|
|
}
|
|
|
|
// Add the object to the cache. This will overwrite the current map entry.
|
|
|
|
objectCache.get(gameObject.getObjectType()).put(gameObject.getObjectUUID(), gameObject);
|
|
|
|
return true;
|
|
}
|
|
|
|
public static java.util.Collection<AbstractGameObject> getList(GameObjectType gameObjectType) {
|
|
|
|
if (objectCache.get(gameObjectType) == null)
|
|
return null;
|
|
|
|
return objectCache.get(gameObjectType).values();
|
|
}
|
|
|
|
public static PreparedStatement prepareStatement(String sql) throws SQLException {
|
|
return getConnection().prepareStatement(sql, 1);
|
|
}
|
|
|
|
public static ConcurrentHashMap<Integer, AbstractGameObject> getMap(
|
|
GameObjectType gameObjectType) {
|
|
|
|
if (objectCache.get(gameObjectType) == null)
|
|
return null;
|
|
|
|
return objectCache.get(gameObjectType);
|
|
|
|
}
|
|
|
|
/**
|
|
* @return the conn
|
|
*/
|
|
//XXX I think we have a severe resource leak here! No one is putting the connections back!
|
|
public static Connection getConnection() {
|
|
try {
|
|
return DbManager.connectionPool.getConnection();
|
|
} catch (SQLException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
public static void configureConnectionPool() {
|
|
|
|
HikariConfig config = new HikariConfig();
|
|
|
|
// Magicbane requires at least 15 db connections min to boot.
|
|
|
|
int connectionCount = Math.max(15, Runtime.getRuntime().availableProcessors() * 2) + 1;
|
|
config.setMaximumPoolSize(connectionCount);
|
|
|
|
config.setJdbcUrl("jdbc:mysql://" + ConfigManager.MB_DATABASE_ADDRESS.getValue() +
|
|
":" + ConfigManager.MB_DATABASE_PORT.getValue() + "/" +
|
|
ConfigManager.MB_DATABASE_NAME.getValue());
|
|
config.setUsername(ConfigManager.MB_DATABASE_USER.getValue());
|
|
config.setPassword(ConfigManager.MB_DATABASE_PASS.getValue());
|
|
|
|
// Must be set lower than SQL server connection lifetime!
|
|
|
|
config.addDataSourceProperty("maxLifetime", "3600000");
|
|
|
|
config.addDataSourceProperty("characterEncoding", "utf8");
|
|
|
|
config.addDataSourceProperty("useServerPrepStmts", "true");
|
|
config.addDataSourceProperty("cachePrepStmts", "true");
|
|
config.addDataSourceProperty("prepStmtCacheSize", "500");
|
|
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
|
|
|
|
config.addDataSourceProperty("leakDetectionThreshold", "5000");
|
|
config.addDataSourceProperty("cacheServerConfiguration", "true");
|
|
|
|
connectionPool = new HikariDataSource(config); // setup the connection pool
|
|
|
|
Logger.info("Database configured with " + connectionCount + " connections");
|
|
}
|
|
|
|
}
|