Archipelago/worlds/cvcotm/lz10.py

266 lines
6.4 KiB
Python
Raw Normal View History

Castlevania: Circle of the Moon - Implement New Game (#3299) * Add the cotm package with working seed playthrough generation. * Add the proper event flag IDs for the Item codes. * Oooops. Put the world completion condition in! * Adjust the game name and abbreviations. * Implement more settings. * Account for too many start_inventory_from_pool cards with Halve DSS Cards Placed. * Working (albeit very sloooooooooooow) ROM patching. * Screw you, bsdiff! AP Procedure Patch for life! * Nuke stage_assert_generate as the ROM is no longer needed for that. * Working item writing and position adjusting. * Fix the magic item graphics in Locations wherein they can be fixed. * Enable sub-weapon shuffle * Get the seed display working. * Get the enemy item drop randomization working. Phew! * Enemy drop rando and seed display fixes. * Functional Countdown + Early Double setting * Working multiworld (yay!) * Fix item links and demo shenanigans. * Add Wii U VC hash and a docs section explaining the rereleases. * Change all client read/writes to EWRAM instead of Combined WRAM. * Custom text insertion foundations. * Working text converter and word wrap detector. * More refinements to the text wrap system. * Well and truly working sent/received messages. * Add DeathLink and Battle Arena goal options. * Add tracker stuff, unittests, all locations countdown, presets. * Add to README, CODEOWNERS, and inno_setup * Add to README, CODEOWNERS, and inno_setup * Address some suggestions/problems. * Switch the Items and Locations to using dataclasses. * Add note about the alternate classes to the Game Page. * Oooops, typo! * Touch up the Options descriptions. * Fix Battle Arena flag being detected incorrectly on connection and name the locked location/item pairs better. * Implement option groups * Swap the Lizard-man Locations into their correct Regions. * Local start inventory, better DeathLink message handling, handle receiving over 255 of an item. * Update the PopTracker pack links to no longer point to the Releases page. * Add Skip Dialogues option. * Update the presets for the accessibility rework. * Swap the choices in the accessibility preset options. * Uhhhhhhh...just see the apworld v4 changelog for this one. * Ooops, typo! * . * Bunch of small stuff * Correctly change "Fake" to "Breakable" in this comment. Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Make can_touch_water one line. Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Make broke_iron_maidens one line. Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Fix majors countdown and make can_open_ceremonial_door one line. * Make the Trap AP Item less obvious. * Add Progression + Useful stuff, patcher handling for incompatible versions, and fix some mypy stuff. * Better option groups. * Change Early Double to Early Escape Item. * Update DeathLink description and ditch the Menu region. * Fix the Start Broken choice for Iron Maiden Behavior * Remove the forced option change with Arena goal + required All Bosses and Arena. * Update the Game Page with the removal of the forced option combination change. * Fix client potential to send packets nonstop. * More review addressing. * Fix the new select_drop code. * Fix the new select_drop code for REAL this time. * Send another LocationScout if we send Location checks without having the Location info. --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-12-12 13:47:47 +00:00
from collections import defaultdict
from operator import itemgetter
import struct
from typing import Union
ByteString = Union[bytes, bytearray, memoryview]
"""
Taken from the Archipelago Metroid: Zero Mission implementation by Lil David at:
https://github.com/lilDavid/Archipelago-Metroid-Zero-Mission/blob/main/lz10.py
Tweaked version of nlzss modified to work with raw data and return bytes instead of operating on whole files.
LZ11 functionality has been removed since it is not necessary for Zero Mission nor Circle of the Moon.
https://github.com/magical/nlzss
"""
def decompress(data: ByteString):
"""Decompress LZSS-compressed bytes. Returns a bytearray containing the decompressed data."""
header = data[:4]
if header[0] == 0x10:
decompress_raw = decompress_raw_lzss10
else:
raise DecompressionError("not as lzss-compressed file")
decompressed_size = int.from_bytes(header[1:], "little")
data = data[4:]
return decompress_raw(data, decompressed_size)
def compress(data: bytearray):
byteOut = bytearray()
# header
byteOut.extend(struct.pack("<L", (len(data) << 8) + 0x10))
# body
length = 0
for tokens in chunkit(_compress(data), 8):
flags = [type(t) is tuple for t in tokens]
byteOut.extend(struct.pack(">B", packflags(flags)))
for t in tokens:
if type(t) is tuple:
count, disp = t
count -= 3
disp = (-disp) - 1
assert 0 <= disp < 4096
sh = (count << 12) | disp
byteOut.extend(struct.pack(">H", sh))
else:
byteOut.extend(struct.pack(">B", t))
length += 1
length += sum(2 if f else 1 for f in flags)
# padding
padding = 4 - (length % 4 or 4)
if padding:
byteOut.extend(b'\xff' * padding)
return byteOut
class SlidingWindow:
# The size of the sliding window
size = 4096
# The minimum displacement.
disp_min = 2
# The hard minimum — a disp less than this can't be represented in the
# compressed stream.
disp_start = 1
# The minimum length for a successful match in the window
match_min = 3
# The maximum length of a successful match, inclusive.
match_max = 3 + 0xf
def __init__(self, buf):
self.data = buf
self.hash = defaultdict(list)
self.full = False
self.start = 0
self.stop = 0
# self.index = self.disp_min - 1
self.index = 0
assert self.match_max is not None
def next(self):
if self.index < self.disp_start - 1:
self.index += 1
return
if self.full:
olditem = self.data[self.start]
assert self.hash[olditem][0] == self.start
self.hash[olditem].pop(0)
item = self.data[self.stop]
self.hash[item].append(self.stop)
self.stop += 1
self.index += 1
if self.full:
self.start += 1
else:
if self.size <= self.stop:
self.full = True
def advance(self, n=1):
"""Advance the window by n bytes"""
for _ in range(n):
self.next()
def search(self):
match_max = self.match_max
match_min = self.match_min
counts = []
indices = self.hash[self.data[self.index]]
for i in indices:
matchlen = self.match(i, self.index)
if matchlen >= match_min:
disp = self.index - i
if self.disp_min <= disp:
counts.append((matchlen, -disp))
if matchlen >= match_max:
return counts[-1]
if counts:
match = max(counts, key=itemgetter(0))
return match
return None
def match(self, start, bufstart):
size = self.index - start
if size == 0:
return 0
matchlen = 0
it = range(min(len(self.data) - bufstart, self.match_max))
for i in it:
if self.data[start + (i % size)] == self.data[bufstart + i]:
matchlen += 1
else:
break
return matchlen
def _compress(input, windowclass=SlidingWindow):
"""Generates a stream of tokens. Either a byte (int) or a tuple of (count,
displacement)."""
window = windowclass(input)
i = 0
while True:
if len(input) <= i:
break
match = window.search()
if match:
yield match
window.advance(match[0])
i += match[0]
else:
yield input[i]
window.next()
i += 1
def packflags(flags):
n = 0
for i in range(8):
n <<= 1
try:
if flags[i]:
n |= 1
except IndexError:
pass
return n
def chunkit(it, n):
buf = []
for x in it:
buf.append(x)
if n <= len(buf):
yield buf
buf = []
if buf:
yield buf
def bits(byte):
return ((byte >> 7) & 1,
(byte >> 6) & 1,
(byte >> 5) & 1,
(byte >> 4) & 1,
(byte >> 3) & 1,
(byte >> 2) & 1,
(byte >> 1) & 1,
byte & 1)
def decompress_raw_lzss10(indata, decompressed_size, _overlay=False):
"""Decompress LZSS-compressed bytes. Returns a bytearray."""
data = bytearray()
it = iter(indata)
if _overlay:
disp_extra = 3
else:
disp_extra = 1
def writebyte(b):
data.append(b)
def readbyte():
return next(it)
def readshort():
# big-endian
a = next(it)
b = next(it)
return (a << 8) | b
def copybyte():
data.append(next(it))
while len(data) < decompressed_size:
b = readbyte()
flags = bits(b)
for flag in flags:
if flag == 0:
copybyte()
elif flag == 1:
sh = readshort()
count = (sh >> 0xc) + 3
disp = (sh & 0xfff) + disp_extra
for _ in range(count):
writebyte(data[-disp])
else:
raise ValueError(flag)
if decompressed_size <= len(data):
break
if len(data) != decompressed_size:
raise DecompressionError("decompressed size does not match the expected size")
return data
class DecompressionError(ValueError):
pass