Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,7 @@ def fill_slot_data(self) -> dict:
"ring_link": self.options.ring_link.value,
"tag_link": self.options.tag_link.value,
"trap_link": self.options.trap_link.value,
"damage_link": self.options.damage_link.value,
"receive_notifications": self.options.receive_notifications.value,
"LevelOrder": ", ".join([level.name for order, level in self.spoiler.settings.level_order.items()]),
"StartingKongs": ", ".join([kong.name for kong in self.spoiler.settings.starting_kong_list]),
Expand Down
106 changes: 104 additions & 2 deletions archipelago/DK64Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
MAX_DELIVER_COUNT = 10000
MAX_STRING_LENGTH = 0x20
FAST_TEXT_SPEED = 50
DAMAGE_POINTS_PER_UNIT = 20 # DamageLink: 80 points = 1 full melon (20 per quarter-melon)
INCOMING_PACKET_CAP = 160 # DamageLink: cap a single inbound bounce at 2 melon
NORMAL_TEXT_SPEED = 130
MIN_ITEMS_FOR_SPEED_SCALING = 5
KONG_COUNT = 5
Expand Down Expand Up @@ -166,6 +168,7 @@ class DK64Client:
ENABLE_RINGLINK = False
ENABLE_TAGLINK = False
ENABLE_TRAPLINK = False
ENABLE_DAMAGELINK = False
deathlink_debounce = True
pending_deathlink = False

Expand Down Expand Up @@ -933,7 +936,7 @@ def get_current_deliver_count(self):

return data

async def main_tick(self, item_get_cb, deathlink_cb, map_change_cb, ring_link, tag_link, trap_link, hint_cb=None):
async def main_tick(self, item_get_cb, deathlink_cb, map_change_cb, ring_link, tag_link, trap_link, damage_link, hint_cb=None):
"""Game loop tick."""
await self.readChecks(item_get_cb)
# await self.item_tracker.readItems()
Expand Down Expand Up @@ -970,6 +973,8 @@ def check_safe_death():
await tag_link()
if self.ENABLE_TRAPLINK:
await trap_link()
if self.ENABLE_DAMAGELINK:
await damage_link()

# Check for hint access
if hint_cb:
Expand Down Expand Up @@ -1127,6 +1132,21 @@ def _cmd_traplink(self):
self.ctx.tags.add("TrapLink")
create_task_log_exception(self.ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": self.ctx.tags}]))

def _cmd_damagelink(self):
"""Toggle damagelink from client. Overrides default setting."""
if isinstance(self.ctx, DK64Context):
if self.ctx.ENABLE_DAMAGELINK:
self.ctx.ENABLE_DAMAGELINK = False
self.ctx.client.ENABLE_DAMAGELINK = False
self.ctx.tags.discard("SharedDamage")
logger.info("Damagelink disabled")
else:
self.ctx.ENABLE_DAMAGELINK = True
self.ctx.client.ENABLE_DAMAGELINK = True
logger.info("Damagelink enabled")
self.ctx.tags.add("SharedDamage")
create_task_log_exception(self.ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": self.ctx.tags}]))


class DK64Context(CommonContext):
"""Context for Donkey Kong 64."""
Expand All @@ -1140,6 +1160,11 @@ class DK64Context(CommonContext):
ENABLE_RINGLINK = False
ENABLE_TAGLINK = False
ENABLE_TRAPLINK = False
ENABLE_DAMAGELINK = False
pending_damage = 0
self_inflicted = 0
prev_health = None
damage_label = None
command_processor = DK64CommandProcessor
won = False
hint_locations = {}
Expand Down Expand Up @@ -1269,6 +1294,8 @@ def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.slot_info[self.slot].game
self.slot_data = args.get("slot_data", {})
if not hasattr(self, "instance_id"):
self.instance_id = time.time()
self.setup_hint_locations()
if self.slot_data.get("Version"):
ap_version = get_ap_version()
Expand Down Expand Up @@ -1320,6 +1347,12 @@ def on_package(self, cmd: str, args: dict):
self.ENABLE_TRAPLINK = True
self.client.ENABLE_TRAPLINK = True
asyncio.create_task(self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]))
if self.slot_data.get("damage_link"):
if "SharedDamage" not in self.tags:
self.tags.add("SharedDamage")
self.ENABLE_DAMAGELINK = True
self.client.ENABLE_DAMAGELINK = True
asyncio.create_task(self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]))
if self.slot_data.get("receive_notifications"):
self.client.send_mode = self.slot_data.get("receive_notifications")
# Set Helm Hurry flag in client
Expand Down Expand Up @@ -1370,6 +1403,11 @@ def on_package(self, cmd: str, args: dict):
if self.pending_trap_link != 0:
self.pending_trap_original = args["data"]["trap_name"]
self.pending_trap_source = source_name
if "SharedDamage" in self.tags and "SharedDamage" in args.get("tags", []):
if args["data"].get("uuid") != self.instance_id: # ignore our own echo
points = int(args["data"].get("damage_points", 0))
if points > 0:
self.pending_damage += min(points, INCOMING_PACKET_CAP)

