Skip to content

Commit

Permalink
KDL3: Version 2.0.0 (ArchipelagoMW#3323)
Browse files Browse the repository at this point in the history
* initial work on procedure patch

* more flexibility

load default procedure for version 5 patches
add args for procedure
add default extension for tokens and bsdiff
allow specifying additional required extensions for generation

* pushing current changes to go fix tloz bug

* move tokens into a separate inheritable class

* forgot the commit to remove token from ProcedurePatch

* further cleaning from bad commit

* start on docstrings

* further work on docstrings and typing

* improve docstrings

* fix incorrect docstring

* cleanup

* clean defaults and docstring

* define interface that has only the bare minimum required
for `Patch.create_rom_file`

* change to dictionary.get

* remove unnecessary if statement

* update to explicitly check for procedure, restore compatible version and manual override

* Update Files.py

* remove struct uses

* Update Rom.py

* convert KDL3 to APPP

* change class variables to instance variables

* Update worlds/Files.py

Co-authored-by: black-sliver <[email protected]>

* Update worlds/Files.py

Co-authored-by: black-sliver <[email protected]>

* move required_extensions to tuple

* fix missing tuple ellipsis

* fix classvar mixup

* rename tokens to _tokens. use hasattr

* type hint cleanup

* Update Files.py

* initial base for local items, need to finish

* coo not clean

* handle local items for real, appp cleanup

* actually make bosses send their locations

* fix cloudy park 4 rule, zero deathlink message

* remove redundant door_shuffle bool

when generic ER gets in, this whole function gets rewritten. So just clean it a little now.

* properly fix deathlink messages, fix fill error

* update docs

* add prefill items

* fix kine fill error

* Update Rom.py

* Update Files.py

* mypy and softlock fix

* Update Gifting.py

* mypy phase 1

* fix rare async client bug

* Update __init__.py

* typing cleanup

* fix stone softlock

because of the way Kine's Stone works, you can't clear the stone blocks before clearing the burning blocks, so we have to bring Burning from outside

* Update Rom.py

* Add option groups

* Rename to lowercase

* finish rename

* whoops broke the world

* fix animal duplication bug

* overhaul filler generation

* add Miku flavor

* Update gifting.py

* fix issues related to max_hs increase

* Update test_locations.py

* fix boss shuffle not working if level shuffle is disabled

* fix bleeding default levels

* Update options.py

* thought this would print seed

* yay bad merges

* forgot options too

* yeah lets just break generation while at it

* this is probably a problem

* cap required heart stars

* Revert "cap required heart stars"

This reverts commit 759efd3.

* fix duplication removal placement, deprecated test option

* forgot that we need to account for what we place

* move location ids

* rewrite trap handling

* further stage renumber fixes

* forgot one more

* basic UT support

* fix local heart star checks

* fix pattern

---------

Co-authored-by: beauxq <[email protected]>
Co-authored-by: black-sliver <[email protected]>
Co-authored-by: NewSoupVi <[email protected]>
  • Loading branch information
4 people authored Aug 31, 2024
1 parent b1be597 commit 920cffd
Show file tree
Hide file tree
Showing 28 changed files with 2,436 additions and 2,072 deletions.
940 changes: 0 additions & 940 deletions worlds/kdl3/Locations.py

This file was deleted.

577 changes: 0 additions & 577 deletions worlds/kdl3/Rom.py

This file was deleted.

95 changes: 0 additions & 95 deletions worlds/kdl3/Room.py

This file was deleted.

187 changes: 109 additions & 78 deletions worlds/kdl3/__init__.py

Large diffs are not rendered by default.

35 changes: 28 additions & 7 deletions worlds/kdl3/Aesthetics.py → worlds/kdl3/aesthetics.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import struct
from .Options import KirbyFlavorPreset, GooeyFlavorPreset
from .options import KirbyFlavorPreset, GooeyFlavorPreset
from typing import TYPE_CHECKING, Optional, Dict, List, Tuple

if TYPE_CHECKING:
from . import KDL3World

kirby_flavor_presets = {
1: {
Expand Down Expand Up @@ -223,6 +227,23 @@
"14": "E6E6FA",
"15": "976FBD",
},
14: {
"1": "373B3E",
"2": "98d5d3",
"3": "1aa5ab",
"4": "168f95",
"5": "4f5559",
"6": "1dbac2",
"7": "137a7f",
"8": "093a3c",
"9": "86cecb",
"10": "a0afbc",
"11": "62bfbb",
"12": "50b8b4",
"13": "bec8d1",
"14": "bce4e2",
"15": "91a2b1",
}
}

gooey_flavor_presets = {
Expand Down Expand Up @@ -398,37 +419,37 @@
}


def get_kirby_palette(world):
def get_kirby_palette(world: "KDL3World") -> Optional[Dict[str, str]]:
palette = world.options.kirby_flavor_preset.value
if palette == KirbyFlavorPreset.option_custom:
return world.options.kirby_flavor.value
return kirby_flavor_presets.get(palette, None)


def get_gooey_palette(world):
def get_gooey_palette(world: "KDL3World") -> Optional[Dict[str, str]]:
palette = world.options.gooey_flavor_preset.value
if palette == GooeyFlavorPreset.option_custom:
return world.options.gooey_flavor.value
return gooey_flavor_presets.get(palette, None)


def rgb888_to_bgr555(red, green, blue) -> bytes:
def rgb888_to_bgr555(red: int, green: int, blue: int) -> bytes:
red = red >> 3
green = green >> 3
blue = blue >> 3
outcol = (blue << 10) + (green << 5) + red
return struct.pack("H", outcol)


def get_palette_bytes(palette, target, offset, factor):
def get_palette_bytes(palette: Dict[str, str], target: List[str], offset: int, factor: float) -> bytes:
output_data = bytearray()
for color in target:
hexcol = palette[color]
if hexcol.startswith("#"):
hexcol = hexcol.replace("#", "")
colint = int(hexcol, 16)
col = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF)
col: Tuple[int, ...] = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF)
col = tuple(int(int(factor*x) + offset) for x in col)
byte_data = rgb888_to_bgr555(col[0], col[1], col[2])
output_data.extend(bytearray(byte_data))
return output_data
return bytes(output_data)
70 changes: 38 additions & 32 deletions worlds/kdl3/Client.py → worlds/kdl3/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
from NetUtils import ClientStatus, color
from Utils import async_start
from worlds.AutoSNIClient import SNIClient
from .Locations import boss_locations
from .Gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes
from .ClientAddrs import consumable_addrs, star_addrs
from .locations import boss_locations
from .gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes
from .client_addrs import consumable_addrs, star_addrs
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from SNIClient import SNIClientCommandProcessor
from SNIClient import SNIClientCommandProcessor, SNIContext

