def init_free_airship(rom: Rom) -> Rom: # Move the airship's start location to right outside of Coneria Castle. airship_start = OutputStream() airship_start.put_u32(0x918) airship_start.put_u32(0x998) return rom.apply_patch(0x65280, airship_start.get_buffer())
def write(self, rom: Rom) -> Rom: permissions_stream = OutputStream() for permission in self._permissions: permissions_stream.put_u16(permission) rom = rom.apply_patch(0x1A20C0, permissions_stream.get_buffer()) rom = self._spells.write(rom) return self._shops.write(rom)
def update_xp_requirements(rom: Rom, value) -> Rom: level_data = rom.open_bytestream(0x1BE3B4, 396) new_table = OutputStream() next_value = level_data.get_u32() while next_value is not None: new_table.put_u32(int(next_value * value)) next_value = level_data.get_u32() rom = rom.apply_patch(0x1BE3B4, new_table.get_buffer()) return rom
def test_overlap_patch(self): patch = OutputStream() patch.put_u32(0x12345678) patch.put_u32(0x12345678) with self.assertRaises(RuntimeError): patched = self.rom.apply_patches({ 0x0: patch.get_buffer(), 0x4: patch.get_buffer() }) self.assertNotEqual(patched, patched)
def write(self, rom: Rom) -> Rom: patches = {} for index, map in enumerate(self._maps): # TODO: Figure out what breaks the Caravan. if index == 0x73: continue data = OutputStream() map.write(data) patches[Rom.pointer_to_offset( self._map_lut[index])] = data.get_buffer() return rom.apply_patches(patches)
def write(self, rom: Rom) -> Rom: # Since there's a LUT and the data that it points to, create two output streams. # This should work because both are continuous. data_lut_stream = OutputStream() shop_inventory = OutputStream() next_shop_addr = self.shop_data_pointers[0].pointer start_size = 0 for index in range(51): new_shop_length = self.shop_inventories[index].write( shop_inventory) sdp = self.shop_data_pointers[index] sdp.contents = ( (sdp.shop_graphic << 4) & 0xf0) | (new_shop_length & 0x0f) sdp.pointer = next_shop_addr sdp.write(data_lut_stream) # Because the size of the data varies by shop, we have to keep track of how # large the output buffer was and move the pointer up by the difference. next_shop_addr += (shop_inventory.size() - start_size) start_size = shop_inventory.size() # Make a dictionary for the two parts so we only have to write the new Rom once. patches = { 0x1E070C: data_lut_stream.get_buffer(), Rom.pointer_to_offset(self.shop_data_pointers[0].pointer): shop_inventory.get_buffer() } return rom.apply_patches(patches)
def write(self, stream: OutputStream): self.header.write(stream) for tile in self.tiles: tile.write(stream) for npc in self.npcs: npc.write(stream) for chest in self.chests: chest.write(stream) for sprite in self.sprites: sprite.write(stream) for shop in self.shops: shop.write(stream) # Close the map at the end. :) stream.put_u16(0xffff)
def random_bucketed_treasures(rom: Rom, rng: Random, wealth_level: int = 0) -> Rom: """Randomly generates and shuffles treasured based on wealth_level""" bucket_data = TreasureBuckets() chest_stream = rom.open_bytestream(0x217FB4, 0x400) items = [(0, x + 1) for x in list(range(0x45))] items = items + [(1, x + 1) for x in list(range(0x3F))] items = items + [(2, x + 1) for x in list(range(0x2A))] itemCount = len(items) chests_to_shuffle = [] original_list = [] moneyCount = 0 itemTotal = 0 for index in range(256): chest = TreasureChest.read(chest_stream) original_list.append(chest) if isinstance(chest, ItemChest): if chest.item_type != 0: item_bucket = bucket_data.getBucket(chest.item_type, chest.item_id) if wealth_level == 1: item_bucket = bucket_data.up_one(item_bucket) if wealth_level == -1: item_bucket = bucket_data.down_one(item_bucket) new_item = bucket_data.pullFromBucket(item_bucket, rng, 1) chest.item_id = new_item[0] itemTotal += 1 chests_to_shuffle.append(chest) elif isinstance(chest, MoneyChest): chest.qty = rng.randint(1, 0xfff) * rng.randint(1, 6) chests_to_shuffle.append(chest) moneyCount += 1 else: print("BAD CHEST") rng.shuffle(chests_to_shuffle) chest_data = OutputStream() for chest in original_list: if isinstance(chest, MoneyChest) or chest.item_type != 0: new_chest = chests_to_shuffle.pop() new_chest.write(chest_data) else: chest.write(chest_data) return rom.apply_patch(0x217FB4, chest_data.get_buffer())
def random_treasures(rom: Rom, rng: Random) -> Rom: chest_stream = rom.open_bytestream(0x217FB4, 0x400) items = [(0, x + 1) for x in list(range(0x45))] items = items + [(1, x + 1) for x in list(range(0x3F))] items = items + [(2, x + 1) for x in list(range(0x2A))] itemCount = len(items) print(itemCount) chests_to_shuffle = [] original_list = [] moneyCount = 0 itemTotal = 0 for index in range(256): chest = TreasureChest.read(chest_stream) original_list.append(chest) if isinstance(chest, ItemChest): if chest.item_type != 0: new_item = items[rng.randint(0, itemCount - 1)] chest.item_type = new_item[0] chest.item_id = new_item[1] itemTotal += 1 chests_to_shuffle.append(chest) elif isinstance(chest, MoneyChest): chest.qty = rng.randint(1, 0xfff) * rng.randint(1, 6) chests_to_shuffle.append(chest) print(chest.qty) moneyCount += 1 else: print("BAD CHEST") return rng.shuffle(chests_to_shuffle) chest_data = OutputStream() for chest in original_list: if isinstance(chest, MoneyChest) or chest.item_type != 0: new_chest = chests_to_shuffle.pop() new_chest.write(chest_data) else: chest.write(chest_data) return rom.apply_patch(0x217FB4, chest_data.get_buffer())
def treasure_shuffle(rom: Rom, rng: Random) -> Rom: chest_stream = rom.open_bytestream(0x217FB4, 0x400) chests_to_shuffle = [] original_list = [] for index in range(256): chest = TreasureChest.read(chest_stream) original_list.append(chest) if isinstance(chest, MoneyChest) or chest.item_type != 0: chests_to_shuffle.append(chest) rng.shuffle(chests_to_shuffle) chest_data = OutputStream() for chest in original_list: if isinstance(chest, MoneyChest) or chest.item_type != 0: new_chest = chests_to_shuffle.pop() new_chest.write(chest_data) else: chest.write(chest_data) return rom.apply_patch(0x217FB4, chest_data.get_buffer())
def test_patches(self): patch = OutputStream() patch.put_u32(0x12345678) patched = self.rom.apply_patches({ 0x0: patch.get_buffer(), 0x10: patch.get_buffer() }) confirm = patched.open_bytestream(0x0, 0x4) self.assertEqual(confirm.get_u32(), 0x12345678) confirm = patched.open_bytestream(0x10, 0x4) self.assertEqual(confirm.get_u32(), 0x12345678)
def build(self): stream = OutputStream() written_length = 0 if len(self._magic) > 0: for magic_id in self._magic: stream.put_u8(magic_id) written_length += 1 if len(self._armor) > 0: stream.put_u8(0xfc) for item_id in self._armor: stream.put_u8(item_id) written_length += 1 if len(self._weapons) > 0: stream.put_u8(0xfd) for item_id in self._weapons: stream.put_u8(item_id) written_length += 1 if len(self._items) > 0: stream.put_u8(0xfe) for item_id in self._items: stream.put_u8(item_id) written_length += 1 if written_length > 0xf: raise RuntimeError( f"Error: Too many items in shop: {written_length}") return ShopInventory(InputStream(stream.get_buffer()), written_length)
def pack(self, rom: Rom) -> Rom: text_block = OutputStream() text_lut = OutputStream() next_addr = self.lut[0] text_block_offset = Rom.pointer_to_offset(next_addr) for index, data in enumerate(self.strings): if data is not None: text_lut.put_u32(next_addr) text_block.put_bytes(data) next_addr += len(data) else: text_lut.put_u32(self.lut[0]) patches = { self.lut_offset: text_lut.get_buffer(), text_block_offset: text_block.get_buffer() } return rom.apply_patches(patches)
def write(self, stream: OutputStream): stream.put_u16(self.base_hp) stream.put_u16(self.base_mp) stream.put_u8(self.starting_spell_level) stream.put_u8(self.base_strength) stream.put_u8(self.base_agility) stream.put_u8(self.base_intellect) stream.put_u8(self.base_stamina) stream.put_u8(self.base_luck) stream.put_u8(self.base_accuracy) stream.put_u8(self.base_evade) stream.put_u8(self.base_mdef) stream.put_u8(self.weapon_id) stream.put_u8(self.armor_id) stream.put_u8(self.unused)
def write(self, stream: OutputStream) -> int: written_length = 0 if len(self.magic) > 0: for magic_id in self.magic: stream.put_u8(magic_id) written_length += 1 if len(self.armor) > 0: stream.put_u8(0xfc) for item_id in self.armor: stream.put_u8(item_id) written_length += 1 if len(self.weapons) > 0: stream.put_u8(0xfd) for item_id in self.weapons: stream.put_u8(item_id) written_length += 1 if len(self.items) > 0: stream.put_u8(0xfe) for item_id in self.items: stream.put_u8(item_id) written_length += 1 if written_length > 0xf: raise RuntimeError( f"Error: Too many items in shop: {written_length}") return written_length
def _save_chests(self): # Save the chests (without key items in them). chest_data = OutputStream() for chest in self.chests: chest.write(chest_data) self.rom = self.rom.apply_patch(0x217FB4, chest_data.get_buffer())
def parse(source: str, base_addr: int, debug=False) -> bytearray: symbol_table = {} icode = [] current_addr = base_addr for line_number, line in enumerate(source.splitlines()): tokens = TokenStream(line_number, line) token = tokens.next() if token is None: # Empty or comment only line continue if isinstance(token, RawCommandToken): parameters = [] token = tokens.expect(GRAMMAR["$$value$$"]) while token is not None: if isinstance(token, SymbolToken): if token in symbol_table: parameters.append(symbol_table[token]) else: raise SymbolNotDefinedError(token, line, line_number) else: parameters.append(token) token = tokens.expect(GRAMMAR["$$value$$"]) icode.append(parameters) current_addr += parameters[1] else: # Save the op name op_name = token if type(token) not in GRAMMAR: raise ParserSyntaxError(token, line, line_number) match = GRAMMAR[type(token)] if isinstance(match, dict): rule_set = match["rule"] else: rule_set = match parameters = [] if rule_set is not None: for rule in rule_set: if isinstance(rule, str) and rule.startswith("$$"): rule = GRAMMAR[rule] token = tokens.expect(rule) if token is None: raise ParserSyntaxError(token, line, line_number) if isinstance(op_name, SymbolToken): parameters.append(token) else: if isinstance(token, SymbolToken): if token in symbol_table: parameters.append(symbol_table[token]) else: raise SymbolNotDefinedError( token, line, line_number) else: parameters.append(token) verify_end = tokens.expect(CommentToken()) else: verify_end = tokens.expect(CommentToken()) if op_name == "def_symbol" or op_name == "def_label": name = parameters[0] value = parameters[1] if name in symbol_table: raise DuplicateSymbolError(name, line, line_number) if isinstance(value, ColonToken): value = current_addr symbol_table[name] = value else: if isinstance(op_name, list): output = simple_gen(op_name, parameters) else: method = getattr(codegen, op_name) output = method(parameters) if output is not None: icode.append(output) current_addr += output[1] # At this point, all of the intermediate code is built and the only thing left is to resolve # the left over symbols, which will all bel labels. bytecode = OutputStream() for code in icode: txt = "" for bd in code: if isinstance(bd, LabelToken): label = bd if label not in symbol_table: raise UndefinedLabel(label) bytecode.put_u32(symbol_table[label]) txt += f"{label} ({hex(symbol_table[label])}) " else: txt += f"{hex(bd)} " bytecode.put_u8(bd) if debug: print(txt) # Done! return bytecode.get_buffer()
class FormationRandomization(object): FLYING_MONSTER_IDS = [0x51, 0x52, 0x52, 0xA2, 0x3E, 0x3F, 0xBB] MINI_BOSS_IDS = [ 0xf, # Pirate(s) 0x69, # Garland 0x71, # Astos 0x76 # Death Machine ] def __init__(self, rom: Rom, rng: Random): size_stream = rom.open_bytestream(0x223644, 0xc3) sizes = [] while not size_stream.is_eos(): sizes.append(size_stream.get_u8()) small_enemies = [] large_enemies = [] enemy_id_map = [] for index, size in enumerate(sizes): enemy_id_map.append(index) if index >= 0x80: break if index not in FormationRandomization.MINI_BOSS_IDS: if size == 0: small_enemies.append(index) elif size == 1: large_enemies.append(index) shuffled_small = ShuffledList(small_enemies, rng) shuffled_large = ShuffledList(large_enemies, rng) enemy_data_stream = rom.open_bytestream(0x1DE044, 0x1860) enemies = [] scaled_enemies = [] while not enemy_data_stream.is_eos(): enemy = EnemyStats(enemy_data_stream) enemies.append(enemy) scaled_enemies.append(enemy) for index in range(len(small_enemies)): original = shuffled_small.original(index) replacement = shuffled_small[index] scaled_enemy = FormationRandomization.scale_enemy( enemies[original], enemies[replacement]) scaled_enemies[replacement] = scaled_enemy enemy_id_map[original] = replacement for index in range(len(large_enemies)): original = shuffled_large.original(index) replacement = shuffled_large[index] scaled_enemy = FormationRandomization.scale_enemy( enemies[original], enemies[replacement]) scaled_enemies[replacement] = scaled_enemy enemy_id_map[original] = replacement self._scaled_enemies_out = OutputStream() for enemy in scaled_enemies: enemy.write(self._scaled_enemies_out) formations_stream = rom.open_bytestream(0x2288B4, 0x14 * 0x171) self._formations_out = OutputStream() while not formations_stream.is_eos(): formation = Encounter(formations_stream) has_land = False has_flying = False for index, group in enumerate(formation.groups): if group.enemy_id < len(enemy_id_map): group.enemy_id = enemy_id_map[group.enemy_id] if group.max_count == 0: group.enemy_id = 0 if group.enemy_id in FormationRandomization.FLYING_MONSTER_IDS: has_flying = True else: has_land = True if formation.config in [0x0, 0x05]: if has_flying and has_land: # Both land and flying -> Mixed formation.config = 0x5 else: # Even if they're all flying, they still get mapped to "1-9 small" formation.config = 0x0 # Save the new formations formation.write(self._formations_out) def patches(self) -> dict: return { 0x1DE044: self._scaled_enemies_out.get_buffer(), 0x2288B4: self._formations_out.get_buffer() } @staticmethod def scale_enemy(from_enemy: EnemyStats, to_enemy: EnemyStats) -> EnemyStats: self_exp = from_enemy.exp_reward if from_enemy.exp_reward > 1 else 1620 other_exp = to_enemy.exp_reward if to_enemy.exp_reward > 1 else 1620 ratio = other_exp / self_exp estimate_damage = ( (to_enemy.acc / 200) * to_enemy.atk) * to_enemy.hit_count target_attack = (estimate_damage / (from_enemy.acc / 200)) / from_enemy.hit_count new_hit_count = from_enemy.hit_count if target_attack > 220: if new_hit_count == 1: new_hit_count = 2 target_attack = min(target_attack / 2, 220) scaled_enemy = copy.deepcopy(from_enemy) scaled_enemy.exp_reward = to_enemy.exp_reward if from_enemy.exp_reward > 1 else from_enemy.exp_reward scaled_enemy.gil_reward = int( from_enemy.gil_reward * ratio) if from_enemy.gil_reward > 1 else 1 scaled_enemy.hp = int(from_enemy.max_hp * ratio) scaled_enemy.intel = min(int(from_enemy.intel * ratio), 160) scaled_enemy.attack = min(int(target_attack), 220) scaled_enemy.hit_count = int(new_hit_count) return scaled_enemy
def write(self, stream: OutputStream): stream.put_u16(self.identifier) stream.put_u16(self.event) stream.put_u16(self.x_pos) stream.put_u16(self.y_pos) stream.put_u16(self.sprite_id) stream.put_u16(self.move_speed) stream.put_u16(self.facing) stream.put_u16(self.in_room)
def write(self, stream: OutputStream): stream.put_u16(self.exp_reward) stream.put_u16(self.gil_reward) stream.put_u16(self.max_hp) stream.put_u8(self.morale) stream.put_u8(self.unused_ai) stream.put_u8(self.evasion) stream.put_u8(self.pdef) stream.put_u8(self.hit_count) stream.put_u8(self.acc) stream.put_u8(self.atk) stream.put_u8(self.agi) stream.put_u8(self.intel) stream.put_u8(self.crit_rate) stream.put_u16(self.status_atk_elem) stream.put_u8(self.status_atk_ailment) stream.put_u8(self.family) stream.put_u8(self.mdef) stream.put_u8(self.unused) stream.put_u16(self.elem_weakness) stream.put_u16(self.elem_resists) stream.put_u8(self.drop_type) stream.put_u8(self.drop_id) stream.put_u8(self.drop_chance) for data in self.padding: stream.put_u8(data)
def __init__(self, rom: Rom, rng: Random): size_stream = rom.open_bytestream(0x223644, 0xc3) sizes = [] while not size_stream.is_eos(): sizes.append(size_stream.get_u8()) small_enemies = [] large_enemies = [] enemy_id_map = [] for index, size in enumerate(sizes): enemy_id_map.append(index) if index >= 0x80: break if index not in FormationRandomization.MINI_BOSS_IDS: if size == 0: small_enemies.append(index) elif size == 1: large_enemies.append(index) shuffled_small = ShuffledList(small_enemies, rng) shuffled_large = ShuffledList(large_enemies, rng) enemy_data_stream = rom.open_bytestream(0x1DE044, 0x1860) enemies = [] scaled_enemies = [] while not enemy_data_stream.is_eos(): enemy = EnemyStats(enemy_data_stream) enemies.append(enemy) scaled_enemies.append(enemy) for index in range(len(small_enemies)): original = shuffled_small.original(index) replacement = shuffled_small[index] scaled_enemy = FormationRandomization.scale_enemy( enemies[original], enemies[replacement]) scaled_enemies[replacement] = scaled_enemy enemy_id_map[original] = replacement for index in range(len(large_enemies)): original = shuffled_large.original(index) replacement = shuffled_large[index] scaled_enemy = FormationRandomization.scale_enemy( enemies[original], enemies[replacement]) scaled_enemies[replacement] = scaled_enemy enemy_id_map[original] = replacement self._scaled_enemies_out = OutputStream() for enemy in scaled_enemies: enemy.write(self._scaled_enemies_out) formations_stream = rom.open_bytestream(0x2288B4, 0x14 * 0x171) self._formations_out = OutputStream() while not formations_stream.is_eos(): formation = Encounter(formations_stream) has_land = False has_flying = False for index, group in enumerate(formation.groups): if group.enemy_id < len(enemy_id_map): group.enemy_id = enemy_id_map[group.enemy_id] if group.max_count == 0: group.enemy_id = 0 if group.enemy_id in FormationRandomization.FLYING_MONSTER_IDS: has_flying = True else: has_land = True if formation.config in [0x0, 0x05]: if has_flying and has_land: # Both land and flying -> Mixed formation.config = 0x5 else: # Even if they're all flying, they still get mapped to "1-9 small" formation.config = 0x0 # Save the new formations formation.write(self._formations_out)
def write(self, rom: Rom) -> Rom: spell_stream = OutputStream() for spell in self._spell_data: spell.write(spell_stream) return rom.apply_patch(0x1A1980, spell_stream.get_buffer())
def write(self, stream: OutputStream): stream.put_u16(self.identifier) stream.put_u16(self.low_x) stream.put_u16(self.low_y) stream.put_u16(self.high_x) stream.put_u16(self.high_y)
def get_lut(self) -> bytearray: stream = OutputStream() for addr in self._lut: stream.put_u32(addr) return stream.get_buffer()
def write(self, stream: OutputStream): stream.put_u8(self.contents) for data in self.unused: stream.put_u8(data) stream.put_u32(self.pointer)
def randomize_rom(rom: Rom, flags: Flags, rom_seed: str) -> Rom: rng = random.Random() rng.seed(rom_seed) print(f"Randomize ROM: {flags.text()}, seed='{rom_seed}'") patches_to_load = BASE_PATCHES if flags.encounters is not None: patches_to_load.append("data/FF1EncounterToggle.ips") if flags.default_party is not None: patches_to_load.append("data/RandomDefault.ips") patched_rom_data = rom.rom_data for patch_path in patches_to_load: patch = Patch.load(patch_path) patched_rom_data = patch.apply(patched_rom_data) rom = Rom(data=bytearray(patched_rom_data)) rom = init_free_airship(rom) rom = add_credits(rom) event_text_block = EventTextBlock(rom) event_text_block.shrink() rom = event_text_block.pack(rom) rom = update_xp_requirements(rom, flags.exp_mult) if flags.key_item_shuffle is not None: placement = KeyItemPlacement(rom, rng.randint(0, 0xffffffff)) else: placement = KeyItemPlacement(rom) rom = placement.rom if flags.magic is not None: shuffle_magic = SpellShuffle(rom, rng) rom = shuffle_magic.write(rom) if flags.treasures is not None: if flags.treasures == "shuffle": rom = treasure_shuffle(rom, rng) else: rom = random_bucketed_treasures(rom, rng, flags.wealth) if flags.debug is not None: class_stats_stream = rom.open_bytestream(0x1E1354, 96) class_stats = [] while not class_stats_stream.is_eos(): class_stats.append(JobClass(class_stats_stream)) class_out_stream = OutputStream() for job_class in class_stats: # Set the starting weapon and armor for all classes to something # very fair and balanced: Masamune + Diamond Armlet. :) job_class.weapon_id = 0x28 job_class.armor_id = 0x0e # Write the (very balanced) new data out job_class.write(class_out_stream) rom = rom.apply_patch(0x1E1354, class_out_stream.get_buffer()) if flags.shuffle_formations: formation = FormationRandomization(rom, rng) rom = rom.apply_patches(formation.patches()) if True: enemy_data_stream = rom.open_bytestream(0x1DE044, 0x1860) enemies = [] while not enemy_data_stream.is_eos(): enemies.append(EnemyStats(enemy_data_stream)) # Rebalance (Revisited) Fiend HP enemies[0x78].max_hp = enemies[0x77].max_hp * 2 enemies[0x7a].max_hp = enemies[0x79].max_hp * 2 enemies[0x7c].max_hp = enemies[0x7b].max_hp * 2 enemies[0x7e].max_hp = enemies[0x7d].max_hp * 2 # And Chaos enemies[0x7f].max_hp = enemies[0x7e].max_hp * 2 # Finally, Piscodemons can suck it enemies[0x67].atk = int(enemies[0x67].atk / 2) # We'll also lower everyone's INT just to see how that works for index in range(0x80): enemies[index].intel = int(.666 * enemies[index].intel) # print(f"{hex(index)} HP: {enemies[index].max_hp}, INT: {enemies[index].intel}") out = OutputStream() for enemy in enemies: enemy.write(out) rom = rom.apply_patch(0x1DE044, out.get_buffer()) # Add the seed + flags to the party creation screen. seed_str = TextBlock.encode_text(f"Seed:\n{rom_seed}\nFlags:\n{flags}\x00") pointer = OutputStream() pointer.put_u32(0x8227054) rom = rom.apply_patches({ 0x227054: seed_str, 0x4d8d4: pointer.get_buffer() }) return rom
def write(self, stream: OutputStream): stream.put_u8(self.config) stream.put_u8(self.unrunnable) stream.put_u16(self.surprise_chance) for data in self.groups: data.write(stream)
def write(self, stream: OutputStream): stream.put_u16(self.identifier) stream.put_u16(self.event) stream.put_u16(self.x_pos) stream.put_u16(self.y_pos)
def add_credits(rom: Rom) -> Rom: credits_lut = rom.get_lut(0x1D871C, 128) base_addr = credits_lut[0] new_lut = OutputStream() data_stream = OutputStream() for index, line in enumerate(CREDITS_TEXT.splitlines()[1:]): line = line.strip() if len(line) > 0: encoded = TextBlock.encode_text(line) new_lut.put_u32(base_addr + data_stream.size()) data_stream.put_bytes(encoded) else: new_lut.put_u32(0x0) # And EOF marker new_lut.put_u32(0xffffffff) # Change the duration so it doesn't take so long to scroll duration = OutputStream() duration.put_u16(60 * 60) return rom.apply_patches({ 0x016848: duration.get_buffer(), 0x1D871C: new_lut.get_buffer(), Rom.pointer_to_offset(base_addr): data_stream.get_buffer() })
def write(self, stream: OutputStream): stream.put_u8(self.enemy_id) stream.put_u8(self.min_count) stream.put_u8(self.max_count) stream.put_u8(self.unused)