Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spelunker: Implement New Game #3282

Draft
wants to merge 39 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1ee9467
Create d
PinkSwitch Dec 25, 2023
f14c704
Create d
PinkSwitch Dec 25, 2023
cad67a8
Delete worlds/mariomissing/d
PinkSwitch Dec 25, 2023
1e6d15f
Delete mariomissing directory
PinkSwitch Dec 25, 2023
4a964e4
Create d
PinkSwitch Dec 25, 2023
5f3608b
Add files via upload
PinkSwitch Dec 25, 2023
814b145
Delete worlds/mariomissing/d
PinkSwitch Dec 25, 2023
e45fd1c
Delete worlds/mariomissing directory
PinkSwitch Dec 25, 2023
39e056c
Merge branch 'ArchipelagoMW:main' into main
PinkSwitch Jan 6, 2024
06faaa5
Merge branch 'ArchipelagoMW:main' into main
PinkSwitch Apr 7, 2024
6b1358e
Merge branch 'ArchipelagoMW:main' into main
PinkSwitch Apr 14, 2024
d4f61c2
Merge branch 'ArchipelagoMW:main' into main
PinkSwitch Apr 23, 2024
4310e10
Add files via upload
PinkSwitch Apr 23, 2024
7c0a350
Delete worlds/sai2 directory
PinkSwitch Apr 23, 2024
dee8fbb
Merge branch 'ArchipelagoMW:main' into main
PinkSwitch May 6, 2024
0bccc74
Merge branch 'ArchipelagoMW:main' into spelunker
PinkSwitch May 9, 2024
6bbfeb2
Create spelunker
PinkSwitch May 9, 2024
95d70d6
Delete worlds/spelunker
PinkSwitch May 9, 2024
cf3816c
Create t
PinkSwitch May 9, 2024
b606513
Delete worlds/spelunker/t
PinkSwitch May 9, 2024
ac0bd67
Create t
PinkSwitch May 9, 2024
73a4b64
Add New Game: Spelunker
PinkSwitch May 9, 2024
ceb36cf
Delete worlds/spelunker/t
PinkSwitch May 9, 2024
ac3bcb5
Fix stray instances of YoshisIslandWorld
PinkSwitch May 9, 2024
c73df3d
Clean stray commas and fix incorrect check values
PinkSwitch May 9, 2024
005ebbf
Fix rom still being asked for even though it was on procedure patch
PinkSwitch May 9, 2024
2811813
Update worlds/spelunker/Client.py
PinkSwitch May 9, 2024
176791d
Clean up code and remove unnecessary code
PinkSwitch May 9, 2024
c722172
Remove unnecessary item return
PinkSwitch May 10, 2024
fd4dd72
Convert item lists to lists
PinkSwitch May 10, 2024
9a12623
fix the rest of the stuff
PinkSwitch May 10, 2024
0827165
remove energylink from options
PinkSwitch May 10, 2024
6ddc992
Update Options.py
PinkSwitch May 10, 2024
29c8f8d
Fix deathlink being always enabled
PinkSwitch May 13, 2024
1169999
Merge branch 'ArchipelagoMW:main' into spelunker
PinkSwitch May 23, 2024
179e4cd
Update README.md
PinkSwitch May 23, 2024
988193c
Update CODEOWNERS
PinkSwitch May 23, 2024
41f7c9c
Merge branch 'ArchipelagoMW:main' into spelunker
PinkSwitch Jun 5, 2024
5e7e8c2
Merge branch 'ArchipelagoMW:main' into spelunker
PinkSwitch Jun 22, 2024
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
207 changes: 207 additions & 0 deletions worlds/spelunker/Client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import logging
import struct
import time
from struct import pack
from .Rom import location_table, hidden_table
from typing import TYPE_CHECKING, Dict, Set
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved

# TODO: Remove this when Archipelago 0.4.4 gets released
import sys

if "worlds._bizhawk" not in sys.modules:
import importlib
import os
import zipimport