snes_logger = logging.getLogger("SNES")

Expand Down Expand Up @@ -81,17 +81,16 @@


@mark_raw
def cmd_gift(self: "SNIClientCommandProcessor"):
def cmd_gift(self: "SNIClientCommandProcessor") -> None:
"""Toggles gifting for the current game."""
if not getattr(self.ctx, "gifting", None):
self.ctx.gifting = True
else:
self.ctx.gifting = not self.ctx.gifting
self.output(f"Gifting set to {self.ctx.gifting}")
handler = self.ctx.client_handler
assert isinstance(handler, KDL3SNIClient)
handler.gifting = not handler.gifting
self.output(f"Gifting set to {handler.gifting}")
async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", {
f"{self.ctx.slot}":
{
"IsOpen": self.ctx.gifting,
"IsOpen": handler.gifting,
**kdl3_gifting_options
}
}))
Expand All @@ -100,16 +99,17 @@ def cmd_gift(self: "SNIClientCommandProcessor"):
class KDL3SNIClient(SNIClient):
game = "Kirby's Dream Land 3"
patch_suffix = ".apkdl3"
levels = None
consumables = None
stars = None
item_queue: typing.List = []
initialize_gifting = False
levels: typing.Dict[int, typing.List[int]] = {}
consumables: typing.Optional[bool] = None
stars: typing.Optional[bool] = None
item_queue: typing.List[int] = []
initialize_gifting: bool = False
gifting: bool = False
giftbox_key: str = ""
motherbox_key: str = ""
client_random: random.Random = random.Random()

async def deathlink_kill_player(self, ctx) -> None:
async def deathlink_kill_player(self, ctx: "SNIContext") -> None:
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
game_state = await snes_read(ctx, KDL3_GAME_STATE, 1)
if game_state[0] == 0xFF:
Expand All @@ -131,7 +131,7 @@ async def deathlink_kill_player(self, ctx) -> None:
ctx.death_state = DeathState.dead
ctx.last_death_link = time.time()

async def validate_rom(self, ctx) -> bool:
async def validate_rom(self, ctx: "SNIContext") -> bool:
from SNIClient import snes_read
rom_name = await snes_read(ctx, KDL3_ROMNAME, 0x15)
if rom_name is None or rom_name == bytes([0] * 0x15) or rom_name[:4] != b"KDL3":
Expand All @@ -141,17 +141,18 @@ async def validate_rom(self, ctx) -> bool:

ctx.game = self.game
ctx.rom = rom_name
ctx.items_handling = 0b111 # always remote items
ctx.items_handling = 0b101 # default local items with remote start inventory
ctx.allow_collect = True
if "gift" not in ctx.command_processor.commands:
ctx.command_processor.commands["gift"] = cmd_gift

death_link = await snes_read(ctx, KDL3_DEATH_LINK_ADDR, 1)
if death_link:
await ctx.update_death_link(bool(death_link[0] & 0b1))
ctx.items_handling |= (death_link[0] & 0b10) # set local items if enabled
return True

