Load static NPC guilds from the guild seed at world server boot

GuildManager.loadStaticGuilds builds the map's NPC guilds from
maps/<MB_WORLD_MAPID>.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 <noreply@anthropic.com>
This commit is contained in:
MagicBotandClaude Opus 5.5 committed 2026-10-27 21:15:09 -04:00
1 parent 4839883530
commit 42a6070d76
3 files changed
+152 -9

No files matched your search

+100
View File
@@ -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<Integer, Guild> _npcGuildsByKey = new HashMap<>();
public static final HashMap<Integer, Guild> _npcGuildsByNationKey = new HashMap<>();
public static final HashSet<Integer> _noGuildKeys = new HashSet<>();
// Static NPC guilds from maps/<map>.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<String, TreeSet<String>> missingKeys = new TreeMap<>();
ArrayList<CZoneProp> 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<CZoneProp> props, ArrayList<CZoneProp> 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);
}
+43 -9
View File
@@ -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<Integer, Condemned> guildCondemned = new ConcurrentHashMap<>();
public boolean wooWasModified;
public HashMap<Integer, GuildAlliances> 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;
+9
View File
@@ -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();