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

Pokemon Crystal: Implement new game #3494

Open
wants to merge 185 commits into
base: main
Choose a base branch
from

Conversation

AliceMousie
Copy link

What is this fixing or adding?

Adds Pokemon Crystal as a supported game

How was this tested?

Tested by the community over the last 7 months

If this makes graphical changes, please attach screenshots.

Title Screen has text "ARCHIPELAGO" added

Copy link
Collaborator

@Zunawe Zunawe left a comment

Choose a reason for hiding this comment

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

Started in __init__.py and got through pre_fill including going into functions in other files as they were called. I'll come back later and check the rest, but wanted to leave these comments for now anyway.

worlds/pokemon_crystal/__init__.py Outdated Show resolved Hide resolved
worlds/pokemon_crystal/__init__.py Outdated Show resolved Hide resolved
worlds/pokemon_crystal/__init__.py Show resolved Hide resolved
worlds/pokemon_crystal/regions.py Outdated Show resolved Hide resolved
worlds/pokemon_crystal/regions.py Outdated Show resolved Hide resolved
worlds/pokemon_crystal/__init__.py Show resolved Hide resolved
worlds/pokemon_crystal/__init__.py Outdated Show resolved Hide resolved
worlds/pokemon_crystal/rules.py Outdated Show resolved Hide resolved
worlds/pokemon_crystal/__init__.py Outdated Show resolved Hide resolved
Copy link
Collaborator

@Zunawe Zunawe left a comment

Choose a reason for hiding this comment

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

Another partial review. Just what I could get through today.

worlds/pokemon_crystal/client.py Outdated Show resolved Hide resolved
worlds/pokemon_crystal/client.py Show resolved Hide resolved
worlds/pokemon_crystal/client.py Outdated Show resolved Hide resolved
worlds/pokemon_crystal/client.py Outdated Show resolved Hide resolved
worlds/pokemon_crystal/items.py Outdated Show resolved Hide resolved
worlds/pokemon_crystal/data/regions.json Show resolved Hide resolved
worlds/pokemon_crystal/locations.py Show resolved Hide resolved
@Exempt-Medic Exempt-Medic added the waiting-on: author Issue/PR is waiting for feedback or changes from its author. label Jul 19, 2024
@Exempt-Medic Exempt-Medic removed the waiting-on: author Issue/PR is waiting for feedback or changes from its author. label Jul 20, 2024
Copy link
Collaborator

@Zunawe Zunawe left a comment

Choose a reason for hiding this comment

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

Still just minor things that are easily addressed.

Overall, I'd appreciate more type hints when it's not fully obvious from the function/arg name, but it's not terrible. I could figure them all out with a few seconds of reading the code.

Things I haven't checked yet: trainers.py, rules.py, rom.py, docs/, test/. Feel free to bother me if you get through this and I haven't finished up yet.

