forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
245 lines
7.1 KiB
Python
245 lines
7.1 KiB
Python
"""Patcher class and Functions for modifying ROM files."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import os
|
|
from io import BytesIO
|
|
import pkgutil
|
|
from typing import TYPE_CHECKING, Union
|
|
|
|
import js
|
|
|
|
if TYPE_CHECKING:
|
|
from randomizer.Enums.Kongs import Kongs
|
|
from randomizer.Lists.EnemyTypes import Enemies
|
|
from randomizer.Lists.MapsAndExits import Maps
|
|
from randomizer.Patching.ItemRando import CustomActors
|
|
|
|
|
|
class ROM:
|
|
"""Patcher for ROM files loaded via Rompatcherjs."""
|
|
|
|
def __init__(self, file=None):
|
|
"""Patch functions for the ROM loaded within Rompatcherjs.
|
|
|
|
This is mostly a hint file, you could directly call the javascript functions,
|
|
but to keep this a bit more logical for team members we just import it and treat
|
|
this like a bytesIO object.
|
|
|
|
Args:
|
|
file ([type], optional): [description]. Defaults to None.
|
|
"""
|
|
if file is None:
|
|
self.rom = js.patchedRom
|
|
else:
|
|
self.rom = file
|
|
|
|
def write(self, val: int):
|
|
"""Write value to current position.
|
|
|
|
Starts at 0x0 as the inital position without seeking.
|
|
|
|
Args:
|
|
val (int): Int value to write.
|
|
"""
|
|
self.rom.writeU8(val)
|
|
|
|
def writeBytes(self, byte_data: bytes):
|
|
"""Write an array a bytes to the current position.
|
|
|
|
Starts at 0x0 as the inital position without seeking.
|
|
|
|
Args:
|
|
byte_data (bytes): Bytes object to write to current position.
|
|
"""
|
|
self.rom.writeBytes(bytes(byte_data))
|
|
|
|
def writeMultipleBytes(self, value: int, size: int):
|
|
"""Write multiple bytes of a size to the current position.
|
|
|
|
Starts at 0x0 as the inital position without seeking.
|
|
|
|
Args:
|
|
value (int): Value to write.
|
|
size (int): Size of the bytes to write.
|
|
"""
|
|
arr = []
|
|
temp = value
|
|
for x in range(size):
|
|
arr.append(0)
|
|
will_pass = True
|
|
idx = size - 1
|
|
while will_pass:
|
|
write = temp % 256
|
|
arr[idx] = write
|
|
temp = int((temp - write) / 256)
|
|
if idx == 0 or temp == 0:
|
|
will_pass = False
|
|
idx -= 1
|
|
for x in arr:
|
|
self.write(x)
|
|
|
|
def save(self, file_name: str):
|
|
"""Save the patched file to a downloadable file.
|
|
|
|
You need to pass the whole file and extension.
|
|
eg save("dk64-randomizer-12345.z64")
|
|
|
|
Args:
|
|
file_id (str): Name of file to save as.
|
|
"""
|
|
self.rom.fileName = file_name
|
|
self.rom.save()
|
|
|
|
def seek(self, val: int):
|
|
"""Seek to position in current file.
|
|
|
|
Args:
|
|
val (int): Position to seek to.
|
|
"""
|
|
self.rom.seek(val)
|
|
|
|
def readBytes(self, len: int):
|
|
"""Read bytes from current position.
|
|
|
|
Starts at 0x0 as the inital position without seeking.
|
|
|
|
Args:
|
|
len (int): Length to read.
|
|
|
|
Returns:
|
|
bytes: List of bytes read from current position.
|
|
"""
|
|
return bytes(self.rom.readBytes(len))
|
|
|
|
def fixChecksum(self):
|
|
"""Fix the checksum of the current file."""
|
|
js.fixChecksum(self.rom)
|
|
|
|
def fixSecurityValue(self):
|
|
"""Set the security code and update the rom checksum."""
|
|
self.seek(0x3154)
|
|
self.write(0)
|
|
self.fixChecksum()
|
|
|
|
|
|
# Try except for when the browser is trying to load this file
|
|
def load_base_rom() -> None:
|
|
"""Load the base ROM file for patching."""
|
|
try:
|
|
print("Loading base rom")
|
|
from randomizer.Patching import BPS as bps
|
|
|
|
try:
|
|
patch = open("./static/patches/shrink-dk64.bps", "rb")
|
|
except Exception:
|
|
try:
|
|
patch = BytesIO(js.getFile("static/patches/shrink-dk64.bps"))
|
|
except Exception:
|
|
patch = open("./worlds/dk64/static/patches/shrink-dk64.bps", "rb")
|
|
|
|
original = open("dk64.z64", "rb")
|
|
og_patched_rom = BytesIO(bps.patch(original, patch).read())
|
|
return og_patched_rom
|
|
except Exception as e:
|
|
print(e)
|
|
raise Exception("Unable to load BPS.") from e
|
|
pass
|
|
|
|
|
|
class LocalROM:
|
|
"""Patcher for ROM files loaded via Rompatcherjs."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Patch functions for the ROM loaded within Rompatcherjs.
|
|
|
|
This is mostly a hint file, you could directly call the javascript functions,
|
|
but to keep this a bit more logical for team members we just import it and treat
|
|
this like a bytesIO object.
|
|
|
|
Args:
|
|
file ([type], optional): [description]. Defaults to None.
|
|
"""
|
|
patchedRom = None
|
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
|
data_size = 32 * 1024 # 32KB = 32 * 1024 bytes
|
|
data = bytes(range(256)) * (data_size // 256) # Repeat values from 0 to 255 to fill 32KB
|
|
# Create a BytesIO object
|
|
patchedRom = BytesIO(data)
|
|
else:
|
|
if not os.path.exists("dk64.z64"):
|
|
raise Exception("No ROM was loaded, please make sure you have dk64.z64 in the root directory of the project.")
|
|
elif patchedRom is None:
|
|
patchedRom = load_base_rom()
|
|
|
|
self.rom = patchedRom
|
|
|
|
def write(self, val: Union[Maps, int]) -> None:
|
|
"""Write value to current position.
|
|
|
|
Starts at 0x0 as the inital position without seeking.
|
|
|
|
Args:
|
|
val (int): Int value to write.
|
|
"""
|
|
self.rom.write((val).to_bytes(1, byteorder="big", signed=False))
|
|
|
|
def writeBytes(self, byte_data: Union[bytearray, bytes]) -> None:
|
|
"""Write an array a bytes to the current position.
|
|
|
|
Starts at 0x0 as the inital position without seeking.
|
|
|
|
Args:
|
|
byte_data (bytes): Bytes object to write to current position.
|
|
"""
|
|
self.rom.write(bytes(byte_data))
|
|
|
|
def writeMultipleBytes(self, value: Union[int, Enemies, Maps, Kongs, CustomActors], size: int) -> None:
|
|
"""Write multiple bytes of a size to the current position.
|
|
|
|
Starts at 0x0 as the inital position without seeking.
|
|
|
|
Args:
|
|
value (int): Value to write.
|
|
size (int): Size of the bytes to write.
|
|
"""
|
|
arr = []
|
|
temp = value
|
|
for x in range(size):
|
|
arr.append(0)
|
|
will_pass = True
|
|
idx = size - 1
|
|
while will_pass:
|
|
if temp is None:
|
|
temp = 0
|
|
write = temp % 256
|
|
arr[idx] = write
|
|
temp = int((temp - write) / 256)
|
|
if idx == 0 or temp == 0:
|
|
will_pass = False
|
|
idx -= 1
|
|
for x in arr:
|
|
self.write(x)
|
|
|
|
def seek(self, val: int) -> None:
|
|
"""Seek to position in current file.
|
|
|
|
Args:
|
|
val (int): Position to seek to.
|
|
"""
|
|
self.rom.seek(val)
|
|
|
|
def readBytes(self, len: int) -> bytes:
|
|
"""Read bytes from current position.
|
|
|
|
Starts at 0x0 as the inital position without seeking.
|
|
|
|
Args:
|
|
len (int): Length to read.
|
|
|
|
Returns:
|
|
bytes: List of bytes read from current position.
|
|
"""
|
|
return bytes(self.rom.read(len))
|