async def send_ring_link(self, amount: int):
"""Send a ring link message."""
Expand Down Expand Up @@ -1530,6 +1568,66 @@ async def handle_ring_link(self):

self.pending_ring_link = 0

async def send_damage_link(self, points: int):
"""Send a SharedDamage (DamageLink) bounce."""
if "SharedDamage" not in self.tags or self.slot is None:
return
if not hasattr(self, "instance_id"):
self.instance_id = time.time()
await self.send_msgs([{"cmd": "Bounce", "tags": ["SharedDamage"], "data": {
"time": time.time(),
"uuid": self.instance_id,
"source": self.player_names.get(self.slot),
"damage_points": int(points),
}}])

async def handle_damage_link(self):
"""Apply received DamageLink damage."""
if not self.client.ENABLE_DAMAGELINK:
return
n64 = self.client.n64_client
base = self.client.memory_pointer

# Show total amount of damagelink points in client (taken from mycena waffle's client).
try:
if self.damage_label is None and getattr(self, "ui", None) is not None:
try:
from kvui import MDLabel as Label
except ImportError:
from kvui import Label
self.damage_label = Label(text="", size_hint_x=None, width=120, halign="center")
self.ui.connect_layout.add_widget(self.damage_label)
if self.damage_label is not None:
self.damage_label.text = f"DMG: {self.pending_damage}"
except Exception:
pass

health = n64.read_u8(DK64MemoryMap.health)
if health > 127:
health -= 256
if self.prev_health is None:
self.prev_health = health

# Hand any received damage to the game, remembering the health it will cost us.
units = self.pending_damage // DAMAGE_POINTS_PER_UNIT
if units > 0:
self.pending_damage -= units * DAMAGE_POINTS_PER_UNIT
queued = n64.read_u8(base + DK64MemoryMap.receive_damage)
n64.write_u8(base + DK64MemoryMap.receive_damage, min(queued + units, 255))
self.self_inflicted += units

# Broadcast damag
lost = self.prev_health - health
if lost > 0:
mine = min(lost, self.self_inflicted)
self.self_inflicted -= mine
genuine = lost - mine
if genuine > 0:
await self.send_damage_link(genuine * DAMAGE_POINTS_PER_UNIT)
elif health <= 0:
self.self_inflicted = 0 # once you die, reset points
self.prev_health = health

async def send_tag_link(self, kong: int):
"""Send a tag link message."""
if "TagLink" not in self.tags or self.slot is None:
Expand Down Expand Up @@ -1742,6 +1840,10 @@ async def trap_link():
"""Handle a trap link."""
await self.handle_trap_link()

async def damage_link():
"""Handle a damage link."""
await self.handle_damage_link()