if item_code > 0 and get_item_classification(item_code) != ItemClassification.filler:
# If TMs are randomized, TM items without move names are added to the pool
if item_code in crystal_data.tm_replace_map and self.options.randomize_tm_moves:
default_itempool += [self.create_item_by_code(item_code + 256)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

default_itempool.append(item)
is faster and (in my opinion) more readable than
default_itempool += [item]

Same for other instances below.

Comment on lines +11 to +20
weighted_pool = [["RARE_CANDY"] * 3, ["ETHER", "ELIXER", "MAX_ETHER", "MAX_ELIXER", "MYSTERYBERRY"] * 5,
["WATER_STONE", "FIRE_STONE", "THUNDERSTONE", "LEAF_STONE", "SUN_STONE", "MOON_STONE"] * 2,
["ESCAPE_ROPE"] * 3, ["NUGGET", "STAR_PIECE", "STARDUST", "PEARL", "BIG_PEARL"] * 2,
["POKE_BALL", "GREAT_BALL", "ULTRA_BALL"] * 5,
["POTION", "SUPER_POTION", "ENERGY_ROOT", "ENERGYPOWDER"] * 12,
["HYPER_POTION", "FULL_RESTORE"] * 2, ["REPEL", "SUPER_REPEL", "MAX_REPEL"] * 3,
["REVIVE", "REVIVAL_HERB"] * 4 + ["MAX_REVIVE"] * 2,
["HP_UP", "PP_UP", "PROTEIN", "CARBOS", "CALCIUM", "IRON"] * 5,
["GUARD_SPEC", "DIRE_HIT", "X_ATTACK", "X_DEFEND", "X_SPEED", "X_SPECIAL"] * 2,
["HEAL_POWDER", "BURN_HEAL", "PARLYZ_HEAL", "ICE_HEAL", "ANTIDOTE", "AWAKENING", "FULL_HEAL"] * 5]
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would put this in a constant outside the function and name it _WEIGHTED_FILLER or something instead of constructing it every time the function is called.

I was going to suggest reformatting the data and using random.choices, but it turns out to be slower, so that would only be a matter of readability really.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Same for convert_to_ingame_text.


randomize_pokemon(self)

if self.options.randomize_starters.value:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm always going to be a fan of checking options explicitly against named values rather than relying on the falsiness of the particular option value, but I think I may be in the minority there. I value the safety and readability, but I know others consider it verbose and less elegant.

Disregard if you like it as it is.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I may have made this comment before; I'll try to remember that my opinion has been shared already and stop flagging it, lol.

Copy link
Member

@NewSoupVi NewSoupVi Aug 20, 2024

Choose a reason for hiding this comment

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

I think it should be even less characters: if self.options.randomize_starters

Copy link
Member

@NewSoupVi NewSoupVi Aug 20, 2024

Choose a reason for hiding this comment

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

Either way, everything that's been mentioned here is fine as far as core is concerned :)

@@ -0,0 +1,99 @@
from typing import TYPE_CHECKING
Copy link
Collaborator

Choose a reason for hiding this comment

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

I was going to comment on the strangeness of this file and its functions, but I realized I've been reading misc as short for "miscellaneous" and not "mischief", and I think that's likely going to be true for most people reading this for the first time. I don't think that ambiguity is worth the abbreviation. If nothing else, I'd suggest renaming this file.

ids = [item_id for item_id, item_data in data.items.items() if item_data.item_const == const_name]
if len(ids):
return ids[0] if ids[0] < 256 else ids[0] - 256
return 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it's a little suspicious to return 0 if you can't find a match. Is that default case useful somewhere? I think I would rather see an exception here than get reports of missing or nonexistant or empty items and tracking it back here. Especially if it's "exceptional" to not match an item name.

Copy link
Collaborator

Choose a reason for hiding this comment

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

These two functions are also reasonable candidates for the @cache decorator.

return new_learnset


def get_random_move(random, move_type=None, attacking=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like attacking should be default False and just have line 43 do if attacking:

Comment on lines +38 to +42
not move_data.is_hm and move_name not in ["STRUGGLE", "BEAT_UP", "NO_MOVE", "STRUGGLE"]]
else:
move_pool = [move_name for move_name, move_data in crystal_data.moves.items() if
not move_data.is_hm and move_data.type == move_type
and move_name not in ["STRUGGLE", "BEAT_UP", "NO_MOVE", "STRUGGLE"]]
Copy link
Collaborator

@Zunawe Zunawe Aug 2, 2024

Choose a reason for hiding this comment

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

Extra STRUGGLEs.

While I'm here, list is (theoretically) slower than tuple is (actually) slower than set even for these ad-hoc filters where you build a blacklist inside the comprehension. Probably mostly inconsequential, but getting random moves is a very hot path in Emerald, so if there's anywhere that small notes on performance might actually matter, it's somewhere like this.

EDIT: Black Sliver made a writeup recently on this topic. I would trust his opinion/experience/measurements over my own. I think (hope) that document is planned to be PR'd to this repo somewhere, but I think for now we only have this link to the discord message.

Comment on lines +70 to +71
move_pool = [move_data for move_name, move_data in copy.deepcopy(crystal_data.moves).items() if
not move_data.is_hm and move_name not in ["ROCK_SMASH", "NO_MOVE", "STRUGGLE"]]
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think you need to make a copy of the moves list here. I could be missing something.

move.level <= level and move.move != "NO_MOVE"]
# double learnset pool to dilute HMs slightly
# exclude beat up as it can softlock the game if an enemy trainer uses it
move_pool += move_pool + [move for move in world.generated_pokemon[pokemon].tm_hm if move != "BEAT_UP"]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can Beat Up sneak in here if learnsets are vanilla?



def generate_phone_traps(world: "PokemonCrystalWorld"):
if world.options.phone_trap_weight.value:
Copy link
Collaborator

Choose a reason for hiding this comment

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

This if isn't necessary here, since the same condition is used to decide whether to call this function in the first place.

If that weren't the case, there are a couple variables that get initialized inside this if block which are used below it, which would cause exceptions if this condition were ever False while in this function.

Copy link
Collaborator

@Zunawe Zunawe left a comment

Choose a reason for hiding this comment

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

More nitpicks and small improvements. Finally got through every file.

Left a lot of comments, but I have no major issues with the structure of the code on the whole. Only a handful of things really need addressing, and the rest is just code quality and minor optimizations.

Generated a few 1-world and a few 100-world multis, both with entirely random yamls. And I've generated with the world before in actual real games. I haven't played it myself though.

There's a conflict with main in the current list of games in the README, but otherwise all testing was after merging this branch with main.

ride with the S.S. Ticket
- Magnet train between Goldenrod and Saffron is availble to ride with the Pass before power is restored to Kanto
- Misty is always in Cerulean Gym
- There is a ledge above the Route 2 entry to Digglet Cave, allowing you to reach the rest of West Kanto without Cut
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
- There is a ledge above the Route 2 entry to Digglet Cave, allowing you to reach the rest of West Kanto without Cut
- There is a ledge above the Route 2 entry to Diglett Cave, allowing you to reach the rest of West Kanto without Cut