bh_apworld_path = os.path.join(
os.path.dirname(sys.modules["worlds"].__file__), "_bizhawk.apworld"
)
if os.path.isfile(bh_apworld_path):
importer = zipimport.zipimporter(bh_apworld_path)
spec = importer.find_spec(os.path.basename(bh_apworld_path).rsplit(".", 1)[0])
mod = importlib.util.module_from_spec(spec)
mod.__package__ = f"worlds.{mod.__package__}"
mod.__name__ = f"worlds.{mod.__name__}"
sys.modules[mod.__name__] = mod
importer.exec_module(mod)
elif not os.path.isdir(os.path.splitext(bh_apworld_path)[0]):
raise AssertionError("Could not import worlds._bizhawk")
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved

from NetUtils import ClientStatus
import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient
import time

if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext
else:
BizHawkClientContext = object
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved

# Add .apwl suffix to bizhawk client
from worlds.LauncherComponents import SuffixIdentifier, components

for component in components:
if component.script_name == "BizHawkClient":
component.file_identifier = SuffixIdentifier(
*(*component.file_identifier.suffixes, ".apsplunker")
)
break
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved

EXPECTED_ROM_NAME = "SPELUNKERAP"


class SpelunkerClient(BizHawkClient):
game = "Spelunker"
system = ("NES")
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved
location_map = location_table
received_deathlinks = 0
deathlink_all_clear = False

def __init__(self) -> None:
super().__init__()

async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from CommonClient import logger

try:
# Check ROM name/patch version
rom_name_bytes = (
await bizhawk.read(ctx.bizhawk_ctx, [(0x7030, 11, "PRG ROM")])
)[0]
rom_name = bytes([byte for byte in rom_name_bytes if byte != 0]).decode(
"ascii"
)
if not rom_name.startswith(EXPECTED_ROM_NAME):
logger.info(
"ERROR: Rom is not valid!"
)
return False
except UnicodeDecodeError:
return False
except bizhawk.RequestFailedError:
return False # Should verify on the next pass

ctx.game = self.game
ctx.items_handling = 0b111

death_link = await bizhawk.read(ctx.bizhawk_ctx, [(0x7052, 1, "PRG ROM")])
if death_link:
await ctx.update_death_link(bool(death_link[0]))
return True

async def set_auth(self, ctx: BizHawkClientContext) -> None:
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved
from CommonClient import logger
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved

slot_name_length = await bizhawk.read(ctx.bizhawk_ctx, [(0x7040, 1, "PRG ROM")])
slot_name_bytes = await bizhawk.read(
ctx.bizhawk_ctx, [(0x7041, slot_name_length[0][0], "PRG ROM")]
)
ctx.auth = bytes([byte for byte in slot_name_bytes[0] if byte != 0]).decode(
"utf-8"
)

def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
if cmd != "Bounced":
return
if "tags" not in args:
return
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that two players playing on the same slot won't send deathlinks to each other. If you don't care about supporting that, don't worry about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to think on this one.

self.received_deathlinks += 1

async def game_watcher(self, ctx: BizHawkClientContext) -> None:
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved
from CommonClient import logger
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved

if ctx.server_version.build > 0:
ctx.connected = True
Copy link
Member

@Exempt-Medic Exempt-Medic Aug 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't know anything about clients, so this might be some issue somewhere outside of this world, but mypy does state:

"BizHawkClientContext" has no attribute "connected"

I asked about it in the server https://discord.com/channels/731205301247803413/1214608557077700720/1275922312646103151

else:
ctx.connected = False
ctx.refresh_connect = True

if ctx.slot_data != None:
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved
ctx.data_present = True
else:
ctx.data_present = False

if ctx.server is None or ctx.server.socket.closed or ctx.slot_data is None:
return

from .Rom import item_ids

#if goal_flag[0] != 0x00:
#await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
#ctx.finished_game = True

read_state = await bizhawk.read(ctx.bizhawk_ctx, [(0x229, 1, "RAM"),
(0x0500, 0xFF, "RAM"),
(0x0780, 1, "RAM"),
(0x7051, 1, "PRG ROM"),
(0x022C, 1, "RAM"),
(0x022D, 1, "RAM"),
(0x0783, 1, "RAM"),
(0x0781, 1, "RAM")])

demo_mode = int.from_bytes(read_state[0], "little")
loc_array = bytearray(read_state[1])
item_pause = int.from_bytes(read_state[2], "little")
hidden_checks = int.from_bytes(read_state[3], "little")
is_dead = int.from_bytes(read_state[4], "little")
is_paused = int.from_bytes(read_state[5], "little")
goal_trigger = int.from_bytes(read_state[6], "little")
recv_count = int.from_bytes(read_state[7], "big")

if hidden_checks == 0x01:
self.location_map.update(hidden_table)

if demo_mode != 0x00:
return

if item_pause != 0x00:
return

if is_dead == 0x00:
self.deathlink_all_clear = True

if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
if is_dead in range(0x01,0x80) and self.received_deathlinks == 0x00 and is_paused != 0xFF and self.deathlink_all_clear == True:
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved
self.deathlink_all_clear = False
await ctx.send_death(f"{ctx.player_names[ctx.slot]} died!")

#The death animation is long enough to send 2 deathlinks per death
if self.received_deathlinks != 0:
await bizhawk.write(ctx.bizhawk_ctx, [(0x780, bytes([0x0D]), "RAM")])
self.received_deathlinks -= 1

new_checks = []

for loc_id, loc_pointer in self.location_map.items():
if loc_id not in ctx.locations_checked:
location = loc_array[loc_pointer]
if location == 0:
new_checks.append(loc_id)

if loc_id in ctx.checked_locations:
loc_array[loc_pointer] = 0

await bizhawk.write(ctx.bizhawk_ctx, [(0x0500, loc_array, "RAM")])

for new_check_id in new_checks:
ctx.locations_checked.add(new_check_id)
location = ctx.location_names[new_check_id]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is location used anywhere?

await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved

if recv_count < len(ctx.items_received):
item = ctx.items_received[recv_count]
recv_count += 1

if item.item in item_ids:
ram_item = item_ids[item.item]
await bizhawk.write(ctx.bizhawk_ctx, [(0x780, bytes([ram_item]), "RAM")])
await bizhawk.write(ctx.bizhawk_ctx, [(0x781, bytes([recv_count]), "RAM")])

if not ctx.finished_game and goal_trigger == 0x01:
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL
}])


PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved
49 changes: 49 additions & 0 deletions worlds/spelunker/Items.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from typing import Dict, Set, Tuple, NamedTuple, Optional
from BaseClasses import ItemClassification

class ItemData(NamedTuple):
category: str
code: Optional[int]
classification: ItemClassification
amount: Optional[int] = 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a time this can be None? If not, you can just do this:

Suggested change
amount: Optional[int] = 1
amount: int = 1

Another thing you could do is change the default to 0 and you could remove the , 0 from the item_table, but that's stylistic, and totally up to you


item_table: Dict[str, ItemData] = {
"Money Bag": ItemData("Treasure", 0x696969, ItemClassification.filler, 0),
"Coin": ItemData("Treasure", 0x69696A, ItemClassification.filler, 0),
"Miracle": ItemData("Treasure", 0x69696B, ItemClassification.filler, 0),
"Diamond": ItemData("Treasure", 0x69696C, ItemClassification.filler, 0),
"Dynamite": ItemData("Equipment", 0x69696D, ItemClassification.progression, 18),
"Flare": ItemData("Equipment", 0x69696E, ItemClassification.progression, 9),
"Blue Key": ItemData("Equipment", 0x69696F, ItemClassification.progression, 9),
"Red Key": ItemData("Equipment", 0x696970, ItemClassification.progression, 6),

"1-Up": ItemData("Powerups", 0x696971, ItemClassification.useful, 0),
"Multiplier": ItemData("Powerups", 0x696972, ItemClassification.useful, 0),
"Potion": ItemData("Powerups", 0x696973, ItemClassification.useful, 0),
"Invincibility": ItemData("Powerups", 0x696974, ItemClassification.useful, 0),

"Golden Pyramid": ItemData("Events", None, ItemClassification.progression, 0)
}

filler_items: Tuple[str, ...] = (
"Money Bag",
"Coin",
"Diamond",
"Miracle"
)

useful_items: Tuple[str, ...] = (
"1-Up",
"Multiplier",
"Potion",
"Invincibility"
)
PinkSwitch marked this conversation as resolved.
Show resolved Hide resolved

def get_item_names_per_category() -> Dict[str, Set[str]]:
categories: Dict[str, Set[str]] = {}

for name, data in item_table.items():
if data.category != "Events":
categories.setdefault(data.category, set()).add(name)

return categories
Loading
Loading