async def deathlink():
"""Handle a deathlink."""
await self.send_deathlink()
Expand Down Expand Up @@ -1814,7 +1916,7 @@ async def disconnect_check():
if status is False:
await asyncio.sleep(0.033)
continue
await self.client.main_tick(on_item_get, deathlink, map_change, ring_link, tag_link, trap_link, hint_accessed)
await self.client.main_tick(on_item_get, deathlink, map_change, ring_link, tag_link, trap_link, damage_link, hint_accessed)
await asyncio.sleep(0.033)
now = time.time()
if self.last_resend + 0.5 < now:
Expand Down
123 changes: 15 additions & 108 deletions archipelago/FillSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,25 +485,17 @@ def fillsettings(options: DK64Options, multiworld: MultiWorld, random_obj: Rando
slam_name = options.alter_switch_allocation.value[level_key]
settings_dict[f"prog_slam_level_{i + 1}"] = slam_map.get(slam_name, SlamRequirement.green)


def generate_blocker(option_value: str, blocker_max: int, random: Random):
"""Randomize a B. Locker value, either within a range or up to the maximum."""
upper_bound = blocker_max if option_value == "random" else int(option_value.split("-")[1]) + 1
lower_bound = 0 if option_value == "random" else int(option_value.split("-")[0])
return random.randrange(lower_bound, upper_bound)


def apply_blocker_settings(settings_dict: dict, options, random_obj) -> None:
"""Apply level blocker settings."""
blocker_options = [0, 0, 0, 0, 0, 0, 0, 0]
for blocker, amount in options.level_blockers.value.items():
blocker_number = int(blocker.removeprefix("level_")) - 1
try:
blocker_options[blocker_number] = int(amount)
except (TypeError, ValueError):
blocker_options[blocker_number] = generate_blocker(amount, options.blocker_max.value, random_obj)

# Blocker settings - prioritize chaos blockers, then randomization setting
# Apply blocker settings
blocker_options: list[int] = [
options.level_blockers.value.get("level_1", 0),
options.level_blockers.value.get("level_2", 0),
options.level_blockers.value.get("level_3", 0),
options.level_blockers.value.get("level_4", 0),
options.level_blockers.value.get("level_5", 0),
options.level_blockers.value.get("level_6", 0),
options.level_blockers.value.get("level_7", 0),
options.level_blockers.value.get("level_8", 64),
]
settings_dict["maximize_helm_blocker"] = options.maximize_level8_blocker.value
if options.enable_chaos_blockers.value:
settings_dict["blocker_text"] = options.chaos_ratio.value
Expand Down Expand Up @@ -965,95 +957,10 @@ def apply_blocker_settings(settings_dict: dict, options, random_obj) -> None:
settings_dict["coin_door_item"] = HelmDoorItem(options.coin_door_item.value)
coin_item_key = door_item_to_key.get(settings_dict["coin_door_item"])
settings_dict["coin_door_item_count"] = options.helm_door_item_count.value.get(coin_item_key, 1) if coin_item_key else 1

if hasattr(multiworld, "generation_is_fake"):
if hasattr(multiworld, "re_gen_passthrough"):
if "Donkey Kong 64" in multiworld.re_gen_passthrough:
passthrough = multiworld.re_gen_passthrough["Donkey Kong 64"]
settings_dict["bonus_barrel_auto_complete"] = passthrough["Autocomplete"]
settings_dict["helm_room_bonus_count"] = HelmBonuses(passthrough["HelmBarrelCount"])


def handle_fake_generation_settings(settings: Settings, multiworld) -> None:
"""Handle settings for fake generation (UT mode)."""
if hasattr(multiworld, "generation_is_fake"):
settings.is_ut_generation = True
if hasattr(multiworld, "re_gen_passthrough"):
if "Donkey Kong 64" in multiworld.re_gen_passthrough:
passthrough = multiworld.re_gen_passthrough["Donkey Kong 64"]
settings.level_order = passthrough["LevelOrder"]

# Switch logic lifted out of level shuffle due to static levels for UT
if settings.alter_switch_allocation:
for x in range(8):
settings.switch_allocation[x] = passthrough["SlamLevels"][x]

settings.starting_kong_list = passthrough["StartingKongs"]
settings.starting_kong = settings.starting_kong_list[0] # fake a starting kong so that we don't force a different kong
settings.medal_requirement = passthrough["JetpacReq"]
settings.rareware_gb_fairies = passthrough["FairyRequirement"]
settings.BLockerEntryItems = passthrough["BLockerEntryItems"]
settings.BLockerEntryCount = passthrough["BLockerEntryCount"]
settings.medal_cb_req = passthrough["MedalCBRequirement"]
settings.medal_cb_req_level = [settings.medal_cb_req] * 8

for level, value in enumerate(passthrough["MedalCBRequirementLevel"]):
settings.medal_cb_req_level[Levels(level)] = int(value)

settings.mermaid_gb_pearls = passthrough["MermaidPearls"]
settings.BossBananas = passthrough["BossBananas"]
settings.boss_maps = passthrough["BossMaps"]
settings.boss_kongs = passthrough["BossKongs"]
settings.lanky_freeing_kong = passthrough["LankyFreeingKong"]
settings.helm_order = passthrough["HelmOrder"]
settings.logic_type = LogicType[passthrough["LogicType"]]
settings.tricks_selected = passthrough["TricksSelected"]
settings.glitches_selected = passthrough["GlitchesSelected"]
settings.open_lobbies = passthrough["OpenLobbies"]
settings.starting_key_list = passthrough["StartingKeyList"]
settings.galleon_water = GalleonWaterSetting[passthrough["GalleonWater"]]
settings.galleon_water_internal = GalleonWaterSetting[passthrough["GalleonWater"]]

# There's multiple sources of truth for helm order.
settings.helm_donkey = 0 in settings.helm_order
settings.helm_diddy = 4 in settings.helm_order
settings.helm_lanky = 3 in settings.helm_order
settings.helm_tiny = 2 in settings.helm_order
settings.helm_chunky = 1 in settings.helm_order

# Switchsanity
for switch, data in passthrough["SwitchSanity"].items():
needed_kong = Kongs[data["kong"]]
switch_type = SwitchType[data["type"]]
settings.switchsanity_data[Switches[switch]] = SwitchInfo(switch, needed_kong, switch_type, 0, 0, [])

if passthrough["Shopkeepers"]:
settings.shuffled_location_types.append(Types.Cranky)
settings.shuffled_location_types.append(Types.Funky)
settings.shuffled_location_types.append(Types.Candy)
settings.shuffled_location_types.append(Types.Snide)


def fillsettings(options, multiworld, random_obj):
"""Fill and configure all DK64 settings."""
# Start with default settings
settings_dict = get_default_settings()

# Apply all setting categories
apply_archipelago_settings(settings_dict, options, multiworld)
apply_blocker_settings(settings_dict, options, random_obj)
apply_item_randomization_settings(settings_dict, options)
apply_hard_mode_settings(settings_dict, options)
apply_kong_settings(settings_dict, options)
apply_switchsanity_settings(settings_dict, options)
apply_logic_and_barriers_settings(settings_dict, options)
apply_glitches_and_tricks_settings(settings_dict, options)
apply_boss_and_key_settings(settings_dict, options)
apply_goal_settings(settings_dict, options, random_obj)
apply_starting_moves_settings(settings_dict, options)
apply_hint_settings(settings_dict, options)
apply_minigame_settings(settings_dict, options, multiworld)
apply_enemies(settings_dict, options)
if hasattr(multiworld, "generation_is_fake") and hasattr(multiworld, "re_gen_passthrough") and "Donkey Kong 64" in multiworld.re_gen_passthrough:
passthrough = multiworld.re_gen_passthrough["Donkey Kong 64"]
settings_dict["bonus_barrel_auto_complete"] = passthrough["Autocomplete"]
settings_dict["helm_room_bonus_count"] = HelmBonuses(passthrough["HelmBarrelCount"])

# Handle fake generation keys if needed
if hasattr(multiworld, "generation_is_fake"):
Expand Down
12 changes: 12 additions & 0 deletions archipelago/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,16 @@ class TrapLink(Toggle):
display_name = "Trap Link"


class DamageLink(Toggle):
"""Determines if Damage Link is enabled.

If enabled, when you take damage it is shared with other players who have Damage Link enabled, and their damage is shared with you.
Accumulated shared damage hurts your kong, but will never kill you on its own (it always leaves you with at least a sliver of health).
"""

display_name = "Damage Link"


class MirrorMode(Toggle):
"""Determines whether the game will be horizontally Mirrored."""

Expand Down Expand Up @@ -1957,6 +1967,7 @@ class DK64Options(PerGameCommonOptions):
ring_link: RingLink
tag_link: TagLink
trap_link: TrapLink
damage_link: DamageLink
goal: Goal
pregiven_keys: NumberOfStartingKeys
require_beating_krool: RequireBeatingKrool
Expand Down Expand Up @@ -2222,6 +2233,7 @@ class DK64Options(PerGameCommonOptions):
TagLink,
RingLink,
TrapLink,
DamageLink,
DeathLink,
],
),
Expand Down
2 changes: 2 additions & 0 deletions archipelago/client/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ class DK64MemoryMap:
sent_trap = 0x063
helm_hurry_item = 0x064
can_receive_shopkeeper = 0x065
receive_damage = 0x066
health = 0x807FCC4B # CollectableBase.Health
current_kong = 0x8074E77C
count_struct_pointer = 0x807FFFB8 # Pointer to CountStruct containing item counts

Expand Down
Loading