## Required Software

- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- An English (UE) Pokémon Crystal v1.0 ROM. The Archipelago community cannot provide this.
Copy link
Collaborator

Choose a reason for hiding this comment

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

You include support for v1.1 now too.


- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- An English (UE) Pokémon Crystal v1.0 ROM. The Archipelago community cannot provide this.
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later. 2.9.1 is recommended.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I have a slight hesitation with recommending a particular version, because it will be outdated eventually (as it was with OoT, which is why so many people continued to use 2.7 even after 2.9.1 released). And especially when <=2.9.1 has that arbitrary code execution bug on N64 games.

Comment on lines +156 to +166
def hidden():
return world.options.randomize_hidden_items

def pokegear():
return world.options.randomize_pokegear

def johto_only():
return world.options.johto_only.value

def trainersanity():
return world.options.trainersanity
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think these can be normal variables instead of functions.

hidden = bool(world.options.randomize_hidden_items)

or the value, or whatever you think makes the most sense. But it looks like they're only shortcuts, so nothing is really gained from them being functions.

Comment on lines +23 to +24
rom_bytes = bytearray(rom)
if rom_bytes[revision_address] == 1:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
rom_bytes = bytearray(rom)
if rom_bytes[revision_address] == 1:
if rom[revision_address] == 1:

item_flag = location.address
player_name = world.multiworld.player_name[location.item.player].upper()
item_name = location.item.name.upper()
item_texts.append([player_name, item_name, item_flag])
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
item_texts.append([player_name, item_name, item_flag])
item_texts.append((player_name, item_name, item_flag))

# if we somehow run out of capacity in both banks, just finish the table and break,
# there is a fallback string in the ROM, so it should handle this gracefully.
write_bytes(patch, [0xFF], item_name_table_adr + table_offset_adr)
print("oopsie")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Lol, if leaving this in was intentional, it should probably be a logging.warning or debug or something with a clearer message.

write_bytes(patch, [0xFF], item_name_table_adr + item_name_table_length - 1)

if world.options.randomize_static_pokemon:
for _static_name, pkmn_data in world.generated_static.items():
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
for _static_name, pkmn_data in world.generated_static.items():
for pkmn_data in world.generated_static.values():

Comment on lines +135 to +139
for i in range(3): # morn, day, nite
for encounter in grass_encounters:
pokemon_id = data.pokemon[encounter.pokemon].id
write_bytes(patch, [encounter.level, pokemon_id], cur_address)
cur_address += 2
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this for i in range(3) accomplishes anything; i is unused in here.


start_inventory_address = data.rom_addresses["AP_Start_Inventory"]
start_inventory = copy.deepcopy(world.options.start_inventory.value)
for item, quantity in start_inventory.items():
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd recommend building this from world.multiworld.precollected_items[world.player] instead. If you ever decide to automatically add something into your players' starting inventories, it's usually recommended to use push_precollected instead of modifying the player's options. And you want to give the player anything that the multiworld as a whole has decided is precollected, which may be partially outside your control. The generator's relatively new panic_method setting might put unplaced progression items into precollected_items. I've not tried that setting out myself though.

@NewSoupVi
Copy link
Member

NewSoupVi commented Sep 1, 2024

You should look at this
#3865

The overlapping location/item IDs between this game and ALTTP are causing the Rogue Legacy client to crash. That is not "your fault" as Crystal is unsupported right now, but as it stands, as long as there are supported games whose clients crash with overlapping IDs, we won't allow a game with overlapping IDs to become supported, so it is blocking for this PR.

@NewSoupVi NewSoupVi added the waiting-on: author Issue/PR is waiting for feedback or changes from its author. label Sep 1, 2024
@AliceMousie
Copy link
Author

I can move this to a draft until that gets fixed if you'd prefer, I've been short on time to look at the reviews anyway. I don't want to bring back the stupid ID offsetting again, and I'm sure there will be more broken worlds found from Crystal overlapping.

@NewSoupVi
Copy link
Member

NewSoupVi commented Sep 2, 2024

Drafting hurts the visibility so I wouldn't necessarily ask you to do that, since this PR is, for all other intents and purposes, fully "reviewable". But maybe we can slap the "waiting on: other" label on it (I'm on mobile rn and idk how to do it, if noone else gets to it I'll try to remember later)

@Exempt-Medic Exempt-Medic added the waiting-on: other Issue/PR is waiting for something else, like another PR. label Sep 2, 2024
@Zunawe
Copy link
Collaborator

Zunawe commented Sep 3, 2024

One more comment, you should add an entry to inno_setup.iss for the installer to register the crystal patch file with BizHawkClient.exe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
is: new game Pull requests for implementing new games into Archipelago. waiting-on: author Issue/PR is waiting for feedback or changes from its author. waiting-on: other Issue/PR is waiting for something else, like another PR. waiting-on: peer-review Issue/PR has not been reviewed by enough people yet.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants