From 48ce01f8d0a741a7b2bed1638b25bcabd3977592 Mon Sep 17 00:00:00 2001 From: MagicBot Date: Sat, 24 Oct 2026 11:00:51 -0400 Subject: [PATCH] Parse zone resource and state events, add event save and JSON ArcZoneEvent_BuildFrom (sb.exe 0xC8ED20) builds ArcZoneEvent, ArcZoneResourceEvent (0xC904F0) or ArcZoneStateEvent (0xC952E0); Python read the base fields only for all three and had no save or JSON code. Field names come from each class's ParseKeyword; trigger types and state actions use the client name tables (0x176AF20, 0x176A40C). Base event unknowns are the event-name hash, parent-name hash and has-parent flag; the trigger action's third bool is has_items. Co-Authored-By: Claude Opus 5.5 --- .../arcane/enums/zone/arc_zone_event.py | 76 ++++ mbEditorPro2.0/arcane/zones/ArcZoneEvent.py | 369 +++++++++++++++++- 2 files changed, 424 insertions(+), 21 deletions(-) create mode 100644 mbEditorPro2.0/arcane/enums/zone/arc_zone_event.py diff --git a/mbEditorPro2.0/arcane/enums/zone/arc_zone_event.py b/mbEditorPro2.0/arcane/enums/zone/arc_zone_event.py new file mode 100644 index 0000000..2668ab7 --- /dev/null +++ b/mbEditorPro2.0/arcane/enums/zone/arc_zone_event.py @@ -0,0 +1,76 @@ +# • ▌ ▄ ·. ▄▄▄▄· ▄▄▄ .·▄▄▄▄ ▪ ▄▄▄▄▄ ▄▄▄ ▄▄▄·▄▄▄ +# ·██ ▐███▪▐█ ▀█▪ ▀▄.▀·██▪ ██ ██ •██ ▪ ▀▄ █· ▐█ ▄█▀▄ █·▪ +# ▐█ ▌▐▌▐█·▐█▀▀█▄ ▐▀▀▪▄▐█· ▐█▌▐█· ▐█.▪ ▄█▀▄ ▐▀▀▄ ██▀·▐▀▀▄ ▄█▀▄ +# ██ ██▌▐█▌██▄▪▐█ ▐█▄▄▌██. ██ ▐█▌ ▐█▌·▐█▌.▐▌▐█•█▌ ▐█▪·•▐█•█▌▐█▌.▐▌ +# ▀▀ █▪▀▀▀·▀▀▀▀ ▀▀▀ ▀▀▀▀▀• ▀▀▀ ▀▀▀ ▀█▄▀▪.▀ ▀ .▀ .▀ ▀ ▀█▄▀▪ +# Magicbane Emulator Project © 2013 - 2022 +# www.magicbane.com + +EVENT = 1 +RESOURCE = 2 +STATE = 3 + +ZONE_EVENT_TO_STRING = { + EVENT: 'EVENT', + RESOURCE: 'RESOURCE', + STATE: 'STATE', +} + +STRING_TO_ZONE_EVENT = { + 'EVENT': EVENT, + 'RESOURCE': RESOURCE, + 'STATE': STATE, +} + +TRIGGER_NONE = 0 +TRIGGER_ITEM_ACTION = 1 +TRIGGER_POWER_ACTION = 2 +TRIGGER_STANDARD_TRIGGER = 3 +TRIGGER_TIME_CHANGE = 4 +TRIGGER_SEASON_CHANGE = 5 + +TRIGGER_TYPE_TO_STRING = { + TRIGGER_NONE: 'NONE', + TRIGGER_ITEM_ACTION: 'ITEM_ACTION', + TRIGGER_POWER_ACTION: 'POWER_ACTION', + TRIGGER_STANDARD_TRIGGER: 'STANDARD_TRIGGER', + TRIGGER_TIME_CHANGE: 'TIME_CHANGE', + TRIGGER_SEASON_CHANGE: 'SEASON_CHANGE', +} + +STRING_TO_TRIGGER_TYPE = { + 'NONE': TRIGGER_NONE, + 'ITEM_ACTION': TRIGGER_ITEM_ACTION, + 'POWER_ACTION': TRIGGER_POWER_ACTION, + 'STANDARD_TRIGGER': TRIGGER_STANDARD_TRIGGER, + 'TIME_CHANGE': TRIGGER_TIME_CHANGE, + 'SEASON_CHANGE': TRIGGER_SEASON_CHANGE, +} + +STATE_ACTION_NONE = 0 +STATE_ACTION_ASSET_RANK = 1 +STATE_ACTION_MONSTER_SPAWN = 2 +STATE_ACTION_MONSTER_DESPAWN = 3 +STATE_ACTION_MONSTER_NOSPAWN = 4 +STATE_ACTION_CHANGE_GOAL = 5 +STATE_ACTION_CHANGE_DEATH_TRIGGER = 6 + +STATE_ACTION_TO_STRING = { + STATE_ACTION_NONE: 'NONE', + STATE_ACTION_ASSET_RANK: 'ASSET_RANK', + STATE_ACTION_MONSTER_SPAWN: 'MONSTER_SPAWN', + STATE_ACTION_MONSTER_DESPAWN: 'MONSTER_DESPAWN', + STATE_ACTION_MONSTER_NOSPAWN: 'MONSTER_NOSPAWN', + STATE_ACTION_CHANGE_GOAL: 'CHANGE_GOAL', + STATE_ACTION_CHANGE_DEATH_TRIGGER: 'CHANGE_DEATH_TRIGGER', +} + +STRING_TO_STATE_ACTION = { + 'NONE': STATE_ACTION_NONE, + 'ASSET_RANK': STATE_ACTION_ASSET_RANK, + 'MONSTER_SPAWN': STATE_ACTION_MONSTER_SPAWN, + 'MONSTER_DESPAWN': STATE_ACTION_MONSTER_DESPAWN, + 'MONSTER_NOSPAWN': STATE_ACTION_MONSTER_NOSPAWN, + 'CHANGE_GOAL': STATE_ACTION_CHANGE_GOAL, + 'CHANGE_DEATH_TRIGGER': STATE_ACTION_CHANGE_DEATH_TRIGGER, +} diff --git a/mbEditorPro2.0/arcane/zones/ArcZoneEvent.py b/mbEditorPro2.0/arcane/zones/ArcZoneEvent.py index c5e0e17..57d465c 100644 --- a/mbEditorPro2.0/arcane/zones/ArcZoneEvent.py +++ b/mbEditorPro2.0/arcane/zones/ArcZoneEvent.py @@ -6,57 +6,384 @@ # Magicbane Emulator Project © 2013 - 2022 # www.magicbane.com +from collections import OrderedDict + +from arcane.enums.hashes import hash_to_string, string_to_hash +from arcane.enums.zone.arc_zone_event import * from arcane.util import ResStream +from .ArcMobile import ArcMobile +from .ArcProp import ArcProp + + +def read_hashes(stream: ResStream): + count = stream.read_dword() + return [stream.read_dword() for _ in range(count)] + + +def write_hashes(stream: ResStream, values): + stream.write_dword(len(values)) + for value in values: + stream.write_dword(value) class ArcZTriggerAction: def load_binary(self, stream: ResStream): - self.zone_trigger_action_type = stream.read_dword() + self.zone_trigger_action_type = stream.read_long() self.zone_trigger_action_unknown1 = stream.read_bool() self.zone_trigger_action_is_timed = stream.read_bool() - self.zone_trigger_action_unknown2 = stream.read_bool() - + self.zone_trigger_action_has_items = stream.read_bool() if self.zone_trigger_action_is_timed: self.zone_trigger_action_time_begin = stream.read_float() self.zone_trigger_action_time_end = stream.read_float() self.zone_trigger_action_has_state = stream.read_bool() if self.zone_trigger_action_has_state: - self.zone_trigger_action_new_state = stream.read_dword() - num_specifics = stream.read_dword() - self.zone_trigger_action_specifics = [stream.read_dword() for _ in range(num_specifics)] - num_items = stream.read_dword() - self.zone_trigger_action_items = [stream.read_dword() for _ in range(num_items)] - num_parent_states = stream.read_dword() - self.zone_trigger_action_parent_states = [stream.read_dword() for _ in range(num_parent_states)] + self.zone_trigger_action_new_state = stream.read_long() + self.zone_trigger_action_specifics = read_hashes(stream) + self.zone_trigger_action_items = read_hashes(stream) + self.zone_trigger_action_parent_states = read_hashes(stream) + + def save_binary(self, stream: ResStream): + stream.write_long(self.zone_trigger_action_type) + stream.write_bool(self.zone_trigger_action_unknown1) + stream.write_bool(self.zone_trigger_action_is_timed) + stream.write_bool(self.zone_trigger_action_has_items) + if self.zone_trigger_action_is_timed: + stream.write_float(self.zone_trigger_action_time_begin) + stream.write_float(self.zone_trigger_action_time_end) + stream.write_bool(self.zone_trigger_action_has_state) + if self.zone_trigger_action_has_state: + stream.write_long(self.zone_trigger_action_new_state) + write_hashes(stream, self.zone_trigger_action_specifics) + write_hashes(stream, self.zone_trigger_action_items) + write_hashes(stream, self.zone_trigger_action_parent_states) + + def save_json(self): + data = OrderedDict() + data['zone_trigger_action_type'] = TRIGGER_TYPE_TO_STRING[self.zone_trigger_action_type] + data['zone_trigger_action_unknown1'] = self.zone_trigger_action_unknown1 + data['zone_trigger_action_is_timed'] = self.zone_trigger_action_is_timed + data['zone_trigger_action_has_items'] = self.zone_trigger_action_has_items + if self.zone_trigger_action_is_timed: + data['zone_trigger_action_time_begin'] = self.zone_trigger_action_time_begin + data['zone_trigger_action_time_end'] = self.zone_trigger_action_time_end + data['zone_trigger_action_has_state'] = self.zone_trigger_action_has_state + if self.zone_trigger_action_has_state: + data['zone_trigger_action_new_state'] = self.zone_trigger_action_new_state + data['zone_trigger_action_specifics'] = [hash_to_string(h) for h in self.zone_trigger_action_specifics] + data['zone_trigger_action_items'] = [hash_to_string(h) for h in self.zone_trigger_action_items] + data['zone_trigger_action_parent_states'] = [hash_to_string(h) for h in self.zone_trigger_action_parent_states] + return data + + def load_json(self, data): + self.zone_trigger_action_type = STRING_TO_TRIGGER_TYPE[data['zone_trigger_action_type']] + self.zone_trigger_action_unknown1 = data['zone_trigger_action_unknown1'] + self.zone_trigger_action_is_timed = data['zone_trigger_action_is_timed'] + self.zone_trigger_action_has_items = data['zone_trigger_action_has_items'] + if self.zone_trigger_action_is_timed: + self.zone_trigger_action_time_begin = data['zone_trigger_action_time_begin'] + self.zone_trigger_action_time_end = data['zone_trigger_action_time_end'] + self.zone_trigger_action_has_state = data['zone_trigger_action_has_state'] + if self.zone_trigger_action_has_state: + self.zone_trigger_action_new_state = data['zone_trigger_action_new_state'] + self.zone_trigger_action_specifics = [string_to_hash(s) for s in data['zone_trigger_action_specifics']] + self.zone_trigger_action_items = [string_to_hash(s) for s in data['zone_trigger_action_items']] + self.zone_trigger_action_parent_states = [string_to_hash(s) for s in data['zone_trigger_action_parent_states']] class ArcZoneEventInfo: def load_binary(self, stream: ResStream): - self.zone_event_recycle = stream.read_dword() + self.zone_event_recycle = stream.read_long() self.zone_event_spawn_radius = stream.read_float() self.zone_event_fc_label = stream.read_string() self.zone_event_event_name = stream.read_string() - self.zone_event_unknown1 = stream.read_dword() + self.zone_event_id = stream.read_dword() self.zone_event_spawn_location = stream.read_tuple() self.zone_event_parent_name = stream.read_string() - self.zone_event_unknown2 = stream.read_dword() - self.zone_event_unknown3 = stream.read_bool() + self.zone_event_parent_id = stream.read_dword() + self.zone_event_has_parent = stream.read_bool() num_triggers = stream.read_dword() self.zone_event_triggers = [ArcZTriggerAction() for _ in range(num_triggers)] for action in self.zone_event_triggers: action.load_binary(stream) + def save_binary(self, stream: ResStream): + stream.write_long(self.zone_event_recycle) + stream.write_float(self.zone_event_spawn_radius) + stream.write_string(self.zone_event_fc_label) + stream.write_string(self.zone_event_event_name) + stream.write_dword(self.zone_event_id) + stream.write_tuple(self.zone_event_spawn_location) + stream.write_string(self.zone_event_parent_name) + stream.write_dword(self.zone_event_parent_id) + stream.write_bool(self.zone_event_has_parent) + stream.write_dword(len(self.zone_event_triggers)) + for action in self.zone_event_triggers: + action.save_binary(stream) + + def save_json(self): + data = OrderedDict() + data['zone_event_recycle'] = self.zone_event_recycle + data['zone_event_spawn_radius'] = self.zone_event_spawn_radius + data['zone_event_fc_label'] = self.zone_event_fc_label + data['zone_event_event_name'] = self.zone_event_event_name + data['zone_event_id'] = self.zone_event_id + data['zone_event_spawn_location'] = self.zone_event_spawn_location + data['zone_event_parent_name'] = self.zone_event_parent_name + data['zone_event_parent_id'] = self.zone_event_parent_id + data['zone_event_has_parent'] = self.zone_event_has_parent + data['zone_event_triggers'] = [action.save_json() for action in self.zone_event_triggers] + return data + + def load_json(self, data): + self.zone_event_recycle = data['zone_event_recycle'] + self.zone_event_spawn_radius = data['zone_event_spawn_radius'] + self.zone_event_fc_label = data['zone_event_fc_label'] + self.zone_event_event_name = data['zone_event_event_name'] + self.zone_event_id = data['zone_event_id'] + self.zone_event_spawn_location = data['zone_event_spawn_location'] + self.zone_event_parent_name = data['zone_event_parent_name'] + self.zone_event_parent_id = data['zone_event_parent_id'] + self.zone_event_has_parent = data['zone_event_has_parent'] + self.zone_event_triggers = [] + for action_data in data['zone_event_triggers']: + action = ArcZTriggerAction() + action.load_json(action_data) + self.zone_event_triggers.append(action) + + +class ArcZoneResourceEventInfo(ArcZoneEventInfo): + def load_binary(self, stream: ResStream): + super().load_binary(stream) + self.resource_event_max_spawns = stream.read_long() + self.resource_event_ultimate_limit = stream.read_long() + self.resource_event_respawn_delay = stream.read_long() + self.resource_event_items_per_spawn = stream.read_long() + self.resource_event_random_time = stream.read_long() + self.resource_event_auto = stream.read_bool() + self.resource_event_timed = stream.read_bool() + self.resource_event_possible = stream.read_bool() + num_item_spawns = stream.read_dword() + self.resource_event_item_spawns = [[ + stream.read_qword(), + stream.read_float(), + ] for _ in range(num_item_spawns)] + + def save_binary(self, stream: ResStream): + super().save_binary(stream) + stream.write_long(self.resource_event_max_spawns) + stream.write_long(self.resource_event_ultimate_limit) + stream.write_long(self.resource_event_respawn_delay) + stream.write_long(self.resource_event_items_per_spawn) + stream.write_long(self.resource_event_random_time) + stream.write_bool(self.resource_event_auto) + stream.write_bool(self.resource_event_timed) + stream.write_bool(self.resource_event_possible) + stream.write_dword(len(self.resource_event_item_spawns)) + for item_id, chance in self.resource_event_item_spawns: + stream.write_qword(item_id) + stream.write_float(chance) + + def save_json(self): + data = super().save_json() + data['resource_event_max_spawns'] = self.resource_event_max_spawns + data['resource_event_ultimate_limit'] = self.resource_event_ultimate_limit + data['resource_event_respawn_delay'] = self.resource_event_respawn_delay + data['resource_event_items_per_spawn'] = self.resource_event_items_per_spawn + data['resource_event_random_time'] = self.resource_event_random_time + data['resource_event_auto'] = self.resource_event_auto + data['resource_event_timed'] = self.resource_event_timed + data['resource_event_possible'] = self.resource_event_possible + data['resource_event_item_spawns'] = self.resource_event_item_spawns + return data + + def load_json(self, data): + super().load_json(data) + self.resource_event_max_spawns = data['resource_event_max_spawns'] + self.resource_event_ultimate_limit = data['resource_event_ultimate_limit'] + self.resource_event_respawn_delay = data['resource_event_respawn_delay'] + self.resource_event_items_per_spawn = data['resource_event_items_per_spawn'] + self.resource_event_random_time = data['resource_event_random_time'] + self.resource_event_auto = data['resource_event_auto'] + self.resource_event_timed = data['resource_event_timed'] + self.resource_event_possible = data['resource_event_possible'] + self.resource_event_item_spawns = data['resource_event_item_spawns'] + + +class EventStateInfo: + def load_binary(self, stream: ResStream): + self.state_action = stream.read_long() + self.state_index = stream.read_long() + self.state_new_faction = stream.read_bool() + self.state_new_goal = stream.read_bool() + self.state_use_goal = stream.read_bool() + self.state_name = stream.read_dword() + self.state_goal = stream.read_dword() + self.state_faction = stream.read_dword() + self.state_has_auto_state = stream.read_bool() + if self.state_has_auto_state: + self.state_auto_state = [stream.read_long() for _ in range(3)] + self.state_has_death_trigger = stream.read_bool() + if self.state_has_death_trigger: + self.state_death_trigger = stream.read_dword() + self.state_trigger_only = stream.read_bool() + self.state_max_mob_spawns = stream.read_long() + + def save_binary(self, stream: ResStream): + stream.write_long(self.state_action) + stream.write_long(self.state_index) + stream.write_bool(self.state_new_faction) + stream.write_bool(self.state_new_goal) + stream.write_bool(self.state_use_goal) + stream.write_dword(self.state_name) + stream.write_dword(self.state_goal) + stream.write_dword(self.state_faction) + stream.write_bool(self.state_has_auto_state) + if self.state_has_auto_state: + for value in self.state_auto_state: + stream.write_long(value) + stream.write_bool(self.state_has_death_trigger) + if self.state_has_death_trigger: + stream.write_dword(self.state_death_trigger) + stream.write_bool(self.state_trigger_only) + stream.write_long(self.state_max_mob_spawns) + + def save_json(self): + data = OrderedDict() + data['state_action'] = STATE_ACTION_TO_STRING[self.state_action] + data['state_index'] = self.state_index + data['state_new_faction'] = self.state_new_faction + data['state_new_goal'] = self.state_new_goal + data['state_use_goal'] = self.state_use_goal + data['state_name'] = hash_to_string(self.state_name) + data['state_goal'] = hash_to_string(self.state_goal) + data['state_faction'] = hash_to_string(self.state_faction) + data['state_has_auto_state'] = self.state_has_auto_state + if self.state_has_auto_state: + data['state_auto_state'] = self.state_auto_state + data['state_has_death_trigger'] = self.state_has_death_trigger + if self.state_has_death_trigger: + data['state_death_trigger'] = hash_to_string(self.state_death_trigger) + data['state_trigger_only'] = self.state_trigger_only + data['state_max_mob_spawns'] = self.state_max_mob_spawns + return data + + def load_json(self, data): + self.state_action = STRING_TO_STATE_ACTION[data['state_action']] + self.state_index = data['state_index'] + self.state_new_faction = data['state_new_faction'] + self.state_new_goal = data['state_new_goal'] + self.state_use_goal = data['state_use_goal'] + self.state_name = string_to_hash(data['state_name']) + self.state_goal = string_to_hash(data['state_goal']) + self.state_faction = string_to_hash(data['state_faction']) + self.state_has_auto_state = data['state_has_auto_state'] + if self.state_has_auto_state: + self.state_auto_state = data['state_auto_state'] + self.state_has_death_trigger = data['state_has_death_trigger'] + if self.state_has_death_trigger: + self.state_death_trigger = string_to_hash(data['state_death_trigger']) + self.state_trigger_only = data['state_trigger_only'] + self.state_max_mob_spawns = data['state_max_mob_spawns'] + + +class ArcZoneStateEventInfo(ArcZoneEventInfo): + def load_binary(self, stream: ResStream): + super().load_binary(stream) + self.state_event_default_state = stream.read_long() + self.state_event_require_recycle = stream.read_bool() + num_states = stream.read_dword() + self.state_event_states = [EventStateInfo() for _ in range(num_states)] + for state in self.state_event_states: + state.load_binary(stream) + num_props = stream.read_dword() + self.state_event_props = [] + for _ in range(num_props): + state_num = stream.read_long() + prop = ArcProp() + prop.load_binary(stream) + self.state_event_props.append([state_num, prop]) + num_mobiles = stream.read_dword() + self.state_event_mobiles = [] + for _ in range(num_mobiles): + state_num = stream.read_long() + mobile = ArcMobile() + mobile.load_binary(stream) + self.state_event_mobiles.append([state_num, mobile]) + + def save_binary(self, stream: ResStream): + super().save_binary(stream) + stream.write_long(self.state_event_default_state) + stream.write_bool(self.state_event_require_recycle) + stream.write_dword(len(self.state_event_states)) + for state in self.state_event_states: + state.save_binary(stream) + stream.write_dword(len(self.state_event_props)) + for state_num, prop in self.state_event_props: + stream.write_long(state_num) + prop.save_binary(stream) + stream.write_dword(len(self.state_event_mobiles)) + for state_num, mobile in self.state_event_mobiles: + stream.write_long(state_num) + mobile.save_binary(stream) + + def save_json(self): + data = super().save_json() + data['state_event_default_state'] = self.state_event_default_state + data['state_event_require_recycle'] = self.state_event_require_recycle + data['state_event_states'] = [state.save_json() for state in self.state_event_states] + data['state_event_props'] = [[state_num, prop.save_json()] for state_num, prop in self.state_event_props] + data['state_event_mobiles'] = [[state_num, mobile.save_json()] for state_num, mobile in self.state_event_mobiles] + return data + + def load_json(self, data): + super().load_json(data) + self.state_event_default_state = data['state_event_default_state'] + self.state_event_require_recycle = data['state_event_require_recycle'] + self.state_event_states = [] + for state_data in data['state_event_states']: + state = EventStateInfo() + state.load_json(state_data) + self.state_event_states.append(state) + self.state_event_props = [] + for state_num, prop_data in data['state_event_props']: + prop = ArcProp() + prop.load_json(prop_data) + self.state_event_props.append([state_num, prop]) + self.state_event_mobiles = [] + for state_num, mobile_data in data['state_event_mobiles']: + mobile = ArcMobile() + mobile.load_json(mobile_data) + self.state_event_mobiles.append([state_num, mobile]) + class ArcZoneEvent: def load_binary(self, stream: ResStream): self.zone_event_type = stream.read_dword() - if self.zone_event_type == 1: + if self.zone_event_type == EVENT: self.zone_event_data = ArcZoneEventInfo() - self.zone_event_data.load_binary(stream) - elif self.zone_event_type == 2: + elif self.zone_event_type == RESOURCE: + self.zone_event_data = ArcZoneResourceEventInfo() + elif self.zone_event_type == STATE: + self.zone_event_data = ArcZoneStateEventInfo() + self.zone_event_data.load_binary(stream) + + def save_binary(self, stream: ResStream): + stream.write_dword(self.zone_event_type) + self.zone_event_data.save_binary(stream) + + def save_json(self): + data = OrderedDict() + data['zone_event_type'] = ZONE_EVENT_TO_STRING[self.zone_event_type] + data['zone_event_data'] = self.zone_event_data.save_json() + return data + + def load_json(self, data): + self.zone_event_type = STRING_TO_ZONE_EVENT[data['zone_event_type']] + if self.zone_event_type == EVENT: self.zone_event_data = ArcZoneEventInfo() - self.zone_event_data.load_binary(stream) - elif self.zone_event_type == 3: - self.zone_event_data = ArcZoneEventInfo() - self.zone_event_data.load_binary(stream) + elif self.zone_event_type == RESOURCE: + self.zone_event_data = ArcZoneResourceEventInfo() + elif self.zone_event_type == STATE: + self.zone_event_data = ArcZoneStateEventInfo() + self.zone_event_data.load_json(data['zone_event_data'])