async def pop_item(self, ctx, in_stage):
async def pop_item(self, ctx: "SNIContext", in_stage: bool) -> None:
from SNIClient import snes_buffered_write, snes_read
if len(self.item_queue) > 0:
item = self.item_queue.pop()
Expand All @@ -168,8 +169,8 @@ async def pop_item(self, ctx, in_stage):
else:
self.item_queue.append(item) # no more slots, get it next go around

async def pop_gift(self, ctx):
if ctx.stored_data[self.giftbox_key]:
async def pop_gift(self, ctx: "SNIContext") -> None:
if self.giftbox_key in ctx.stored_data and ctx.stored_data[self.giftbox_key]:
from SNIClient import snes_read, snes_buffered_write
key, gift = ctx.stored_data[self.giftbox_key].popitem()
await pop_object(ctx, self.giftbox_key, key)
Expand Down Expand Up @@ -214,7 +215,7 @@ async def pop_gift(self, ctx):
quality = min(10, quality * 2)
else:
# it's not really edible, but he'll eat it anyway
quality = self.client_random.choices(range(0, 2), {0: 75, 1: 25})[0]
quality = self.client_random.choices(range(0, 2), [75, 25])[0]
kirby_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1)
gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1)
snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26]))
Expand All @@ -224,7 +225,8 @@ async def pop_gift(self, ctx):
else:
snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality, 10)))

async def pick_gift_recipient(self, ctx, gift):
async def pick_gift_recipient(self, ctx: "SNIContext", gift: int) -> None:
assert ctx.slot
if gift != 4:
gift_base = kdl3_gifts[gift]
else:
Expand All @@ -238,7 +240,7 @@ async def pick_gift_recipient(self, ctx, gift):
if desire > most_applicable:
most_applicable = desire
most_applicable_slot = int(slot)
elif most_applicable_slot == ctx.slot and info["AcceptsAnyGift"]:
elif most_applicable_slot != ctx.slot and most_applicable == -1 and info["AcceptsAnyGift"]:
# only send to ourselves if no one else will take it
most_applicable_slot = int(slot)
# print(most_applicable, most_applicable_slot)
Expand All @@ -257,7 +259,7 @@ async def pick_gift_recipient(self, ctx, gift):
item_uuid: item,
})

async def game_watcher(self, ctx) -> None:
async def game_watcher(self, ctx: "SNIContext") -> None:
try:
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
rom = await snes_read(ctx, KDL3_ROMNAME, 0x15)
Expand All @@ -278,11 +280,12 @@ async def game_watcher(self, ctx) -> None:
await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0]))
self.initialize_gifting = True
# can't check debug anymore, without going and copying the value. might be important later.
if self.levels is None:
if not self.levels:
self.levels = dict()
for i in range(5):
level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14)
self.levels[i] = unpack("HHHHHHH", level_data)
self.levels[i] = [int.from_bytes(level_data[idx:idx+1], "little")
for idx in range(0, len(level_data), 2)]
self.levels[5] = [0x0205, # Hyper Zone
0, # MG-5, can't send from here
0x0300, # Boss Butch
Expand Down Expand Up @@ -371,7 +374,7 @@ async def game_watcher(self, ctx) -> None:
stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60)
stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw)
for i in range(30):
loc_id = 0x770000 + i + 1
loc_id = 0x770000 + i
if stages[i] == 1 and loc_id not in ctx.checked_locations:
new_checks.append(loc_id)
elif loc_id in ctx.checked_locations:
Expand All @@ -381,8 +384,8 @@ async def game_watcher(self, ctx) -> None:
heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35)
for i in range(5):
start_ind = i * 7
for j in range(1, 7):
level_ind = start_ind + j - 1
for j in range(6):
level_ind = start_ind + j
loc_id = 0x770100 + (6 * i) + j
if heart_stars[level_ind] and loc_id not in ctx.checked_locations:
new_checks.append(loc_id)
Expand All @@ -401,14 +404,17 @@ async def game_watcher(self, ctx) -> None:
if star not in ctx.checked_locations and stars[star_addrs[star]] == 0x01:
new_checks.append(star)

if not game_state:
return

if game_state[0] != 0xFF:
await self.pop_gift(ctx)
await self.pop_item(ctx, game_state[0] != 0xFF)
await snes_flush_writes(ctx)

# boss status
boss_flag_bytes = await snes_read(ctx, KDL3_BOSS_STATUS, 2)
boss_flag = unpack("H", boss_flag_bytes)[0]
boss_flag = int.from_bytes(boss_flag_bytes, "little")
for bitmask, boss in zip(range(1, 11, 2), boss_locations.keys()):
if boss_flag & (1 << bitmask) > 0 and boss not in ctx.checked_locations:
new_checks.append(boss)
Expand Down
File renamed without changes.
File renamed without changes.
Binary file modified worlds/kdl3/data/kdl3_basepatch.bsdiff4
Binary file not shown.
Loading

0 comments on commit 920cffd

Please sign in to comment.