From 42a6070d76ebe7a1d3e4772f3da33007066e5280 Mon Sep 17 00:00:00 2001 From: MagicBot Date: Tue, 27 Oct 2026 21:15:09 -0400 Subject: [PATCH] Load static NPC guilds from the guild seed at world server boot GuildManager.loadStaticGuilds builds the map's NPC guilds from maps/.guild through Guild.createStaticGuild, before zones, so they take the first static IDs (-2 down, file order). Each is flagged static, is its own nation (as live and in the DB) and is indexed by its CZONE guild and nation keys. Boot aborts on a missing or bad seed file or a duplicate key. After zones load, every CITY-prop guild key with no seed entry is logged once with its zones; known no-guild keys are only counted. Nothing references the static guilds yet: NPC cities, NPCs and players still use the DB guilds (UID 2-25) until 2.5, so there is no gameplay change. Seed vs the 24 DB NPC guilds (scripts/task16_guild_seed.py): name, crest, leadership type, motto and repledge/teleport limits identical. Charter differs on all 24: DB 562 (BARBARIAN placeholder) -> guilddata value, read as the client guild type index (sb.exe g_GuildTypeNames 0x175B8E0), e.g. Kingscross NOBLE, Bastion CATHEDRAL, Khar Th'Sekt VIRAKT, hamlets MILITARY. Static guilds have no warehouse hash yet. Co-Authored-By: Claude Opus 5.5 --- src/engine/gameManager/GuildManager.java | 100 +++++++++++++++++++++++ src/engine/objects/Guild.java | 52 ++++++++++-- src/engine/server/world/WorldServer.java | 9 ++ 3 files changed, 152 insertions(+), 9 deletions(-) diff --git a/src/engine/gameManager/GuildManager.java b/src/engine/gameManager/GuildManager.java index a454a154..81b9d5d4 100644 --- a/src/engine/gameManager/GuildManager.java +++ b/src/engine/gameManager/GuildManager.java @@ -16,7 +16,18 @@ import engine.net.client.ClientConnection; import engine.net.client.msg.guild.AcceptInviteToGuildMsg; import engine.net.client.msg.guild.GuildInfoMsg; import engine.objects.*; +import engine.parsers.map.GuildSeedParser; +import engine.parsers.objects.CZoneProp; +import engine.parsers.objects.GuildSeed; +import engine.parsers.objects.GuildSeedEntry; import org.joda.time.DateTime; +import org.pmw.tinylog.Logger; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.TreeMap; +import java.util.TreeSet; public enum GuildManager { @@ -33,6 +44,95 @@ public enum GuildManager { public static final int CREST_RESERVED = 153; //153: Guild crest is reserved public static final int CREST_COLOR_ERROR = 154; //154: All three crest colors cannot be the same + // NPC guilds by CZONE city_prop_gm_guild / city_prop_gm_nation key (W7) + public static final HashMap _npcGuildsByKey = new HashMap<>(); + public static final HashMap _npcGuildsByNationKey = new HashMap<>(); + public static final HashSet _noGuildKeys = new HashSet<>(); + + // Static NPC guilds from maps/.guild. Loaded before zones, so they take the first static IDs. False aborts boot. + + public static boolean loadStaticGuilds(int mapID) { + + GuildSeed guildSeed; + Guild guild; + + guildSeed = GuildSeedParser.parseGuildSeed(mapID); + + if (guildSeed == null) + return false; + + for (GuildSeedEntry seedEntry : guildSeed.guilds) { + + guild = Guild.createStaticGuild(seedEntry); + DbManager.addToCache(guild); + + for (int guildKey : seedEntry.guildKeys) + if (_npcGuildsByKey.put(guildKey, guild) != null || guildSeed.noGuildKeys.contains(guildKey)) { + Logger.error("Guild seed: guild key " + String.format("%08X", guildKey) + " is used twice"); + return false; + } + + if (_npcGuildsByNationKey.put(seedEntry.nationKey, guild) != null) { + Logger.error("Guild seed: nation key " + String.format("%08X", seedEntry.nationKey) + " is used twice"); + return false; + } + } + + _noGuildKeys.addAll(guildSeed.noGuildKeys); + + Logger.info("NPC guilds: " + guildSeed.guilds.size() + " static, " + _npcGuildsByKey.size() + " guild keys"); + + return true; + } + + // Logs each CITY-prop guild key on the map that has no seed entry; known no-guild keys are only counted. + + public static void logMissingGuildKeys() { + + TreeMap> missingKeys = new TreeMap<>(); + ArrayList cityProps = new ArrayList<>(); + int noGuildProps = 0; + + for (Zone zone : ZoneManager.getAllZones()) { + + if (!zone.isStaticWorldObject() || zone.template == null) + continue; + + cityProps.clear(); + collectCityProps(zone.template.zone_prop_info, cityProps); + + for (CZoneProp cityProp : cityProps) { + + if (_npcGuildsByKey.containsKey(cityProp.city_prop_gm_guild)) + continue; + + if (_noGuildKeys.contains(cityProp.city_prop_gm_guild)) { + noGuildProps++; + continue; + } + + missingKeys.computeIfAbsent(String.format("%08X", cityProp.city_prop_gm_guild), key -> new TreeSet<>()) + .add(zone.zoneName); + } + } + + for (String guildKey : missingKeys.keySet()) + Logger.warn("CITY prop guild key " + guildKey + " has no guild seed entry; zones: " + missingKeys.get(guildKey)); + + Logger.info("CITY props: " + noGuildProps + " with no guild, " + missingKeys.size() + " guild keys missing from the seed"); + } + + private static void collectCityProps(ArrayList props, ArrayList cityProps) { + + for (CZoneProp prop : props) { + + if (prop.prop_type == mbEnums.ZonePropType.CITY) + cityProps.add(prop); + + collectCityProps(prop.prop_content_props, cityProps); + } + } + public static boolean joinGuild(PlayerCharacter pc, Guild guild, GuildHistoryType historyType) { return joinGuild(pc, guild, 0, historyType); } diff --git a/src/engine/objects/Guild.java b/src/engine/objects/Guild.java index 3e86c12d..52c001b7 100644 --- a/src/engine/objects/Guild.java +++ b/src/engine/objects/Guild.java @@ -21,6 +21,7 @@ import engine.net.client.msg.AllianceChangeMsg; import engine.net.client.msg.ErrorPopupMsg; import engine.net.client.msg.UpdateClientAlliancesMsg; import engine.net.client.msg.guild.GuildInfoMsg; +import engine.parsers.objects.GuildSeedEntry; import engine.server.world.WorldServer; import org.joda.time.DateTime; import org.pmw.tinylog.Logger; @@ -35,15 +36,15 @@ public class Guild extends AbstractWorldObject { private static Guild errantGuild; private static Guild errantNation; - private final String name; - private final GuildTag guildTag; - public final GuildCharterType charter; - private final String leadershipType; // Have to see how this is sent to the client - private final int repledgeMin; - private final int repledgeMax; - private final int repledgeKick; - private final int teleportMin; - private final int teleportMax; + private String name; + private GuildTag guildTag; + public GuildCharterType charter; + private String leadershipType; // Have to see how this is sent to the client + private int repledgeMin; + private int repledgeMax; + private int repledgeKick; + private int teleportMin; + private int teleportMax; private final ConcurrentHashMap guildCondemned = new ConcurrentHashMap<>(); public boolean wooWasModified; public HashMap guildAlliances = new HashMap<>(); @@ -67,6 +68,13 @@ public class Guild extends AbstractWorldObject { private String hash; private boolean ownerIsNPC; + /** + * No-arg Constructor + */ + private Guild() { + super(); + } + /** * No Id Constructor */ @@ -163,6 +171,32 @@ public class Guild extends AbstractWorldObject { this.hash = rs.getString("hash"); } + // Every NPC guild is its own nation, as live. No hash: it only feeds warehouse records, which are not static-safe yet. + public static Guild createStaticGuild(GuildSeedEntry seedEntry) { + + Guild guild = new Guild(); + + guild.setObjectUUID(DbManager.nextStaticID()); + guild.setStaticWorldObject(true); + guild.name = seedEntry.name; + guild.charter = seedEntry.charter; + guild.leadershipType = seedEntry.leadershipType; + guild.guildTag = new GuildTag(seedEntry.crest[0], seedEntry.crest[1], seedEntry.crest[2], + seedEntry.crest[4], seedEntry.crest[3]); + guild.motto = seedEntry.motto; + guild.repledgeMin = seedEntry.repledgeMin; + guild.repledgeMax = seedEntry.repledgeMax; + guild.repledgeKick = seedEntry.repledgeKick; + guild.teleportMin = seedEntry.teleportMin; + guild.teleportMax = seedEntry.teleportMax; + guild.banishList = new ArrayList<>(); + guild.subGuildList = new ArrayList<>(); + guild.nation = guild; + guild.guildState = GuildManager.calcuateGuildState(guild); + + return guild; + } + public static AbstractCharacter GetGL(Guild guild) { if (guild == null) return null; diff --git a/src/engine/server/world/WorldServer.java b/src/engine/server/world/WorldServer.java index 4c6c55e1..fff428c0 100644 --- a/src/engine/server/world/WorldServer.java +++ b/src/engine/server/world/WorldServer.java @@ -541,6 +541,13 @@ public class WorldServer { long start = System.currentTimeMillis(); + Logger.info("Loading NPC guilds"); + + if (!GuildManager.loadStaticGuilds(worldMapID)) { + Logger.error("ABORT! Guild seed failed to load!"); + exitServer(1); + } + Logger.info("Loading all Zones"); if (!ZoneManager.loadZones(worldMapID)) { @@ -548,6 +555,8 @@ public class WorldServer { exitServer(1); } + GuildManager.logMissingGuildKeys(); + Logger.info("Loading all Buildings"); BuildingManager._npcBuildingOwners = DbManager.BuildingQueries.GET_NPC_BUILDING_OWNERS(); DbManager.BuildingQueries.GET_ALL_BUILDINGS();