Skip to content

Commit

Permalink
Version checking
Browse files Browse the repository at this point in the history
- added a new GameVersion enum that tracks game, version, and md5
  checksums
- added functions to identify version, verify files and verify data for
  a FileTreeEditor
- MSR and Dread now only load files that exist for your version
- resource json's store dicts instead of values, with a crc field and an
  optional versions field of which versions the file is for
- updated the binary format to include a 2byte bitfield for which
  versions each file is in
  • Loading branch information
steven11sjf committed Oct 14, 2024
1 parent 430c6ef commit fe75d40
Show file tree
Hide file tree
Showing 10 changed files with 180,091 additions and 54,902 deletions.
44 changes: 44 additions & 0 deletions src/mercury_engine_data_structures/_dread_data_construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import construct

from mercury_engine_data_structures.game_check import GameVersion


class CompressedZSTD(construct.Tunnel):
def __init__(self, subcon, level: int = 3):
Expand Down Expand Up @@ -47,4 +49,46 @@ def _build(self, obj: dict[str, int], stream, context, path):
return self._build_construct._build(list(obj.items()), stream, context, path)


class VersionedHashesDict(construct.Construct):
def __init__(self):
super().__init__()
self._build_construct = construct.PrefixedArray(
construct.Int32un,
construct.Sequence(
construct.PascalString(construct.Int16un, "ascii"), # key
construct.Int64un, # hash
construct.Int16un, # versions
),
)

def _parse(self, stream, context, path) -> dict[str, int]:
key_struct = struct.Struct("=H")
value_struct = struct.Struct("=QH")

count = construct.Int32un._parse(stream, None, "")

result = {}
for _ in range(count):
key = stream.read(key_struct.unpack(stream.read(2))[0]).decode()
value, versions = value_struct.unpack(stream.read(10))
result[key] = {"crc": value, "versions": versions}

return result

def _build(self, obj: dict[str, dict], stream, context, path):
ver_to_val = GameVersion.versions_for_game(context.target_game)
all_vers = sum([v.bitmask for v in ver_to_val.values()])
for a in obj.values():
vers = a.get("versions")
if vers is not None:
a["versions"] = sum([ver_to_val[v].bitmask for v in vers])
else:
a["versions"] = all_vers

return self._build_construct._build(
list([(k, v["crc"], v["versions"]) for k, v in obj.items()]), stream, context, path
)


KnownHashes = CompressedZSTD(HashesDict(), 15)
VersionedHashes = CompressedZSTD(VersionedHashesDict(), 15)
40 changes: 30 additions & 10 deletions src/mercury_engine_data_structures/dread_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import typing
from pathlib import Path

from mercury_engine_data_structures._dread_data_construct import KnownHashes
from mercury_engine_data_structures._dread_data_construct import KnownHashes, VersionedHashes
from mercury_engine_data_structures.game_check import Game, GameVersion

_root = Path(__file__).parent
DREAD_VERSIONS = GameVersion.versions_for_game(Game.DREAD)
ALL_VERSIONS_BITMASK = sum([v.bitmask for v in DREAD_VERSIONS.values()])


@functools.lru_cache
Expand All @@ -16,23 +19,38 @@ def get_raw_types() -> dict[str, typing.Any]:


@functools.lru_cache
def all_name_to_asset_id() -> dict[str, int]:
def all_name_to_asset_id_and_version() -> dict[str, dict[str, int]]:
bin_path = _root.joinpath("dread_resource_names.bin")
if bin_path.exists():
return dict(KnownHashes.parse_file(bin_path))
return dict(VersionedHashes.parse_file(bin_path))

path = _root.joinpath("dread_resource_names.json")
with path.open() as names_file:
return json.load(names_file)
data: dict[str, dict] = json.load(names_file)

for a in data.values():
vers = a.get("versions")
if vers is not None:
a["versions"] = sum([DREAD_VERSIONS[v].bitmask for v in vers])
else:
a["versions"] = ALL_VERSIONS_BITMASK

return data


@functools.lru_cache
def all_name_to_asset_id(ver: GameVersion | None = None) -> dict[str, int]:
bitmask = ver.bitmask if ver else ALL_VERSIONS_BITMASK
return {k: v["crc"] for k, v in all_name_to_asset_id_and_version().items() if v["versions"] & bitmask != 0}


@functools.lru_cache
def all_asset_id_to_name() -> dict[int, str]:
return {asset_id: name for name, asset_id in all_name_to_asset_id().items()}
def all_asset_id_to_name(ver: GameVersion | None = None) -> dict[int, str]:
return {asset_id: name for name, asset_id in all_name_to_asset_id(ver).items()}


def name_for_asset_id(asset_id: int) -> str | None:
return all_asset_id_to_name().get(asset_id)
def name_for_asset_id(asset_id: int, ver: GameVersion | None = None) -> str | None:
return all_asset_id_to_name(ver).get(asset_id)


@functools.lru_cache
Expand All @@ -53,11 +71,13 @@ def all_property_id_to_name() -> dict[int, str]:
return {asset_id: name for name, asset_id in names.items()}


def all_files_ending_with(ext: str, exclusions: list[str] | None = None) -> list[str]:
def all_files_ending_with(
ext: str, exclusions: list[str] = None, ver: GameVersion | None = None
) -> list[str]:
if not ext.startswith("."):
ext = "." + ext

if exclusions is None:
exclusions = []

return [name for name in all_name_to_asset_id().keys() if name.endswith(ext) and name not in exclusions]
return [name for name in all_name_to_asset_id(ver).keys() if name.endswith(ext) and name not in exclusions]
Loading

0 comments on commit fe75d40

Please sign in to comment.