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 <noreply@anthropic.com>
This commit is contained in:
MagicBotandClaude Opus 5.5 committed 2026-10-24 11:00:51 -04:00
1 parent 90656a4ab5
commit 48ce01f8d0
2 files changed
+424 -21

No files matched your search

@@ -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,
}
+348 -21
View File
@@ -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'])