From ac1fcaecf5f1eef67a90984e2d5a8ea4b7b55907 Mon Sep 17 00:00:00 2001 From: MagicBot Date: Tue, 27 Oct 2026 19:11:45 -0400 Subject: [PATCH] Parse CZONE zone templates at world server boot ZoneParser reads mb.data/CZONE/.json into CZoneTemplate with its props, child props, mobiles and point sets (World-Boot-Plan Task 1.1). Only the agreed server-needed fields are parsed; textures, water, weather, sound and always-default fields are skipped. Boot aborts if any file fails. Nothing uses the data yet (Task 1.2). New mbEnums: ZoneShapeType, SeaLevelType, TerrainGenType, ZonePropType, ZoneMobileType, PointSetType. root FSM maps to MobBehaviourType and monster types to MonsterType. Value changes: none (load only). Co-Authored-By: Claude Opus 5.5 --- src/engine/mbEnums.java | 35 ++ src/engine/parsers/cache/ZoneParser.java | 336 ++++++++++++++++++ src/engine/parsers/objects/CZoneDrop.java | 24 ++ src/engine/parsers/objects/CZoneMobile.java | 59 +++ .../parsers/objects/CZonePlacement.java | 32 ++ src/engine/parsers/objects/CZonePointSet.java | 36 ++ src/engine/parsers/objects/CZoneProp.java | 39 ++ src/engine/parsers/objects/CZoneTemplate.java | 51 +++ src/engine/server/world/WorldServer.java | 10 + 9 files changed, 622 insertions(+) create mode 100644 src/engine/parsers/cache/ZoneParser.java create mode 100644 src/engine/parsers/objects/CZoneDrop.java create mode 100644 src/engine/parsers/objects/CZoneMobile.java create mode 100644 src/engine/parsers/objects/CZonePlacement.java create mode 100644 src/engine/parsers/objects/CZonePointSet.java create mode 100644 src/engine/parsers/objects/CZoneProp.java create mode 100644 src/engine/parsers/objects/CZoneTemplate.java diff --git a/src/engine/mbEnums.java b/src/engine/mbEnums.java index 90918c5d..f0c130b1 100644 --- a/src/engine/mbEnums.java +++ b/src/engine/mbEnums.java @@ -3147,6 +3147,41 @@ public class mbEnums { MULTIPLY } + // CZONE value sets; only the values present in the shipped cache. + + public enum ZoneShapeType { + ELLIPTICAL, + RECTANGULAR + } + + public enum SeaLevelType { + PARENT, + SELF, + WORLD + } + + // NONE is the server's marker for a zone with no zone_terrain_gen block + + public enum TerrainGenType { + NONE, + TARGA, + PLANAR + } + + public enum ZonePropType { + PROP, + CITY + } + + public enum ZoneMobileType { + MOBILE, + TRAINER + } + + public enum PointSetType { + POINTS_ORDEREDREV + } + public enum ServerConfig { MBDB_108("AERYNTH", "ARAC", "MOURNING", 300000), MBDB_192("DALGOTH", "ARAC", "GOOK", 300001), diff --git a/src/engine/parsers/cache/ZoneParser.java b/src/engine/parsers/cache/ZoneParser.java new file mode 100644 index 00000000..80b180c4 --- /dev/null +++ b/src/engine/parsers/cache/ZoneParser.java @@ -0,0 +1,336 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.cache; + +import engine.gameManager.ConfigManager; +import engine.mbEnums; +import engine.parsers.objects.CZoneDrop; +import engine.parsers.objects.CZoneMobile; +import engine.parsers.objects.CZonePlacement; +import engine.parsers.objects.CZonePointSet; +import engine.parsers.objects.CZoneProp; +import engine.parsers.objects.CZoneTemplate; +import org.json.JSONArray; +import org.json.JSONObject; +import org.pmw.tinylog.Logger; + +import java.io.File; +import java.util.HashMap; + +// Loads zone templates from the client cache JSON (CZONE/.json). + +public class ZoneParser { + + private static final String zonePath = ConfigManager.DEFAULT_DATA_DIR + "CZONE/"; + + public static HashMap parseZoneFolder() { + + HashMap zoneTemplates = new HashMap<>(); + File[] zoneFiles = CacheParserUtil.listJsonFiles(zonePath); + int templateId; + JSONObject zoneJson; + int failedCount = 0; + + for (File zoneFile : zoneFiles) { + try { + templateId = CacheParserUtil.templateIdFromFile(zoneFile); + zoneJson = CacheParserUtil.readJsonFile(zoneFile); + zoneTemplates.put(templateId, parseZoneEntry(templateId, zoneJson)); + } catch (Exception e) { + Logger.error(zoneFile.getName() + ": " + e); + failedCount++; + } + } + + Logger.info("Loaded " + zoneTemplates.size() + " of " + zoneFiles.length + " zone templates"); + logCounts(zoneTemplates); + + // Partial zone data is never usable; null tells the caller to abort + if (failedCount > 0) + return null; + + return zoneTemplates; + } + + private static CZoneTemplate parseZoneEntry(int templateId, JSONObject zoneJson) { + + CZoneTemplate zone = new CZoneTemplate(); + JSONObject terrainJson; + JSONArray propsJson; + JSONArray mobilesJson; + JSONArray pointSetsJson; + + zone.template_id = templateId; + zone.zone_type = CacheParserUtil.readEnum(mbEnums.ZoneShapeType.class, zoneJson.getString("zone_type"), templateId, "zone_type"); + zone.zone_name = zoneJson.getString("zone_name"); + zone.zone_peace_zone = zoneJson.getBoolean("zone_peace_zone"); + zone.zone_major_radius = zoneJson.getFloat("zone_major_radius"); + zone.zone_minor_radius = zoneJson.getFloat("zone_minor_radius"); + zone.zone_min_blend = zoneJson.getFloat("zone_min_blend"); + zone.zone_max_blend = zoneJson.getFloat("zone_max_blend"); + zone.zone_sea_level = zoneJson.getFloat("zone_sea_level"); + zone.zone_sea_level_type = CacheParserUtil.readEnum(mbEnums.SeaLevelType.class, zoneJson.getString("zone_sea_level_type"), + templateId, "zone_sea_level_type"); + zone.zone_has_water = zoneJson.getBoolean("zone_has_water"); + + zone.zone_has_terrain_gen = zoneJson.getBoolean("zone_has_terrain_gen"); + zone.terrain_type = mbEnums.TerrainGenType.NONE; + + if (zone.zone_has_terrain_gen) { + terrainJson = zoneJson.getJSONObject("zone_terrain_gen"); + zone.terrain_type = CacheParserUtil.readEnum(mbEnums.TerrainGenType.class, terrainJson.getString("terrain_type"), + templateId, "terrain_type"); + + if (zone.terrain_type == mbEnums.TerrainGenType.TARGA) { + zone.terrain_max_y = terrainJson.getFloat("terrain_max_y"); + zone.terrain_image = terrainJson.getInt("terrain_image"); + } + } + + propsJson = zoneJson.getJSONArray("zone_prop_info"); + + for (int i = 0; i < propsJson.length(); i++) + zone.zone_prop_info.add(parseProp(propsJson.getJSONObject(i), templateId)); + + mobilesJson = zoneJson.getJSONArray("zone_mobile_info"); + + for (int i = 0; i < mobilesJson.length(); i++) + zone.zone_mobile_info.add(parseMobile(mobilesJson.getJSONObject(i), templateId)); + + pointSetsJson = zoneJson.getJSONArray("zone_point_sets"); + + for (int i = 0; i < pointSetsJson.length(); i++) + zone.zone_point_sets.add(parsePointSet(pointSetsJson.getJSONObject(i), templateId)); + + return zone; + } + + private static CZonePlacement parsePlacement(JSONObject placementJson, int templateId) { + + CZonePlacement placement = new CZonePlacement(); + JSONObject sparseJson; + + placement.basic_zone_spawn_location = CacheParserUtil.readVector3(placementJson.getJSONArray("basic_zone_spawn_location")); + placement.basic_zone_name_override = placementJson.getString("basic_zone_name_override"); + placement.basic_zone_time_to_respawn = placementJson.getFloat("basic_zone_time_to_respawn"); + placement.basic_zone_spawn_radius = placementJson.getFloat("basic_zone_spawn_radius"); + placement.basic_zone_y_rot = placementJson.getFloat("basic_zone_y_rot"); + placement.basic_zone_template_id = (int) placementJson.getLong("basic_zone_template_id"); + placement.basic_zone_level_number = placementJson.getInt("basic_zone_level_number"); + placement.basic_zone_room_number = placementJson.getInt("basic_zone_room_number"); + + sparseJson = placementJson.getJSONObject("basic_zone_sparse_data"); + + for (String key : sparseJson.keySet()) { + if (!key.equals("IMMUNE_TO_STEAL")) + throw new IllegalArgumentException(templateId + ": basic_zone_sparse_data has unknown key '" + key + "'"); + + placement.basic_zone_immune_to_steal = sparseJson.getBoolean(key); + } + + return placement; + } + + private static CZoneProp parseProp(JSONObject propJson, int templateId) { + + CZoneProp prop = new CZoneProp(); + JSONObject dataJson; + JSONArray childrenJson; + JSONArray mobilesJson; + + prop.prop_type = CacheParserUtil.readEnum(mbEnums.ZonePropType.class, propJson.getString("prop_type"), templateId, "prop_type"); + + dataJson = propJson.getJSONObject("prop_data"); + prop.prop_basic_zone = parsePlacement(dataJson.getJSONObject("prop_basic_zone"), templateId); + + childrenJson = dataJson.getJSONArray("prop_content_props"); + + for (int i = 0; i < childrenJson.length(); i++) + prop.prop_content_props.add(parseProp(childrenJson.getJSONObject(i), templateId)); + + mobilesJson = dataJson.getJSONArray("prop_content_mobiles"); + + for (int i = 0; i < mobilesJson.length(); i++) + prop.prop_content_mobiles.add(parseMobile(mobilesJson.getJSONObject(i), templateId)); + + prop.prop_registry = dataJson.getInt("prop_registry"); + + if (prop.prop_registry != 0) + prop.prop_registry_name = dataJson.getString("prop_registry_name"); + + prop.prop_has_teleporter = dataJson.getBoolean("prop_has_teleporter"); + + if (prop.prop_has_teleporter) + prop.prop_teleporter = (int) dataJson.getLong("prop_teleporter"); + + if (prop.prop_type == mbEnums.ZonePropType.CITY) { + prop.city_prop_gm_guild = (int) dataJson.getLong("city_prop_gm_guild"); + prop.city_prop_gm_nation = (int) dataJson.getLong("city_prop_gm_nation"); + prop.city_prop_asset_rank = dataJson.getInt("city_prop_asset_rank"); + prop.city_prop_asset_funds = dataJson.getInt("city_prop_asset_funds"); + } + + return prop; + } + + private static CZoneMobile parseMobile(JSONObject mobileJson, int templateId) { + + CZoneMobile mobile = new CZoneMobile(); + JSONObject dataJson; + JSONArray equipmentJson; + JSONArray equipJson; + JSONArray bootiesJson; + JSONObject bootyJson; + JSONArray itemsJson; + String rootFsm; + + mobile.mobile_type = CacheParserUtil.readEnum(mbEnums.ZoneMobileType.class, mobileJson.getString("mobile_type"), templateId, "mobile_type"); + + dataJson = mobileJson.getJSONObject("mobile_data"); + mobile.mobile_base_zone = parsePlacement(dataJson.getJSONObject("mobile_base_zone"), templateId); + + mobile.mobile_number_in_group = dataJson.getInt("mobile_number_in_group"); + mobile.mobile_level = dataJson.getInt("mobile_level"); + mobile.mobile_dialog_override = (int) dataJson.getLong("mobile_dialog_override"); + mobile.mobile_rune_stone_ids.addAll(CacheParserUtil.readIntList(dataJson.getJSONArray("mobile_rune_stone_ids"))); + mobile.mobile_spawn_hps.addAll(CacheParserUtil.readIntList(dataJson.getJSONArray("mobile_spawn_hps"))); + + // Equipment entries are [item id, chance] + + equipmentJson = dataJson.getJSONArray("mobile_equipment"); + + for (int i = 0; i < equipmentJson.length(); i++) { + equipJson = equipmentJson.getJSONArray(i); + mobile.mobile_equipment.add(new CZoneDrop((int) equipJson.getLong(0), equipJson.getFloat(1))); + } + + // Booty items are [chance, item id] pairs, the reverse of equipment + + bootiesJson = dataJson.getJSONArray("mobile_booties"); + + for (int i = 0; i < bootiesJson.length(); i++) { + bootyJson = bootiesJson.getJSONObject(i); + + if (!bootyJson.getString("type").equals("ITEM")) + throw new IllegalArgumentException(templateId + ": mobile_booties has unknown type '" + bootyJson.getString("type") + "'"); + + itemsJson = bootyJson.getJSONArray("items"); + + for (int j = 0; j + 1 < itemsJson.length(); j += 2) + mobile.mobile_booties.add(new CZoneDrop((int) itemsJson.getLong(j + 1), itemsJson.getFloat(j))); + } + + rootFsm = dataJson.getString("mobile_root_fs_mid"); + + if (!rootFsm.isEmpty()) + mobile.mobile_root_fs_mid = CacheParserUtil.readEnum(mbEnums.MobBehaviourType.class, rootFsm, templateId, "mobile_root_fs_mid"); + + mobile.mobile_has_home_goal = dataJson.getBoolean("mobile_has_home_goal"); + + if (mobile.mobile_has_home_goal) { + mobile.mobile_home_goal = (int) dataJson.getLong("mobile_home_goal"); + mobile.mobile_spawn_home_goal = dataJson.getBoolean("mobile_spawn_home_goal"); + } + + mobile.mobile_has_target_goal = dataJson.getBoolean("mobile_has_target_goal"); + + if (mobile.mobile_has_target_goal) { + mobile.mobile_target_goal = (int) dataJson.getLong("mobile_target_goal"); + mobile.mobile_spawn_target_goal = dataJson.getBoolean("mobile_spawn_target_goal"); + } + + mobile.mobile_has_group_tactics = dataJson.getBoolean("mobile_has_group_tactics"); + + if (mobile.mobile_has_group_tactics) + mobile.mobile_group_tactics = (int) dataJson.getLong("mobile_group_tactics"); + + mobile.mobile_enemy_monster_types.addAll(CacheParserUtil.readEnumSet(mbEnums.MonsterType.class, + dataJson.getJSONArray("mobile_enemy_monster_types"), templateId, "mobile_enemy_monster_types")); + mobile.mobile_not_enemy_monster_types.addAll(CacheParserUtil.readEnumSet(mbEnums.MonsterType.class, + dataJson.getJSONArray("mobile_not_enemy_monster_types"), templateId, "mobile_not_enemy_monster_types")); + mobile.mobile_friendly_charters.addAll(CacheParserUtil.readIntList(dataJson.getJSONArray("mobile_friendly_charters"))); + mobile.mobile_parley_name = dataJson.getString("mobile_parley_name"); + + if (mobile.mobile_type == mbEnums.ZoneMobileType.TRAINER) { + mobile.trainer_std_profit_margin = dataJson.getFloat("trainer_std_profit_margin"); + mobile.trainer_profit_margin = dataJson.getFloat("trainer_profit_margin"); + } + + return mobile; + } + + private static CZonePointSet parsePointSet(JSONObject pointSetJson, int templateId) { + + CZonePointSet pointSet = new CZonePointSet(); + JSONArray pointsJson; + JSONObject pointJson; + CZonePointSet.Point point; + + pointSet.zone_goal_type = CacheParserUtil.readEnum(mbEnums.PointSetType.class, pointSetJson.getString("zone_goal_type"), + templateId, "zone_goal_type"); + pointSet.zone_goal_value = (int) pointSetJson.getLong("zone_goal_value"); + pointSet.zone_goal_name = pointSetJson.getString("zone_goal_name"); + + pointsJson = pointSetJson.getJSONArray("zone_goal_points"); + + for (int i = 0; i < pointsJson.length(); i++) { + pointJson = pointsJson.getJSONObject(i); + point = new CZonePointSet.Point(); + point.zone_goal_point_location = CacheParserUtil.readVector3(pointJson.getJSONArray("zone_goal_point_location")); + point.zone_goal_point_vectors = CacheParserUtil.readIntArray(pointJson.getJSONArray("zone_goal_point_vectors")); + pointSet.zone_goal_points.add(point); + } + + return pointSet; + } + + // Counts to check against World-Boot-Plan findings §1 + + private static void logCounts(HashMap zoneTemplates) { + + int topProps = 0; + int childProps = 0; + int cityProps = 0; + int zoneMobiles = 0; + int zoneTrainers = 0; + int propMobiles = 0; + int propTrainers = 0; + int pointSets = 0; + + for (CZoneTemplate zone : zoneTemplates.values()) { + topProps += zone.zone_prop_info.size(); + pointSets += zone.zone_point_sets.size(); + + for (CZoneMobile mobile : zone.zone_mobile_info) { + if (mobile.mobile_type == mbEnums.ZoneMobileType.TRAINER) + zoneTrainers++; + else + zoneMobiles++; + } + + for (CZoneProp prop : zone.zone_prop_info) { + childProps += prop.prop_content_props.size(); + + if (prop.prop_type == mbEnums.ZonePropType.CITY) + cityProps++; + + for (CZoneMobile mobile : prop.prop_content_mobiles) { + if (mobile.mobile_type == mbEnums.ZoneMobileType.TRAINER) + propTrainers++; + else + propMobiles++; + } + } + } + + Logger.info("Zone props: " + topProps + " top level (" + cityProps + " CITY), " + childProps + " children"); + Logger.info("Zone mobiles: " + zoneMobiles + " MOBILE, " + zoneTrainers + " TRAINER; prop mobiles: " + + propMobiles + " MOBILE, " + propTrainers + " TRAINER; point sets: " + pointSets); + } +} diff --git a/src/engine/parsers/objects/CZoneDrop.java b/src/engine/parsers/objects/CZoneDrop.java new file mode 100644 index 00000000..269414da --- /dev/null +++ b/src/engine/parsers/objects/CZoneDrop.java @@ -0,0 +1,24 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.objects; + +// One item with a chance, from a CZONE mobile's mobile_equipment or mobile_booties. +// Equipment chances are 0–1 fractions; booty chances are percentages (100.0, 50.0). + +public class CZoneDrop { + + public final int item_id; + public final float chance; + + public CZoneDrop(int item_id, float chance) { + this.item_id = item_id; + this.chance = chance; + } + +} diff --git a/src/engine/parsers/objects/CZoneMobile.java b/src/engine/parsers/objects/CZoneMobile.java new file mode 100644 index 00000000..115ad894 --- /dev/null +++ b/src/engine/parsers/objects/CZoneMobile.java @@ -0,0 +1,59 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.objects; + +import engine.mbEnums; + +import java.util.ArrayList; +import java.util.EnumSet; + +// A MOBILE or TRAINER entry from a CZONE zone (zone_mobile_info) or from a +// top-level prop (prop_content_mobiles). Field names match the JSON keys. +// Fields that are default in every shipped file are not parsed (Task 1.1 list). + +public class CZoneMobile { + + public mbEnums.ZoneMobileType mobile_type; + public CZonePlacement mobile_base_zone; + + public int mobile_number_in_group; + public int mobile_level; + public int mobile_dialog_override; + public final ArrayList mobile_rune_stone_ids = new ArrayList<>(); + public final ArrayList mobile_equipment = new ArrayList<>(); + public final ArrayList mobile_booties = new ArrayList<>(); + public final ArrayList mobile_spawn_hps = new ArrayList<>(); + + // null when the entry has no root FSM + + public mbEnums.MobBehaviourType mobile_root_fs_mid; + + // Goal values are zone_point_sets zone_goal_value hashes + + public boolean mobile_has_home_goal; + public int mobile_home_goal; + public boolean mobile_spawn_home_goal; + public boolean mobile_has_target_goal; + public int mobile_target_goal; + public boolean mobile_spawn_target_goal; + + public boolean mobile_has_group_tactics; + public int mobile_group_tactics; + + public final EnumSet mobile_enemy_monster_types = EnumSet.noneOf(mbEnums.MonsterType.class); + public final EnumSet mobile_not_enemy_monster_types = EnumSet.noneOf(mbEnums.MonsterType.class); + public final ArrayList mobile_friendly_charters = new ArrayList<>(); + public String mobile_parley_name; + + // TRAINER only + + public float trainer_std_profit_margin; + public float trainer_profit_margin; + +} diff --git a/src/engine/parsers/objects/CZonePlacement.java b/src/engine/parsers/objects/CZonePlacement.java new file mode 100644 index 00000000..b634ff4c --- /dev/null +++ b/src/engine/parsers/objects/CZonePlacement.java @@ -0,0 +1,32 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.objects; + +import engine.math.Vector3fImmutable; + +// The basic_zone_* block shared by every CZONE prop and mobile +// (client BasicZoneTemplateData). Field names match the JSON keys. +// spawn_location is relative to the parent; y_rot is not the client yaw (W8: yaw = π − y_rot). + +public class CZonePlacement { + + public Vector3fImmutable basic_zone_spawn_location; + public String basic_zone_name_override; + public float basic_zone_time_to_respawn; + public float basic_zone_spawn_radius; + public float basic_zone_y_rot; + public int basic_zone_template_id; + public int basic_zone_level_number; + public int basic_zone_room_number; + + // IMMUNE_TO_STEAL is the only sparse key in the shipped cache + + public boolean basic_zone_immune_to_steal; + +} diff --git a/src/engine/parsers/objects/CZonePointSet.java b/src/engine/parsers/objects/CZonePointSet.java new file mode 100644 index 00000000..1f688066 --- /dev/null +++ b/src/engine/parsers/objects/CZonePointSet.java @@ -0,0 +1,36 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.objects; + +import engine.math.Vector3fImmutable; +import engine.mbEnums; + +import java.util.ArrayList; + +// A zone_point_sets entry: a named path that mobile home/target goals point at +// through zone_goal_value. Field names match the JSON keys. + +public class CZonePointSet { + + public mbEnums.PointSetType zone_goal_type; + public int zone_goal_value; + public String zone_goal_name; + public final ArrayList zone_goal_points = new ArrayList<>(); + + public static class Point { + + public Vector3fImmutable zone_goal_point_location; + + // Six ints; the first is the point's index, the rest are unknown + + public int[] zone_goal_point_vectors; + + } + +} diff --git a/src/engine/parsers/objects/CZoneProp.java b/src/engine/parsers/objects/CZoneProp.java new file mode 100644 index 00000000..1ff3cc54 --- /dev/null +++ b/src/engine/parsers/objects/CZoneProp.java @@ -0,0 +1,39 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.objects; + +import engine.mbEnums; + +import java.util.ArrayList; + +// A PROP or CITY entry from a CZONE zone (zone_prop_info) or a child prop +// (prop_content_props). Field names match the JSON keys. +// Children never have children or mobiles in the shipped cache. + +public class CZoneProp { + + public mbEnums.ZonePropType prop_type; + public CZonePlacement prop_basic_zone; + + public final ArrayList prop_content_props = new ArrayList<>(); + public final ArrayList prop_content_mobiles = new ArrayList<>(); + + public int prop_registry; + public String prop_registry_name = ""; + public boolean prop_has_teleporter; + public int prop_teleporter; + + // CITY only. Guild and nation are keys into the map's guild seed (W7). + + public int city_prop_gm_guild; + public int city_prop_gm_nation; + public int city_prop_asset_rank; + public int city_prop_asset_funds; + +} diff --git a/src/engine/parsers/objects/CZoneTemplate.java b/src/engine/parsers/objects/CZoneTemplate.java new file mode 100644 index 00000000..48f8c616 --- /dev/null +++ b/src/engine/parsers/objects/CZoneTemplate.java @@ -0,0 +1,51 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.objects; + +import engine.mbEnums; + +import java.util.ArrayList; +import java.util.HashMap; + +// Zone template as defined by the client's cache data (MBDATA/data/cache/CZONE). +// Field names match the JSON keys. Only server-needed fields are parsed (Task 1.1 list); +// textures, water, weather, sound and fields default in every file are skipped. +// A template is reused by many zones; placement lives in the zone tree. +// +// Read only once loaded. + +public class CZoneTemplate { + + public static HashMap _zones = new HashMap<>(); + + public int template_id; + + public mbEnums.ZoneShapeType zone_type; + public String zone_name; + public boolean zone_peace_zone; + public float zone_major_radius; + public float zone_minor_radius; + public float zone_min_blend; + public float zone_max_blend; + public float zone_sea_level; + public mbEnums.SeaLevelType zone_sea_level_type; + public boolean zone_has_water; + + // zone_terrain_gen; max_y and image exist only on TARGA terrain + + public boolean zone_has_terrain_gen; + public mbEnums.TerrainGenType terrain_type; + public float terrain_max_y; + public int terrain_image; + + public final ArrayList zone_prop_info = new ArrayList<>(); + public final ArrayList zone_mobile_info = new ArrayList<>(); + public final ArrayList zone_point_sets = new ArrayList<>(); + +} diff --git a/src/engine/server/world/WorldServer.java b/src/engine/server/world/WorldServer.java index 7cdb2c78..a2286729 100644 --- a/src/engine/server/world/WorldServer.java +++ b/src/engine/server/world/WorldServer.java @@ -36,6 +36,8 @@ import engine.parsers.cache.DeedParser; import engine.parsers.cache.ItemParser; import engine.parsers.cache.ParityCheck; import engine.parsers.cache.RuneParser; +import engine.parsers.cache.ZoneParser; +import engine.parsers.objects.CZoneTemplate; import engine.parsers.objects.DeedTemplate; import engine.parsers.objects.ItemTemplate; import engine.parsers.objects.RuneTemplate; @@ -430,6 +432,14 @@ public class WorldServer { Logger.info("Indexing Cache Templates"); CObjectIndex.build(); + Logger.info("Loading Cache Zone Templates"); + CZoneTemplate._zones = ZoneParser.parseZoneFolder(); + + if (CZoneTemplate._zones == null || CZoneTemplate._zones.isEmpty()) { + Logger.error("ABORT! Cache zone templates failed to load!"); + exit(1); + } + Logger.info("Loading Rune Power Requirements"); PowerReq.loadRunePowers();