Compare commits

...
Sign in to create a new pull request.

49 commits

Author SHA1 Message Date
8cb2c6b2cd
Merge pull request 'Fix using branch for version number' (#209) from issue/205 into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #209
2025-03-12 12:25:49 +00:00
6ba13ac1ec
Fix using branch for version number 2025-03-12 13:24:03 +01:00
4d0762c0ba
Merge pull request 'Fix branch name in package version' (#207) from issue/205 into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #207
2025-03-12 12:15:30 +00:00
752291fe2d
Merge pull request 'Explicitly define base branches' (#208) from feat/renovate into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #208
2025-03-12 12:15:09 +00:00
71f945a96c
Explicitly define base branches
Currently, the dependency dashboard lists a bunch of pending updates
under a section called "Other branches". I'm not sure, but this sounds
like one of the configs I extend from enables base branches other than
only the default. To test, and make it explicit, set only the branches
I really want checked.

I'm adding the `release/.*` regex for now, even though I don't have
any release process yet.
2025-03-12 13:11:01 +01:00
d15f533e19
Fix branch name in package version
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
2025-03-12 11:52:53 +01:00
1a3c564ecf
Merge pull request 'Improve CI artifact version names' (#206) from issue/205 into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #206
2025-03-12 10:46:26 +00:00
beba47f340
Push a packaged with a fixed version for master
All checks were successful
build/msvc Build for the target platform: msvc
lint/clippy Checking for common mistakes and opportunities for code improvement
build/linux Build for the target platform: linux
To provide something that can easily be linked to, also push packages
built from `master` to a version that doesn't contain the SHA.
2025-03-12 11:33:48 +01:00
5612e271fb
Improve version name for CI artifacts built off master
The name from `git describe --tags` is rather confusing to people that
aren't familiar with it. Especially in the current situation, where
there are no proper versioned releases.

A name like `master-123456` should be much clearer.

Closes #205.
2025-03-12 11:26:24 +01:00
69300e87e6
Merge pull request 'fix(deps): update rust crate thiserror to v2' (#200) from renovate/thiserror-2.x into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #200
2025-02-19 10:08:51 +00:00
a3583b4485
fix(deps): update rust crate thiserror to v2
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
2025-02-19 11:02:02 +01:00
5982a66033
Merge pull request 'chore(deps): update rust crate notify to v8' (#202) from renovate/notify-8.x into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #202
2025-02-19 09:53:28 +00:00
adf9610ecc
chore(deps): update rust crate notify to v8
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
2025-01-10 14:48:31 +00:00
91cd54fff7
Merge pull request 'chore(deps): update rust crate bindgen to 0.71.0' (#201) from renovate/bindgen-0.x into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #201
2024-12-10 10:05:20 +00:00
b219e20f3a
chore(deps): update rust crate bindgen to 0.71.0
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
2024-12-06 20:32:37 +00:00
f9ccdf746e
Merge pull request 'chore(deps): update rust crate notify to v7' (#199) from renovate/notify-7.x into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #199
2024-10-28 09:24:47 +00:00
72ce06b0e5
chore(deps): update rust crate notify to v7
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
2024-10-25 17:32:26 +00:00
fc151f1449
Merge pull request 'fix(deps): update rust crate serde to v1.0.209' (#194) from renovate/serde-monorepo into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #194
2024-08-27 07:07:26 +00:00
659b63bfe9
fix(deps): update rust crate serde to v1.0.209
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
2024-08-27 06:30:39 +00:00
9f90b45275
Merge pull request 'chore(deps): update rust crate minijinja to v2.2.0' (#195) from renovate/minijinja-2.x-lockfile into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #195
2024-08-27 06:20:36 +00:00
67c64bb357
chore(deps): update rust crate minijinja to v2.2.0
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
2024-08-26 20:45:40 +00:00
6017ec058b
Merge pull request 'chore(deps): update rust crate fastrand to v2.1.1' (#193) from renovate/fastrand-2.x-lockfile into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: https://git.sclu1034.dev///bitsquid_dt/dtmt/pulls/193
2024-08-24 14:09:14 +00:00
ffd4927d27
chore(deps): update rust crate fastrand to v2.1.1
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/linux Build for the target platform: linux
build/msvc Build for the target platform: msvc
2024-08-24 10:02:43 +00:00
49a9eb4312
Merge pull request 'fix(deps): update rust crate serde_json to v1.0.127' (#192) from renovate/serde_json-1.x-lockfile into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: https://git.sclu1034.dev///bitsquid_dt/dtmt/pulls/192
2024-08-24 09:48:42 +00:00
4d665200fa
fix(deps): update rust crate serde_json to v1.0.127
All checks were successful
build/msvc Build for the target platform: msvc
lint/clippy Checking for common mistakes and opportunities for code improvement
build/linux Build for the target platform: linux
2024-08-23 21:30:32 +00:00
b3463ffb46
Merge pull request 'chore: Configure Renovate' (#187) from renovate/configure into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: https://git.sclu1034.dev///bitsquid_dt/dtmt/pulls/187
2024-08-21 13:31:35 +00:00
7cb44532b2
Add .renovaterc
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
2024-08-21 15:28:24 +02:00
15aa9bcf5e
Merge pull request 'Update dependencies' (#188) from feat/dependencies into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: https://git.sclu1034.dev///bitsquid_dt/dtmt/pulls/188
2024-08-21 13:28:11 +00:00
a2bbab1398
Update dependencies
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
2024-08-21 14:33:39 +02:00
88becb72a9
Merge pull request 'Consilidate template libraries' (#186) from issue/124 into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: https://git.sclu1034.dev///bitsquid_dt/dtmt/pulls/186
2024-08-21 09:49:18 +00:00
df2992a476
Improve mod template comments
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/linux Build for the target platform: linux
build/msvc Build for the target platform: msvc
2024-08-20 16:28:56 +02:00
e336240094
Consilidate template libraries
Remove last uses of `string_template` in favor of `minijinja`.

Closes #124.
2024-08-20 16:28:08 +02:00
d7fa80f471
Merge pull request 'Add tests for hash inversion' (#185) from feat/murmur-tests into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: https://git.sclu1034.dev///bitsquid_dt/dtmt/pulls/185
2024-08-14 12:07:31 +00:00
831592edf6
Merge pull request 'Implement bundle database resource hashes' (#184) from feat/bundle-database into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #184
2024-08-14 13:58:38 +02:00
2a1d8d815f
Add tests for hash inversion
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/linux Build for the target platform: linux
build/msvc Build for the target platform: msvc
Just a quick round trip test, and an additional assert to demonstrate
that byte order does matter.
2024-08-14 09:22:24 +02:00
d931e6b9ca
dtmt: Add command to search for files
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/linux Build for the target platform: linux
build/msvc Build for the target platform: msvc
Not really of much use at the moment, but inspired by the HD2 community.
2024-07-28 22:04:14 +02:00
7fa08c2efd
dtmt: Implement listing bundle database contents 2024-07-28 22:03:43 +02:00
dbf060032b
sdk: Implement bundle database resource hashes
Algorithm reverse engineered by WhiteGoat.
2024-07-28 14:46:10 +02:00
4b39d290b6 Merge pull request 'Add IdString32' (#183) from feat/various into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #183
2024-07-19 11:41:43 +02:00
3a6e954f9a
sdk: Refactor murmur modules and add IdString32
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/linux Build for the target platform: linux
build/msvc Build for the target platform: msvc
2024-07-19 11:30:09 +02:00
5a880b2953 Merge pull request 'Various minor changes extracted from unfinished projects' (#182) from feat/various into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #182
2024-07-19 11:13:47 +02:00
f1f9a818cc
sdk: Allow any byte stream for hashing dictionary entries
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/linux Build for the target platform: linux
build/msvc Build for the target platform: msvc
2024-07-19 09:48:25 +02:00
c997489e18
Add some doc comments 2024-07-19 09:48:23 +02:00
08219f05ba
sdk: Fix reading strings
Fatshark has a few weird string fields, where they provide a length
field, but then sometimes write a shorter, NUL-terminated string into
that same field and adding padding up to the "advertised" length.
To properly read those strings, we can't rely on just the length field
anymore, but need to check for a NUL, too.
2024-07-19 09:48:21 +02:00
edad0d4493
Improve file listing output
Adds pretty printing for file size and always shows the bundle hash name
2024-07-19 09:48:20 +02:00
74a7aaa6e5
dtmt-shared: Write log lines to stderr
Ideally, I would prefer the usual split per logging level, but that
seems to be somewhat complex with `tracing_subscriber`, so this simply
switches everything over to stderr, so that some of the experiment
commands can write results to stdout.
2024-07-19 09:48:15 +02:00
437e724d07 Merge pull request 'Implement name overrides' (#181) from feat/name-overrides into master
All checks were successful
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
Reviewed-on: #181
2024-07-19 09:35:16 +02:00
95fc6c160b
dtmt: Implement name overrides
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
For most of the game files, we don't know the actual name, only the hash
of that name. To still allow building bundles that contain files with
that name (e.g. to override a game file with a custom one), there needs
to be a way to tell DTMT to name a file such that its hash is the same
as the one in the game.
The initial idea was to just expect the file name on disk to be the
hash, but that wouldn't allow for arbitrary folder structures anymore.

So instead, there is now a new, optional setting in `dtmt.cfg`, where
the modder can map a file path to an override name.
2024-07-18 09:50:48 +02:00
b7e26eee57
refactor(sdk): Split BundleFileType into its own file 2024-07-17 11:14:22 +02:00
41 changed files with 1958 additions and 1226 deletions

View file

@ -125,8 +125,6 @@ jobs:
vars: vars:
pr: "" pr: ""
target: msvc target: msvc
gitea_url: http://forgejo:3000
gitea_api_key: ((gitea_api_key))
- load_var: version_number - load_var: version_number
reveal: true reveal: true
@ -142,10 +140,21 @@ jobs:
fail_fast: true fail_fast: true
override: true override: true
globs: globs:
- artifact/dtmt
- artifact/dtmm
- artifact/*.exe - artifact/*.exe
- artifact/*.sha256 - artifact/*.exe.sha256
- put: package
resource: gitea-package
no_get: true
inputs:
- artifact
params:
version: master
fail_fast: true
override: true
globs:
- artifact/*.exe
- artifact/*.exe.sha256
- name: build-linux - name: build-linux
on_success: on_success:
@ -202,5 +211,20 @@ jobs:
globs: globs:
- artifact/dtmt - artifact/dtmt
- artifact/dtmm - artifact/dtmm
- artifact/*.exe - artifact/dtmm.sha256
- artifact/*.sha256 - artifact/dtmt.sha256
- put: package
resource: gitea-package
no_get: true
inputs:
- artifact
params:
version: master
fail_fast: true
override: true
globs:
- artifact/dtmt
- artifact/dtmm
- artifact/dtmm.sha256
- artifact/dtmt.sha256

View file

@ -24,8 +24,10 @@ PR=${PR:-}
if [ -n "$PR" ]; then if [ -n "$PR" ]; then
title "PR: $(echo "$PR" | jq '.number') - $(echo "$PR" | jq '.title')" title "PR: $(echo "$PR" | jq '.number') - $(echo "$PR" | jq '.title')"
ref="pr-$(echo "$PR" | jq '.number')-$(git rev-parse --short "$(cat .git/ref || echo "HEAD")" 2>/dev/null || echo 'manual')" ref="pr-$(echo "$PR" | jq '.number')-$(git rev-parse --short "$(cat .git/ref || echo "HEAD")" 2>/dev/null || echo 'manual')"
elif [ -f ".git/branch"]; then
ref=$(cat .git/branch)-$(git rev-parse --short $ref)
else else
ref=$(git describe --tags) ref=$(git rev-parse --short "$(cat .git/ref || echo "HEAD")")
fi fi
title "Version: '$ref'" title "Version: '$ref'"

View file

@ -22,7 +22,6 @@ caches:
params: params:
CI: "true" CI: "true"
TARGET: ((target)) TARGET: ((target))
GITEA_API_KEY: ((gitea_api_key))
PR: ((pr)) PR: ((pr))
OUTPUT: artifact OUTPUT: artifact

15
.renovaterc Normal file
View file

@ -0,0 +1,15 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":combinePatchMinorReleases",
":enableVulnerabilityAlerts",
":rebaseStalePrs"
],
"prConcurrentLimit": 10,
"branchPrefix": "renovate/",
"baseBranches": [
"$default",
"/^release\\/.*/"
]
}

View file

@ -20,6 +20,8 @@
- dtmm: fetch file version for Nexus mods - dtmm: fetch file version for Nexus mods
- dtmm: handle `nxm://` URIs via IPC and import the corresponding mod - dtmm: handle `nxm://` URIs via IPC and import the corresponding mod
- dtmm: Add button to open mod on nexusmods.com - dtmm: Add button to open mod on nexusmods.com
- dtmt: Implement commands to list bundles and contents
- dtmt: Implement command to search for files
=== Fixed === Fixed

987
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -13,11 +13,51 @@ members = [
exclude = ["lib/color-eyre"] exclude = ["lib/color-eyre"]
[workspace.dependencies] [workspace.dependencies]
zip = { version = "2.1.3", default-features = false, features = ["deflate", "bzip2", "zstd", "time"] } ansi-parser = "0.9.1"
ansi_term = "0.12.1"
[patch.crates-io] async-recursion = "1.0.5"
bincode = "1.3.3"
bitflags = "2.5.0"
byteorder = "1.4.3"
clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "string", "unicode"] }
cli-table = { version = "0.4.7", default-features = false, features = ["derive"] }
color-eyre = { path = "lib/color-eyre" } color-eyre = { path = "lib/color-eyre" }
ansi-parser = { git = "https://gitlab.com/lschwiderski/ansi-parser.git", branch = "issue/outdated-heapless", version = "0.9.1" } colors-transform = "0.2.11"
confy = "0.6.1"
csv-async = { version = "1.2.4", features = ["tokio", "serde"] }
druid = { version = "0.8", features = ["im", "serde", "image", "png", "jpeg", "bmp", "webp", "svg"] }
druid-widget-nursery = "0.1"
dtmt-shared = { path = "lib/dtmt-shared" }
fastrand = "2.1.0"
futures = "0.3.25"
futures-util = "0.3.24"
glob = "0.3.0"
interprocess = "2.1.0"
lazy_static = "1.4.0"
luajit2-sys = { path = "lib/luajit2-sys" }
minijinja = { version = "2.0.1", default-features = false }
nanorand = "0.7.0"
nexusmods = { path = "lib/nexusmods" }
notify = "8.0.0"
oodle = { path = "lib/oodle" }
open = "5.0.1"
path-clean = "1.0.1"
path-slash = "0.2.1"
pin-project-lite = "0.2.9"
promptly = "0.3.1"
sdk = { path = "lib/sdk" }
serde = { version = "1.0.152", features = ["derive", "rc"] }
serde_sjson = { path = "lib/serde_sjson" }
steamlocate = "2.0.0-beta.2"
strip-ansi-escapes = "0.2.0"
time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-offset", "formatting", "macros"] }
tokio = { version = "1.23.0", features = ["rt-multi-thread", "fs", "process", "macros", "tracing", "io-util", "io-std"] }
tokio-stream = { version = "0.1.12", features = ["fs", "io-util"] }
tracing = { version = "0.1.37", features = ["async-await"] }
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
usvg = "0.25.0"
zip = { version = "2.1.3", default-features = false, features = ["deflate", "bzip2", "zstd", "time"] }
[profile.dev.package.backtrace] [profile.dev.package.backtrace]
opt-level = 3 opt-level = 3

View file

@ -12,37 +12,37 @@ license-file = "LICENSE"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
ansi-parser = "0.9.0" ansi-parser = { workspace = true }
async-recursion = "1.0.5" async-recursion = { workspace = true }
bincode = "1.3.3" bincode = { workspace = true }
bitflags = "2.5.0" bitflags = { workspace = true }
clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "string", "unicode"] } clap = { workspace = true }
color-eyre = "0.6.2" color-eyre = { workspace = true }
colors-transform = "0.2.11" colors-transform = { workspace = true }
confy = "0.6.1" confy = { workspace = true }
druid = { version = "0.8", features = ["im", "serde", "image", "png", "jpeg", "bmp", "webp", "svg"] } druid = { workspace = true }
druid-widget-nursery = "0.1" druid-widget-nursery = { workspace = true }
dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" } dtmt-shared = { workspace = true }
futures = "0.3.25" futures = { workspace = true }
interprocess = "2.1.0" interprocess = { workspace = true }
lazy_static = "1.4.0" lazy_static = { workspace = true }
luajit2-sys = { path = "../../lib/luajit2-sys", version = "*" } luajit2-sys = { workspace = true }
minijinja = { version = "2.0.1", default-features = false } minijinja = { workspace = true }
nexusmods = { path = "../../lib/nexusmods", version = "*" } nexusmods = { workspace = true }
oodle = { path = "../../lib/oodle", version = "*" } oodle = { workspace = true }
open = "5.0.1" open = { workspace = true }
path-slash = "0.2.1" path-slash = { workspace = true }
sdk = { path = "../../lib/sdk", version = "*" } sdk = { workspace = true }
serde = { version = "1.0.152", features = ["derive", "rc"] } serde = { workspace = true }
serde_sjson = { path = "../../lib/serde_sjson", version = "*" } serde_sjson = { workspace = true }
strip-ansi-escapes = "0.2.0" strip-ansi-escapes = { workspace = true }
time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-offset"] } time = { workspace = true }
tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] } tokio = { workspace = true }
tokio-stream = { version = "0.1.12", features = ["fs"] } tokio-stream = { workspace = true }
tracing = "0.1.37" tracing = { workspace = true }
tracing-error = "0.2.0" tracing-error = { workspace = true }
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } tracing-subscriber = { workspace = true }
usvg = "0.25.0" usvg = { workspace = true }
zip = { workspace = true } zip = { workspace = true }
[build-dependencies] [build-dependencies]

View file

@ -324,11 +324,11 @@ async fn build_bundles(state: Arc<ActionState>) -> Result<Vec<Bundle>> {
let mut bundles = Vec::new(); let mut bundles = Vec::new();
let mut add_lua_asset = |name, data: &str| { let mut add_lua_asset = |name: &str, data: &str| {
let span = tracing::info_span!("Compiling Lua", name, data_len = data.len()); let span = tracing::info_span!("Compiling Lua", name, data_len = data.len());
let _enter = span.enter(); let _enter = span.enter();
let file = lua::compile(name, data).wrap_err("Failed to compile Lua")?; let file = lua::compile(name.to_string(), data).wrap_err("Failed to compile Lua")?;
mod_bundle.add_file(file); mod_bundle.add_file(file);
@ -517,8 +517,8 @@ async fn patch_boot_bundle(
.wrap_err("Failed to render template `mod_main.lua`")?; .wrap_err("Failed to render template `mod_main.lua`")?;
tracing::trace!("Main script rendered:\n===========\n{}\n=============", lua); tracing::trace!("Main script rendered:\n===========\n{}\n=============", lua);
let file = let file = lua::compile(MOD_BOOT_SCRIPT.to_string(), lua)
lua::compile(MOD_BOOT_SCRIPT, lua).wrap_err("Failed to compile mod main Lua file")?; .wrap_err("Failed to compile mod main Lua file")?;
boot_bundle.add_file(file); boot_bundle.add_file(file);
} }

View file

@ -297,6 +297,7 @@ fn extract_mod_config<R: Read + Seek>(archive: &mut ZipArchive<R>) -> Result<(Mo
packages: Vec::new(), packages: Vec::new(),
resources, resources,
depends: Vec::new(), depends: Vec::new(),
name_overrides: Default::default(),
}; };
Ok((cfg, root)) Ok((cfg, root))

View file

@ -4,34 +4,36 @@ version = "0.3.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "unicode"] } async-recursion = { workspace = true }
cli-table = { version = "0.4.7", default-features = false, features = ["derive"] } clap = { workspace = true }
color-eyre = "0.6.2" cli-table = { workspace = true }
confy = "0.6.1" color-eyre = { workspace = true }
csv-async = { version = "1.2.4", features = ["tokio", "serde"] } confy = { workspace = true }
dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" } csv-async = { workspace = true }
futures = "0.3.25" dtmt-shared = { workspace = true }
futures-util = "0.3.24" futures = { workspace = true }
glob = "0.3.0" futures-util = { workspace = true }
nanorand = "0.7.0" glob = { workspace = true }
oodle = { path = "../../lib/oodle", version = "*" } luajit2-sys = { workspace = true }
pin-project-lite = "0.2.9" minijinja = { workspace = true }
promptly = "0.3.1" nanorand = { workspace = true }
sdk = { path = "../../lib/sdk", version = "*" } notify = { workspace = true }
serde_sjson = { path = "../../lib/serde_sjson", version = "*" } oodle = { workspace = true }
serde = { version = "1.0.147", features = ["derive"] } path-clean = { workspace = true }
string_template = "0.2.1" path-slash = { workspace = true }
tokio-stream = { version = "0.1.11", features = ["fs", "io-util"] } pin-project-lite = { workspace = true }
tokio = { version = "1.21.2", features = ["rt-multi-thread", "fs", "process", "macros", "tracing", "io-util", "io-std"] } promptly = { workspace = true }
tracing-error = "0.2.0" sdk = { workspace = true }
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } serde = { workspace = true }
tracing = { version = "0.1.37", features = ["async-await"] } serde_sjson = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
tracing-error = { workspace = true }
tracing-subscriber = { workspace = true }
zip = { workspace = true } zip = { workspace = true }
path-clean = "1.0.1"
path-slash = "0.2.1" # Cannot be a workspace dependencies when it's optional
async-recursion = "1.0.2"
notify = "6.1.1"
luajit2-sys = { path = "../../lib/luajit2-sys", version = "*" }
shlex = { version = "1.2.0", optional = true } shlex = { version = "1.2.0", optional = true }
[dev-dependencies] [dev-dependencies]

View file

@ -55,6 +55,7 @@ pub(crate) fn command_definition() -> Command {
) )
} }
/// Try to find a `dtmt.cfg` in the given directory or traverse up the parents.
#[tracing::instrument] #[tracing::instrument]
async fn find_project_config(dir: Option<PathBuf>) -> Result<ModConfig> { async fn find_project_config(dir: Option<PathBuf>) -> Result<ModConfig> {
let (path, mut file) = if let Some(path) = dir { let (path, mut file) = if let Some(path) = dir {
@ -102,39 +103,44 @@ async fn find_project_config(dir: Option<PathBuf>) -> Result<ModConfig> {
Ok(cfg) Ok(cfg)
} }
/// Iterate over the paths in the given `Package` and
/// compile each file by its file type.
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn compile_package_files<P>(pkg: &Package, root: P) -> Result<Vec<BundleFile>> async fn compile_package_files(pkg: &Package, cfg: &ModConfig) -> Result<Vec<BundleFile>> {
where let root = Arc::new(&cfg.dir);
P: AsRef<Path> + std::fmt::Debug, let name_overrides = &cfg.name_overrides;
{
let root = Arc::new(root.as_ref());
let tasks = pkg let tasks = pkg
.iter() .iter()
.flat_map(|(file_type, paths)| { .flat_map(|(file_type, names)| {
paths.iter().map(|path| { names.iter().map(|name| {
( (
*file_type, *file_type,
path, name,
// Cloning the `Arc` here solves the issue that in the next `.map`, I need to // Cloning the `Arc` here solves the issue that in the next `.map`, I need to
// `move` the closure parameters, but can't `move` `root` before it was cloned. // `move` the closure parameters, but can't `move` `root` before it was cloned.
root.clone(), root.clone(),
) )
}) })
}) })
.map(|(file_type, path, root)| async move { .map(|(file_type, name, root)| async move {
let sjson = fs::read_to_string(&path).await?; let path = PathBuf::from(name);
let sjson = fs::read_to_string(&path)
let mut path = path.clone();
path.set_extension("");
BundleFile::from_sjson(
path.to_slash_lossy().to_string(),
file_type,
sjson,
root.as_ref(),
)
.await .await
.wrap_err_with(|| format!("Failed to read file '{}'", path.display()))?;
let name = path.with_extension("").to_slash_lossy().to_string();
let name = if let Some(new_name) = name_overrides.get(&name) {
let new_name = match u64::from_str_radix(new_name, 16) {
Ok(hash) => IdString64::from(hash),
Err(_) => IdString64::from(new_name.clone()),
};
tracing::info!("Overriding '{}' -> '{}'", name, new_name.display());
new_name
} else {
IdString64::from(name.clone())
};
BundleFile::from_sjson(name, file_type, sjson, root.as_ref()).await
}); });
let results = futures::stream::iter(tasks) let results = futures::stream::iter(tasks)
@ -145,13 +151,14 @@ where
results.into_iter().collect() results.into_iter().collect()
} }
/// Read a `.package` file, collect the referenced files
/// and compile all of them into a bundle.
#[tracing::instrument] #[tracing::instrument]
async fn build_package<P1, P2>(package: P1, root: P2) -> Result<Bundle> async fn build_package(
where cfg: &ModConfig,
P1: AsRef<Path> + std::fmt::Debug, package: impl AsRef<Path> + std::fmt::Debug,
P2: AsRef<Path> + std::fmt::Debug, ) -> Result<Bundle> {
{ let root = &cfg.dir;
let root = root.as_ref();
let package = package.as_ref(); let package = package.as_ref();
let mut path = root.join(package); let mut path = root.join(package);
@ -165,7 +172,7 @@ where
.await .await
.wrap_err_with(|| format!("Invalid package file {}", &pkg_name))?; .wrap_err_with(|| format!("Invalid package file {}", &pkg_name))?;
let files = compile_package_files(&pkg, root).await?; let files = compile_package_files(&pkg, cfg).await?;
let mut bundle = Bundle::new(pkg_name); let mut bundle = Bundle::new(pkg_name);
for file in files { for file in files {
bundle.add_file(file); bundle.add_file(file);
@ -174,6 +181,8 @@ where
Ok(bundle) Ok(bundle)
} }
/// Cleans the path of internal parent (`../`) or self (`./`) components,
/// and ensures that it is relative.
fn normalize_file_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> { fn normalize_file_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = path.as_ref(); let path = path.as_ref();
@ -254,14 +263,14 @@ pub(crate) async fn read_project_config(dir: Option<PathBuf>) -> Result<ModConfi
Ok(cfg) Ok(cfg)
} }
pub(crate) async fn build<P1, P2>( #[tracing::instrument]
pub(crate) async fn build<P>(
cfg: &ModConfig, cfg: &ModConfig,
out_path: P1, out_path: impl AsRef<Path> + std::fmt::Debug,
game_dir: Arc<Option<P2>>, game_dir: Arc<Option<P>>,
) -> Result<()> ) -> Result<()>
where where
P1: AsRef<Path>, P: AsRef<Path> + std::fmt::Debug,
P2: AsRef<Path>,
{ {
let out_path = out_path.as_ref(); let out_path = out_path.as_ref();
@ -286,7 +295,7 @@ where
); );
} }
let bundle = build_package(path, &cfg.dir).await.wrap_err_with(|| { let bundle = build_package(&cfg, path).await.wrap_err_with(|| {
format!( format!(
"Failed to build package '{}' at '{}'", "Failed to build package '{}' at '{}'",
path.display(), path.display(),

View file

@ -0,0 +1,174 @@
use std::{io::Cursor, path::PathBuf};
use clap::{value_parser, Arg, ArgMatches, Command};
use color_eyre::{eyre::Context as _, Result};
use sdk::murmur::{HashGroup, IdString64, Murmur64};
use sdk::{BundleDatabase, FromBinary as _};
use tokio::fs;
pub(crate) fn command_definition() -> Command {
Command::new("db")
.about("Various operations regarding `bundle_database.data`.")
.subcommand_required(true)
.subcommand(
Command::new("list-files")
.about("List bundle contents")
.arg(
Arg::new("database")
.required(true)
.help("Path to the bundle database")
.value_parser(value_parser!(PathBuf)),
)
.arg(
Arg::new("bundle")
.help("The bundle name. If omitted, all bundles will be listed.")
.required(false),
),
)
.subcommand(
Command::new("list-bundles").about("List bundles").arg(
Arg::new("database")
.required(true)
.help("Path to the bundle database")
.value_parser(value_parser!(PathBuf)),
),
)
.subcommand(
Command::new("find-file")
.about("Find the bundle a file belongs to")
.arg(
Arg::new("database")
.required(true)
.help("Path to the bundle database")
.value_parser(value_parser!(PathBuf)),
)
.arg(
Arg::new("file-name")
.required(true)
.help("Name of the file. May be a hash in hex representation or a string"),
),
)
}
#[tracing::instrument(skip_all)]
pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> {
let Some((op, sub_matches)) = matches.subcommand() else {
unreachable!("clap is configured to require a subcommand");
};
let database = {
let path = sub_matches
.get_one::<PathBuf>("database")
.expect("argument is required");
let binary = fs::read(&path)
.await
.wrap_err_with(|| format!("Failed to read file '{}'", path.display()))?;
let mut r = Cursor::new(binary);
BundleDatabase::from_binary(&mut r).wrap_err("Failed to parse bundle database")?
};
match op {
"list-files" => {
let index = database.files();
if let Some(bundle) = sub_matches.get_one::<String>("bundle") {
let hash = u64::from_str_radix(bundle, 16)
.map(Murmur64::from)
.wrap_err("Invalid hex sequence")?;
if let Some(files) = index.get(&hash) {
for file in files {
let name = ctx.lookup_hash(file.name, HashGroup::Filename);
let extension = file.extension.ext_name();
println!("{}.{}", name.display(), extension);
}
} else {
tracing::info!("Bundle {} not found in the database", bundle);
}
} else {
for (bundle_hash, files) in index.iter() {
let bundle_name = ctx.lookup_hash(*bundle_hash, HashGroup::Filename);
match bundle_name {
IdString64::String(name) => {
println!("{:016x} {}", bundle_hash, name);
}
IdString64::Hash(hash) => {
println!("{:016x}", hash);
}
}
for file in files {
let name = ctx.lookup_hash(file.name, HashGroup::Filename);
let extension = file.extension.ext_name();
match name {
IdString64::String(name) => {
println!("\t{:016x}.{:<12} {}", file.name, extension, name);
}
IdString64::Hash(hash) => {
println!("\t{:016x}.{}", hash, extension);
}
}
}
println!();
}
}
Ok(())
}
"list-bundles" => {
for bundle_hash in database.bundles().keys() {
let bundle_name = ctx.lookup_hash(*bundle_hash, HashGroup::Filename);
match bundle_name {
IdString64::String(name) => {
println!("{:016x} {}", bundle_hash, name);
}
IdString64::Hash(hash) => {
println!("{:016x}", hash);
}
}
}
Ok(())
}
"find-file" => {
let name = sub_matches
.get_one::<String>("file-name")
.expect("required argument");
let name = match u64::from_str_radix(name, 16).map(Murmur64::from) {
Ok(hash) => hash,
Err(_) => Murmur64::hash(name),
};
let bundles = database.files().iter().filter_map(|(bundle_hash, files)| {
if files.iter().any(|file| file.name == name) {
Some(bundle_hash)
} else {
None
}
});
let mut found = false;
for bundle in bundles {
found = true;
println!("{:016x}", bundle);
}
if !found {
std::process::exit(1);
}
Ok(())
}
_ => unreachable!(
"clap is configured to require a subcommand, and they're all handled above"
),
}
}

View file

@ -36,6 +36,18 @@ enum OutputFormat {
Text, Text,
} }
fn format_byte_size(size: usize) -> String {
if size < 1024 {
format!("{} Bytes", size)
} else if size < 1024 * 1024 {
format!("{} kB", size / 1024)
} else if size < 1024 * 1024 * 1024 {
format!("{} MB", size / (1024 * 1024))
} else {
format!("{} GB", size / (1024 * 1024 * 1024))
}
}
#[tracing::instrument(skip(ctx))] #[tracing::instrument(skip(ctx))]
async fn print_bundle_contents<P>(ctx: &sdk::Context, path: P, fmt: OutputFormat) -> Result<()> async fn print_bundle_contents<P>(ctx: &sdk::Context, path: P, fmt: OutputFormat) -> Result<()>
where where
@ -50,7 +62,11 @@ where
match fmt { match fmt {
OutputFormat::Text => { OutputFormat::Text => {
println!("Bundle: {}", bundle.name().display()); println!(
"Bundle: {} ({:016x})",
bundle.name().display(),
bundle.name()
);
for f in bundle.files().iter() { for f in bundle.files().iter() {
if f.variants().len() != 1 { if f.variants().len() != 1 {
@ -63,9 +79,10 @@ where
let v = &f.variants()[0]; let v = &f.variants()[0];
println!( println!(
"\t{}.{}: {} bytes", "\t{}.{}: {} ({})",
f.base_name().display(), f.base_name().display(),
f.file_type().ext_name(), f.file_type().ext_name(),
format_byte_size(v.size()),
v.size() v.size()
); );
} }

View file

@ -1,6 +1,7 @@
use clap::{ArgMatches, Command}; use clap::{ArgMatches, Command};
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
mod db;
mod decompress; mod decompress;
mod extract; mod extract;
mod inject; mod inject;
@ -14,6 +15,7 @@ pub(crate) fn command_definition() -> Command {
.subcommand(extract::command_definition()) .subcommand(extract::command_definition())
.subcommand(inject::command_definition()) .subcommand(inject::command_definition())
.subcommand(list::command_definition()) .subcommand(list::command_definition())
.subcommand(db::command_definition())
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
@ -23,6 +25,7 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> {
Some(("extract", sub_matches)) => extract::run(ctx, sub_matches).await, Some(("extract", sub_matches)) => extract::run(ctx, sub_matches).await,
Some(("inject", sub_matches)) => inject::run(ctx, sub_matches).await, Some(("inject", sub_matches)) => inject::run(ctx, sub_matches).await,
Some(("list", sub_matches)) => list::run(ctx, sub_matches).await, Some(("list", sub_matches)) => list::run(ctx, sub_matches).await,
Some(("db", sub_matches)) => db::run(ctx, sub_matches).await,
_ => unreachable!( _ => unreachable!(
"clap is configured to require a subcommand, and they're all handled above" "clap is configured to require a subcommand, and they're all handled above"
), ),

View file

@ -351,6 +351,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
}, },
depends: vec![ModDependency::ID(String::from("DMF"))], depends: vec![ModDependency::ID(String::from("DMF"))],
bundled: true, bundled: true,
name_overrides: HashMap::new(),
}; };
tracing::debug!(?dtmt_cfg); tracing::debug!(?dtmt_cfg);

View file

@ -1,18 +1,30 @@
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use clap::{Arg, ArgMatches, Command}; use clap::{Arg, ArgMatches, Command};
use color_eyre::eyre::{self, Context, Result}; use color_eyre::eyre::{self, Context, Result};
use color_eyre::Help; use color_eyre::Help;
use futures::{StreamExt, TryStreamExt}; use futures::{StreamExt, TryStreamExt};
use string_template::Template; use minijinja::Environment;
use tokio::fs::{self, DirBuilder}; use tokio::fs::{self, DirBuilder};
const TEMPLATES: [(&str, &str); 5] = [ const TEMPLATES: [(&str, &str); 5] = [
( (
"dtmt.cfg", "dtmt.cfg",
r#"id = "{{id}}" r#"//
// This is your mod's main configuration file. It tells DTMT how to build the mod,
// and DTMM what to display to your users.
// Certain files have been pre-filled by the template, the ones commented out (`//`)
// are optional.
//
// A unique identifier (preferably lower case, alphanumeric)
id = "{{id}}"
// The display name that your users will see.
// This doesn't have to be unique, but you still want to avoid being confused with other
// mods.
name = "{{name}}" name = "{{name}}"
// It's good practice to increase this number whenever you publish changes.
// It's up to you if you use SemVer or something simpler like `1970-12-24`. It should sort and
// compare well, though.
version = "0.1.0" version = "0.1.0"
// author = "" // author = ""
@ -32,16 +44,25 @@ categories = [
// A list of mod IDs that this mod depends on. You can find // A list of mod IDs that this mod depends on. You can find
// those IDs by downloading the mod and extracting their `dtmt.cfg`. // those IDs by downloading the mod and extracting their `dtmt.cfg`.
// To make your fellow modders' lives easier, publish your own mods' IDs
// somewhere visible, such as the Nexusmods page.
depends = [ depends = [
DMF DMF
] ]
// The primary resources that serve as the entry point to your
// mod's code. Unless for very specific use cases, the generated
// values shouldn't be changed.
resources = { resources = {
init = "scripts/mods/{{id}}/init" init = "scripts/mods/{{id}}/init"
data = "scripts/mods/{{id}}/data" data = "scripts/mods/{{id}}/data"
localization = "scripts/mods/{{id}}/localization" localization = "scripts/mods/{{id}}/localization"
} }
// The list of packages, or bundles, to build.
// Each one corresponds to a package definition in the named folder.
// For mods that contain only code and/or a few small assets, a single
// package will suffice.
packages = [ packages = [
"packages/mods/{{id}}" "packages/mods/{{id}}"
] ]
@ -59,7 +80,6 @@ packages = [
r#"local mod = get_mod("{{id}}") r#"local mod = get_mod("{{id}}")
-- Your mod code goes here. -- Your mod code goes here.
-- https://vmf-docs.verminti.de
"#, "#,
), ),
( (
@ -137,19 +157,23 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
tracing::debug!(root = %root.display(), name, id); tracing::debug!(root = %root.display(), name, id);
let mut data = HashMap::new(); let render_ctx = minijinja::context!(name => name.as_str(), id => id.as_str());
data.insert("name", name.as_str()); let env = Environment::new();
data.insert("id", id.as_str());
let templates = TEMPLATES let templates = TEMPLATES
.iter() .iter()
.map(|(path_tmpl, content_tmpl)| { .map(|(path_tmpl, content_tmpl)| {
let path = Template::new(path_tmpl).render(&data); env.render_str(path_tmpl, &render_ctx)
let content = Template::new(content_tmpl).render(&data); .wrap_err_with(|| format!("Failed to render template: {}", path_tmpl))
.and_then(|path| {
(root.join(path), content) env.render_named_str(&path, content_tmpl, &render_ctx)
.wrap_err_with(|| format!("Failed to render template '{}'", &path))
.map(|content| (root.join(path), content))
}) })
.map(|(path, content)| async move { })
.map(|res| async move {
match res {
Ok((path, content)) => {
let dir = path let dir = path
.parent() .parent()
.ok_or_else(|| eyre::eyre!("invalid root path"))?; .ok_or_else(|| eyre::eyre!("invalid root path"))?;
@ -158,13 +182,20 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
.recursive(true) .recursive(true)
.create(&dir) .create(&dir)
.await .await
.wrap_err_with(|| format!("Failed to create directory {}", dir.display()))?; .wrap_err_with(|| {
format!("Failed to create directory {}", dir.display())
})?;
tracing::trace!("Writing file {}", path.display()); tracing::trace!("Writing file {}", path.display());
fs::write(&path, content.as_bytes()) fs::write(&path, content.as_bytes())
.await .await
.wrap_err_with(|| format!("Failed to write content to path {}", path.display())) .wrap_err_with(|| {
format!("Failed to write content to path {}", path.display())
})
}
Err(e) => Err(e),
}
}); });
futures::stream::iter(templates) futures::stream::iter(templates)

View file

@ -77,17 +77,14 @@ pub(crate) fn command_definition() -> Command {
) )
} }
async fn compile<P1, P2, P3>( #[tracing::instrument]
async fn compile(
cfg: &ModConfig, cfg: &ModConfig,
out_path: P1, out_path: impl AsRef<Path> + std::fmt::Debug,
archive_path: P2, archive_path: impl AsRef<Path> + std::fmt::Debug,
game_dir: Arc<Option<P3>>, game_dir: Arc<Option<impl AsRef<Path> + std::fmt::Debug>>,
) -> Result<()> ) -> Result<()> {
where let out_path = out_path.as_ref();
P1: AsRef<Path> + std::marker::Copy,
P2: AsRef<Path>,
P3: AsRef<Path>,
{
build(cfg, out_path, game_dir) build(cfg, out_path, game_dir)
.await .await
.wrap_err("Failed to build bundles")?; .wrap_err("Failed to build bundles")?;

@ -1 +1 @@
Subproject commit b40962a61c748756d7da293d9fff26aca019603e Subproject commit 228b8ca37ee79ab9afa45c40da415e4dcb029751

View file

@ -6,11 +6,11 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
ansi_term = "0.12.1" ansi_term = { workspace = true }
color-eyre = "0.6.2" color-eyre = { workspace = true }
serde = "1.0.152" serde = { workspace = true }
steamlocate = "2.0.0-beta.2" steamlocate = { workspace = true }
time = { version = "0.3.19", features = ["formatting", "local-offset", "macros"] } time = { workspace = true }
tracing = "0.1.37" tracing = { workspace = true }
tracing-error = "0.2.0" tracing-error = { workspace = true }
tracing-subscriber = "0.3.16" tracing-subscriber = { workspace = true }

View file

@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use color_eyre::eyre::{OptionExt as _, WrapErr as _}; use color_eyre::eyre::{OptionExt as _, WrapErr as _};
@ -67,6 +68,8 @@ pub struct ModConfig {
pub depends: Vec<ModDependency>, pub depends: Vec<ModDependency>,
#[serde(default = "default_true", skip_serializing_if = "is_true")] #[serde(default = "default_true", skip_serializing_if = "is_true")]
pub bundled: bool, pub bundled: bool,
#[serde(default)]
pub name_overrides: HashMap<String, String>,
} }
pub const STEAMAPP_ID: u32 = 1361210; pub const STEAMAPP_ID: u32 = 1361210;

View file

@ -84,7 +84,7 @@ pub fn create_tracing_subscriber() {
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) { let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) {
let fmt_layer = fmt::layer().pretty(); let fmt_layer = fmt::layer().pretty().with_writer(std::io::stderr);
(Some(fmt_layer), None, None) (Some(fmt_layer), None, None)
} else { } else {
// Creates a layer that // Creates a layer that
@ -93,6 +93,7 @@ pub fn create_tracing_subscriber() {
// - does not print spans/targets // - does not print spans/targets
// - only prints time, not date // - only prints time, not date
let fmt_layer = fmt::layer() let fmt_layer = fmt::layer()
.with_writer(std::io::stderr)
.event_format(Formatter) .event_format(Formatter)
.fmt_fields(debug_fn(format_fields)); .fmt_fields(debug_fn(format_fields));

@ -1 +1 @@
Subproject commit 5d1a075742395f767c79d9c0d7466c6fb442f106 Subproject commit 6d94a4dd2c296bf1f044ee4c70fb10dca4c1c241

View file

@ -12,7 +12,7 @@ regex = "1.7.1"
reqwest = { version = "0.12.4" } reqwest = { version = "0.12.4" }
serde = { version = "1.0.152", features = ["derive"] } serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.94" serde_json = "1.0.94"
thiserror = "1.0.39" thiserror = "2.0.0"
time = { version = "0.3.20", features = ["serde"] } time = { version = "0.3.20", features = ["serde"] }
tracing = "0.1.37" tracing = "0.1.37"
url = { version = "2.3.1", features = ["serde"] } url = { version = "2.3.1", features = ["serde"] }

View file

@ -28,7 +28,7 @@ pub enum Error {
HTTP(#[from] reqwest::Error), HTTP(#[from] reqwest::Error),
#[error("invalid URL: {0:?}")] #[error("invalid URL: {0:?}")]
URLParseError(#[from] url::ParseError), URLParseError(#[from] url::ParseError),
#[error("failed to deserialize '{error}': {json}")] #[error("failed to deserialize due to {error}: {json}")]
Deserialize { Deserialize {
json: String, json: String,
error: serde_json::Error, error: serde_json::Error,
@ -37,7 +37,7 @@ pub enum Error {
InvalidHeaderValue(#[from] InvalidHeaderValue), InvalidHeaderValue(#[from] InvalidHeaderValue),
#[error("this error cannot happen")] #[error("this error cannot happen")]
Infallible(#[from] Infallible), Infallible(#[from] Infallible),
#[error("invalid NXM URL '{}': {0}", .1.as_str())] #[error("invalid NXM URL '{url}': {0}", url = .1.as_str())]
InvalidNXM(&'static str, Url), InvalidNXM(&'static str, Url),
#[error("{0}")] #[error("{0}")]
Custom(String), Custom(String),

View file

@ -6,8 +6,8 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
color-eyre = "0.6.2" color-eyre = { workspace = true }
tracing = "0.1.37" tracing = { workspace = true }
[build-dependencies] [build-dependencies]
bindgen = "0.69.4" bindgen = "0.71.0"

View file

@ -7,6 +7,7 @@ use std::ptr;
use color_eyre::{eyre, Result}; use color_eyre::{eyre, Result};
#[allow(dead_code)] #[allow(dead_code)]
#[allow(clippy::identity_op)]
mod bindings { mod bindings {
include!(concat!(env!("OUT_DIR"), "/bindings.rs")); include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
} }

View file

@ -4,23 +4,23 @@ version = "0.3.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
bitflags = "2.5.0" async-recursion = { workspace = true }
byteorder = "1.4.3" bitflags = { workspace = true }
color-eyre = "0.6.2" byteorder = { workspace = true }
csv-async = { version = "1.2.4", features = ["tokio", "serde"] } color-eyre = { workspace = true }
fastrand = "2.1.0" csv-async = { workspace = true }
futures = "0.3.25" fastrand = { workspace = true }
futures-util = "0.3.24" futures = { workspace = true }
glob = "0.3.0" futures-util = { workspace = true }
nanorand = "0.7.0" glob = { workspace = true }
pin-project-lite = "0.2.9" luajit2-sys = { workspace = true }
serde = { version = "1.0.147", features = ["derive"] } nanorand = { workspace = true }
serde_sjson = { path = "../../lib/serde_sjson", version = "*" } oodle = { workspace = true }
oodle = { path = "../../lib/oodle", version = "*" } path-slash = { workspace = true }
tokio = { version = "1.21.2", features = ["rt-multi-thread", "fs", "process", "macros", "tracing", "io-util", "io-std"] } pin-project-lite = { workspace = true }
tokio-stream = { version = "0.1.11", features = ["fs", "io-util"] } serde = { workspace = true }
tracing = { version = "0.1.37", features = ["async-await"] } serde_sjson = { workspace = true }
tracing-error = "0.2.0" tokio = { workspace = true }
luajit2-sys = { path = "../../lib/luajit2-sys", version = "*" } tokio-stream = { workspace = true }
async-recursion = "1.0.2" tracing = { workspace = true }
path-slash = "0.2.1" tracing-error = { workspace = true }

View file

@ -43,6 +43,7 @@ impl<T: FromBinary> FromBinary for Vec<T> {
} }
pub mod sync { pub mod sync {
use std::ffi::CStr;
use std::io::{self, Read, Seek, SeekFrom}; use std::io::{self, Read, Seek, SeekFrom};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
@ -165,25 +166,13 @@ pub mod sync {
} }
fn read_string_len(&mut self, len: usize) -> Result<String> { fn read_string_len(&mut self, len: usize) -> Result<String> {
let mut buf = vec![0; len]; let pos = self.stream_position();
let res = self
.read_exact(&mut buf)
.map_err(Report::new)
.and_then(|_| {
String::from_utf8(buf).map_err(|err| {
let ascii = String::from_utf8_lossy(err.as_bytes()).to_string();
let bytes = format!("{:?}", err.as_bytes());
Report::new(err)
.with_section(move || bytes.header("Bytes:"))
.with_section(move || ascii.header("ASCII:"))
})
});
let res = read_string_len(self, len);
if res.is_ok() { if res.is_ok() {
return res; return res;
} }
let pos = self.stream_position();
if pos.is_ok() { if pos.is_ok() {
res.with_section(|| { res.with_section(|| {
format!("{pos:#X} ({pos})", pos = pos.unwrap()).header("Position: ") format!("{pos:#X} ({pos})", pos = pos.unwrap()).header("Position: ")
@ -243,4 +232,22 @@ pub mod sync {
Err(err).with_section(|| format!("{pos:#X} ({pos})").header("Position: ")) Err(err).with_section(|| format!("{pos:#X} ({pos})").header("Position: "))
} }
fn read_string_len(mut r: impl Read, len: usize) -> Result<String> {
let mut buf = vec![0; len];
r.read_exact(&mut buf)
.wrap_err_with(|| format!("Failed to read {} bytes", len))?;
let res = match CStr::from_bytes_until_nul(&buf) {
Ok(s) => {
let s = s.to_str()?;
Ok(s.to_string())
}
Err(_) => String::from_utf8(buf.clone()).map_err(Report::new),
};
res.wrap_err("Invalid binary for UTF8 string")
.with_section(|| format!("{}", String::from_utf8_lossy(&buf)).header("ASCI:"))
.with_section(|| format!("{:x?}", buf).header("Bytes:"))
}
} }

View file

@ -13,21 +13,21 @@ use crate::binary::ToBinary;
use crate::murmur::Murmur64; use crate::murmur::Murmur64;
use crate::Bundle; use crate::Bundle;
use super::file::BundleFileType; use super::filetype::BundleFileType;
const DATABASE_VERSION: u32 = 0x6; const DATABASE_VERSION: u32 = 0x6;
const FILE_VERSION: u32 = 0x4; const FILE_VERSION: u32 = 0x4;
pub struct BundleFile { pub struct BundleFile {
name: String, pub name: String,
stream: String, pub stream: String,
platform_specific: bool, pub platform_specific: bool,
file_time: u64, pub file_time: u64,
} }
pub struct FileName { pub struct FileName {
extension: BundleFileType, pub extension: BundleFileType,
name: Murmur64, pub name: Murmur64,
} }
pub struct BundleDatabase { pub struct BundleDatabase {
@ -36,7 +36,34 @@ pub struct BundleDatabase {
bundle_contents: HashMap<Murmur64, Vec<FileName>>, bundle_contents: HashMap<Murmur64, Vec<FileName>>,
} }
// Implements the partial Murmur that's used by the engine to compute bundle resource hashes,
// but in a way that the loop can be done outside the function.
#[inline(always)]
fn add_to_resource_hash(mut k: u64, name: impl Into<u64>) -> u64 {
const M: u64 = 0xc6a4a7935bd1e995;
const R: u64 = 47;
let mut h: u64 = name.into();
k = k.wrapping_mul(M);
k ^= k >> R;
k = k.wrapping_mul(M);
h ^= k;
k = M.wrapping_mul(h);
k
}
impl BundleDatabase { impl BundleDatabase {
pub fn bundles(&self) -> &HashMap<Murmur64, Vec<BundleFile>> {
&self.stored_files
}
pub fn files(&self) -> &HashMap<Murmur64, Vec<FileName>> {
&self.bundle_contents
}
pub fn add_bundle(&mut self, bundle: &Bundle) { pub fn add_bundle(&mut self, bundle: &Bundle) {
let hash = bundle.name().to_murmur64(); let hash = bundle.name().to_murmur64();
let name = hash.to_string(); let name = hash.to_string();
@ -69,20 +96,26 @@ impl BundleDatabase {
} }
} }
let mut resource_hash = 0;
for f in bundle.files() { for f in bundle.files() {
let name = f.base_name().to_murmur64();
let file_name = FileName { let file_name = FileName {
extension: f.file_type(), extension: f.file_type(),
name: f.base_name().to_murmur64(), name,
}; };
// TODO: Compute actual resource hash resource_hash = add_to_resource_hash(resource_hash, name);
self.resource_hashes.insert(hash, 0);
// TODO: Make sure each file name only exists once. Probably best to turn
// the `Vec` into a sorted `HashSet`.
self.bundle_contents self.bundle_contents
.entry(hash) .entry(hash)
.or_default() .or_default()
.push(file_name); .push(file_name);
} }
self.resource_hashes.insert(hash, resource_hash);
} }
} }
@ -103,7 +136,7 @@ impl FromBinary for BundleDatabase {
let mut stored_files = HashMap::with_capacity(num_entries); let mut stored_files = HashMap::with_capacity(num_entries);
for _ in 0..num_entries { for _ in 0..num_entries {
let hash = Murmur64::from(r.read_u64()?); let hash = r.read_u64().map(Murmur64::from)?;
let num_files = r.read_u32()? as usize; let num_files = r.read_u32()? as usize;
let mut files = Vec::with_capacity(num_files); let mut files = Vec::with_capacity(num_files);
@ -161,7 +194,7 @@ impl FromBinary for BundleDatabase {
let mut resource_hashes = HashMap::with_capacity(num_hashes); let mut resource_hashes = HashMap::with_capacity(num_hashes);
for _ in 0..num_hashes { for _ in 0..num_hashes {
let name = Murmur64::from(r.read_u64()?); let name = r.read_u64().map(Murmur64::from)?;
let hash = r.read_u64()?; let hash = r.read_u64()?;
resource_hashes.insert(name, hash); resource_hashes.insert(name, hash);
@ -171,14 +204,14 @@ impl FromBinary for BundleDatabase {
let mut bundle_contents = HashMap::with_capacity(num_contents); let mut bundle_contents = HashMap::with_capacity(num_contents);
for _ in 0..num_contents { for _ in 0..num_contents {
let hash = Murmur64::from(r.read_u64()?); let hash = r.read_u64().map(Murmur64::from)?;
let num_files = r.read_u32()? as usize; let num_files = r.read_u32()? as usize;
let mut files = Vec::with_capacity(num_files); let mut files = Vec::with_capacity(num_files);
for _ in 0..num_files { for _ in 0..num_files {
let extension = BundleFileType::from(r.read_u64()?); let extension = r.read_u64().map(BundleFileType::from)?;
let name = Murmur64::from(r.read_u64()?); let name = r.read_u64().map(Murmur64::from)?;
files.push(FileName { extension, name }); files.push(FileName { extension, name });
} }

View file

@ -5,407 +5,12 @@ use bitflags::bitflags;
use color_eyre::eyre::Context; use color_eyre::eyre::Context;
use color_eyre::{eyre, Result}; use color_eyre::{eyre, Result};
use futures::future::join_all; use futures::future::join_all;
use serde::Serialize;
use crate::binary::sync::*; use crate::binary::sync::*;
use crate::filetype::*; use crate::filetype::*;
use crate::murmur::{HashGroup, IdString64, Murmur64}; use crate::murmur::{HashGroup, IdString64, Murmur64};
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] use super::filetype::BundleFileType;
pub enum BundleFileType {
Animation,
AnimationCurves,
Apb,
BakedLighting,
Bik,
BlendSet,
Bones,
Chroma,
CommonPackage,
Config,
Crypto,
Data,
Entity,
Flow,
Font,
Ies,
Ini,
Input,
Ivf,
Keys,
Level,
Lua,
Material,
Mod,
MouseCursor,
NavData,
NetworkConfig,
OddleNet,
Package,
Particles,
PhysicsProperties,
RenderConfig,
RtPipeline,
Scene,
Shader,
ShaderLibrary,
ShaderLibraryGroup,
ShadingEnvionmentMapping,
ShadingEnvironment,
Slug,
SlugAlbum,
SoundEnvironment,
SpuJob,
StateMachine,
StaticPVS,
Strings,
SurfaceProperties,
Texture,
TimpaniBank,
TimpaniMaster,
Tome,
Ugg,
Unit,
Upb,
VectorField,
Wav,
WwiseBank,
WwiseDep,
WwiseEvent,
WwiseMetadata,
WwiseStream,
Xml,
Unknown(Murmur64),
}
impl BundleFileType {
pub fn ext_name(&self) -> String {
match self {
BundleFileType::AnimationCurves => String::from("animation_curves"),
BundleFileType::Animation => String::from("animation"),
BundleFileType::Apb => String::from("apb"),
BundleFileType::BakedLighting => String::from("baked_lighting"),
BundleFileType::Bik => String::from("bik"),
BundleFileType::BlendSet => String::from("blend_set"),
BundleFileType::Bones => String::from("bones"),
BundleFileType::Chroma => String::from("chroma"),
BundleFileType::CommonPackage => String::from("common_package"),
BundleFileType::Config => String::from("config"),
BundleFileType::Crypto => String::from("crypto"),
BundleFileType::Data => String::from("data"),
BundleFileType::Entity => String::from("entity"),
BundleFileType::Flow => String::from("flow"),
BundleFileType::Font => String::from("font"),
BundleFileType::Ies => String::from("ies"),
BundleFileType::Ini => String::from("ini"),
BundleFileType::Input => String::from("input"),
BundleFileType::Ivf => String::from("ivf"),
BundleFileType::Keys => String::from("keys"),
BundleFileType::Level => String::from("level"),
BundleFileType::Lua => String::from("lua"),
BundleFileType::Material => String::from("material"),
BundleFileType::Mod => String::from("mod"),
BundleFileType::MouseCursor => String::from("mouse_cursor"),
BundleFileType::NavData => String::from("nav_data"),
BundleFileType::NetworkConfig => String::from("network_config"),
BundleFileType::OddleNet => String::from("oodle_net"),
BundleFileType::Package => String::from("package"),
BundleFileType::Particles => String::from("particles"),
BundleFileType::PhysicsProperties => String::from("physics_properties"),
BundleFileType::RenderConfig => String::from("render_config"),
BundleFileType::RtPipeline => String::from("rt_pipeline"),
BundleFileType::Scene => String::from("scene"),
BundleFileType::ShaderLibraryGroup => String::from("shader_library_group"),
BundleFileType::ShaderLibrary => String::from("shader_library"),
BundleFileType::Shader => String::from("shader"),
BundleFileType::ShadingEnvionmentMapping => String::from("shading_environment_mapping"),
BundleFileType::ShadingEnvironment => String::from("shading_environment"),
BundleFileType::SlugAlbum => String::from("slug_album"),
BundleFileType::Slug => String::from("slug"),
BundleFileType::SoundEnvironment => String::from("sound_environment"),
BundleFileType::SpuJob => String::from("spu_job"),
BundleFileType::StateMachine => String::from("state_machine"),
BundleFileType::StaticPVS => String::from("static_pvs"),
BundleFileType::Strings => String::from("strings"),
BundleFileType::SurfaceProperties => String::from("surface_properties"),
BundleFileType::Texture => String::from("texture"),
BundleFileType::TimpaniBank => String::from("timpani_bank"),
BundleFileType::TimpaniMaster => String::from("timpani_master"),
BundleFileType::Tome => String::from("tome"),
BundleFileType::Ugg => String::from("ugg"),
BundleFileType::Unit => String::from("unit"),
BundleFileType::Upb => String::from("upb"),
BundleFileType::VectorField => String::from("vector_field"),
BundleFileType::Wav => String::from("wav"),
BundleFileType::WwiseBank => String::from("wwise_bank"),
BundleFileType::WwiseDep => String::from("wwise_dep"),
BundleFileType::WwiseEvent => String::from("wwise_event"),
BundleFileType::WwiseMetadata => String::from("wwise_metadata"),
BundleFileType::WwiseStream => String::from("wwise_stream"),
BundleFileType::Xml => String::from("xml"),
BundleFileType::Unknown(s) => format!("{s:016X}"),
}
}
pub fn decompiled_ext_name(&self) -> String {
match self {
BundleFileType::Texture => String::from("dds"),
BundleFileType::WwiseBank => String::from("bnk"),
BundleFileType::WwiseStream => String::from("ogg"),
_ => self.ext_name(),
}
}
pub fn hash(&self) -> Murmur64 {
Murmur64::from(*self)
}
}
impl std::str::FromStr for BundleFileType {
type Err = color_eyre::Report;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let val = match s {
"animation_curves" => BundleFileType::AnimationCurves,
"animation" => BundleFileType::Animation,
"apb" => BundleFileType::Apb,
"baked_lighting" => BundleFileType::BakedLighting,
"bik" => BundleFileType::Bik,
"blend_set" => BundleFileType::BlendSet,
"bones" => BundleFileType::Bones,
"chroma" => BundleFileType::Chroma,
"common_package" => BundleFileType::CommonPackage,
"config" => BundleFileType::Config,
"crypto" => BundleFileType::Crypto,
"data" => BundleFileType::Data,
"entity" => BundleFileType::Entity,
"flow" => BundleFileType::Flow,
"font" => BundleFileType::Font,
"ies" => BundleFileType::Ies,
"ini" => BundleFileType::Ini,
"input" => BundleFileType::Input,
"ivf" => BundleFileType::Ivf,
"keys" => BundleFileType::Keys,
"level" => BundleFileType::Level,
"lua" => BundleFileType::Lua,
"material" => BundleFileType::Material,
"mod" => BundleFileType::Mod,
"mouse_cursor" => BundleFileType::MouseCursor,
"nav_data" => BundleFileType::NavData,
"network_config" => BundleFileType::NetworkConfig,
"oodle_net" => BundleFileType::OddleNet,
"package" => BundleFileType::Package,
"particles" => BundleFileType::Particles,
"physics_properties" => BundleFileType::PhysicsProperties,
"render_config" => BundleFileType::RenderConfig,
"rt_pipeline" => BundleFileType::RtPipeline,
"scene" => BundleFileType::Scene,
"shader_library_group" => BundleFileType::ShaderLibraryGroup,
"shader_library" => BundleFileType::ShaderLibrary,
"shader" => BundleFileType::Shader,
"shading_environment_mapping" => BundleFileType::ShadingEnvionmentMapping,
"shading_environment" => BundleFileType::ShadingEnvironment,
"slug_album" => BundleFileType::SlugAlbum,
"slug" => BundleFileType::Slug,
"sound_environment" => BundleFileType::SoundEnvironment,
"spu_job" => BundleFileType::SpuJob,
"state_machine" => BundleFileType::StateMachine,
"static_pvs" => BundleFileType::StaticPVS,
"strings" => BundleFileType::Strings,
"surface_properties" => BundleFileType::SurfaceProperties,
"texture" => BundleFileType::Texture,
"timpani_bank" => BundleFileType::TimpaniBank,
"timpani_master" => BundleFileType::TimpaniMaster,
"tome" => BundleFileType::Tome,
"ugg" => BundleFileType::Ugg,
"unit" => BundleFileType::Unit,
"upb" => BundleFileType::Upb,
"vector_field" => BundleFileType::VectorField,
"wav" => BundleFileType::Wav,
"wwise_bank" => BundleFileType::WwiseBank,
"wwise_dep" => BundleFileType::WwiseDep,
"wwise_event" => BundleFileType::WwiseEvent,
"wwise_metadata" => BundleFileType::WwiseMetadata,
"wwise_stream" => BundleFileType::WwiseStream,
"xml" => BundleFileType::Xml,
s => eyre::bail!("Unknown type string '{}'", s),
};
Ok(val)
}
}
impl Serialize for BundleFileType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let value = self.ext_name();
value.serialize(serializer)
}
}
impl From<Murmur64> for BundleFileType {
fn from(value: Murmur64) -> Self {
Self::from(Into::<u64>::into(value))
}
}
impl From<u64> for BundleFileType {
fn from(hash: u64) -> BundleFileType {
match hash {
0x931e336d7646cc26 => BundleFileType::Animation,
0xdcfb9e18fff13984 => BundleFileType::AnimationCurves,
0x3eed05ba83af5090 => BundleFileType::Apb,
0x7ffdb779b04e4ed1 => BundleFileType::BakedLighting,
0xaa5965f03029fa18 => BundleFileType::Bik,
0xe301e8af94e3b5a3 => BundleFileType::BlendSet,
0x18dead01056b72e9 => BundleFileType::Bones,
0xb7893adf7567506a => BundleFileType::Chroma,
0xfe9754bd19814a47 => BundleFileType::CommonPackage,
0x82645835e6b73232 => BundleFileType::Config,
0x69108ded1e3e634b => BundleFileType::Crypto,
0x8fd0d44d20650b68 => BundleFileType::Data,
0x9831ca893b0d087d => BundleFileType::Entity,
0x92d3ee038eeb610d => BundleFileType::Flow,
0x9efe0a916aae7880 => BundleFileType::Font,
0x8f7d5a2c0f967655 => BundleFileType::Ies,
0xd526a27da14f1dc5 => BundleFileType::Ini,
0x2bbcabe5074ade9e => BundleFileType::Input,
0xfa4a8e091a91201e => BundleFileType::Ivf,
0xa62f9297dc969e85 => BundleFileType::Keys,
0x2a690fd348fe9ac5 => BundleFileType::Level,
0xa14e8dfa2cd117e2 => BundleFileType::Lua,
0xeac0b497876adedf => BundleFileType::Material,
0x3fcdd69156a46417 => BundleFileType::Mod,
0xb277b11fe4a61d37 => BundleFileType::MouseCursor,
0x169de9566953d264 => BundleFileType::NavData,
0x3b1fa9e8f6bac374 => BundleFileType::NetworkConfig,
0xb0f2c12eb107f4d8 => BundleFileType::OddleNet,
0xad9c6d9ed1e5e77a => BundleFileType::Package,
0xa8193123526fad64 => BundleFileType::Particles,
0xbf21403a3ab0bbb1 => BundleFileType::PhysicsProperties,
0x27862fe24795319c => BundleFileType::RenderConfig,
0x9ca183c2d0e76dee => BundleFileType::RtPipeline,
0x9d0a795bfe818d19 => BundleFileType::Scene,
0xcce8d5b5f5ae333f => BundleFileType::Shader,
0xe5ee32a477239a93 => BundleFileType::ShaderLibrary,
0x9e5c3cc74575aeb5 => BundleFileType::ShaderLibraryGroup,
0x250e0a11ac8e26f8 => BundleFileType::ShadingEnvionmentMapping,
0xfe73c7dcff8a7ca5 => BundleFileType::ShadingEnvironment,
0xa27b4d04a9ba6f9e => BundleFileType::Slug,
0xe9fc9ea7042e5ec0 => BundleFileType::SlugAlbum,
0xd8b27864a97ffdd7 => BundleFileType::SoundEnvironment,
0xf97af9983c05b950 => BundleFileType::SpuJob,
0xa486d4045106165c => BundleFileType::StateMachine,
0xe3f0baa17d620321 => BundleFileType::StaticPVS,
0x0d972bab10b40fd3 => BundleFileType::Strings,
0xad2d3fa30d9ab394 => BundleFileType::SurfaceProperties,
0xcd4238c6a0c69e32 => BundleFileType::Texture,
0x99736be1fff739a4 => BundleFileType::TimpaniBank,
0x00a3e6c59a2b9c6c => BundleFileType::TimpaniMaster,
0x19c792357c99f49b => BundleFileType::Tome,
0x712d6e3dd1024c9c => BundleFileType::Ugg,
0xe0a48d0be9a7453f => BundleFileType::Unit,
0xa99510c6e86dd3c2 => BundleFileType::Upb,
0xf7505933166d6755 => BundleFileType::VectorField,
0x786f65c00a816b19 => BundleFileType::Wav,
0x535a7bd3e650d799 => BundleFileType::WwiseBank,
0xaf32095c82f2b070 => BundleFileType::WwiseDep,
0xaabdd317b58dfc8a => BundleFileType::WwiseEvent,
0xd50a8b7e1c82b110 => BundleFileType::WwiseMetadata,
0x504b55235d21440e => BundleFileType::WwiseStream,
0x76015845a6003765 => BundleFileType::Xml,
_ => BundleFileType::Unknown(Murmur64::from(hash)),
}
}
}
impl From<BundleFileType> for u64 {
fn from(t: BundleFileType) -> u64 {
match t {
BundleFileType::Animation => 0x931e336d7646cc26,
BundleFileType::AnimationCurves => 0xdcfb9e18fff13984,
BundleFileType::Apb => 0x3eed05ba83af5090,
BundleFileType::BakedLighting => 0x7ffdb779b04e4ed1,
BundleFileType::Bik => 0xaa5965f03029fa18,
BundleFileType::BlendSet => 0xe301e8af94e3b5a3,
BundleFileType::Bones => 0x18dead01056b72e9,
BundleFileType::Chroma => 0xb7893adf7567506a,
BundleFileType::CommonPackage => 0xfe9754bd19814a47,
BundleFileType::Config => 0x82645835e6b73232,
BundleFileType::Crypto => 0x69108ded1e3e634b,
BundleFileType::Data => 0x8fd0d44d20650b68,
BundleFileType::Entity => 0x9831ca893b0d087d,
BundleFileType::Flow => 0x92d3ee038eeb610d,
BundleFileType::Font => 0x9efe0a916aae7880,
BundleFileType::Ies => 0x8f7d5a2c0f967655,
BundleFileType::Ini => 0xd526a27da14f1dc5,
BundleFileType::Input => 0x2bbcabe5074ade9e,
BundleFileType::Ivf => 0xfa4a8e091a91201e,
BundleFileType::Keys => 0xa62f9297dc969e85,
BundleFileType::Level => 0x2a690fd348fe9ac5,
BundleFileType::Lua => 0xa14e8dfa2cd117e2,
BundleFileType::Material => 0xeac0b497876adedf,
BundleFileType::Mod => 0x3fcdd69156a46417,
BundleFileType::MouseCursor => 0xb277b11fe4a61d37,
BundleFileType::NavData => 0x169de9566953d264,
BundleFileType::NetworkConfig => 0x3b1fa9e8f6bac374,
BundleFileType::OddleNet => 0xb0f2c12eb107f4d8,
BundleFileType::Package => 0xad9c6d9ed1e5e77a,
BundleFileType::Particles => 0xa8193123526fad64,
BundleFileType::PhysicsProperties => 0xbf21403a3ab0bbb1,
BundleFileType::RenderConfig => 0x27862fe24795319c,
BundleFileType::RtPipeline => 0x9ca183c2d0e76dee,
BundleFileType::Scene => 0x9d0a795bfe818d19,
BundleFileType::Shader => 0xcce8d5b5f5ae333f,
BundleFileType::ShaderLibrary => 0xe5ee32a477239a93,
BundleFileType::ShaderLibraryGroup => 0x9e5c3cc74575aeb5,
BundleFileType::ShadingEnvionmentMapping => 0x250e0a11ac8e26f8,
BundleFileType::ShadingEnvironment => 0xfe73c7dcff8a7ca5,
BundleFileType::Slug => 0xa27b4d04a9ba6f9e,
BundleFileType::SlugAlbum => 0xe9fc9ea7042e5ec0,
BundleFileType::SoundEnvironment => 0xd8b27864a97ffdd7,
BundleFileType::SpuJob => 0xf97af9983c05b950,
BundleFileType::StateMachine => 0xa486d4045106165c,
BundleFileType::StaticPVS => 0xe3f0baa17d620321,
BundleFileType::Strings => 0x0d972bab10b40fd3,
BundleFileType::SurfaceProperties => 0xad2d3fa30d9ab394,
BundleFileType::Texture => 0xcd4238c6a0c69e32,
BundleFileType::TimpaniBank => 0x99736be1fff739a4,
BundleFileType::TimpaniMaster => 0x00a3e6c59a2b9c6c,
BundleFileType::Tome => 0x19c792357c99f49b,
BundleFileType::Ugg => 0x712d6e3dd1024c9c,
BundleFileType::Unit => 0xe0a48d0be9a7453f,
BundleFileType::Upb => 0xa99510c6e86dd3c2,
BundleFileType::VectorField => 0xf7505933166d6755,
BundleFileType::Wav => 0x786f65c00a816b19,
BundleFileType::WwiseBank => 0x535a7bd3e650d799,
BundleFileType::WwiseDep => 0xaf32095c82f2b070,
BundleFileType::WwiseEvent => 0xaabdd317b58dfc8a,
BundleFileType::WwiseMetadata => 0xd50a8b7e1c82b110,
BundleFileType::WwiseStream => 0x504b55235d21440e,
BundleFileType::Xml => 0x76015845a6003765,
BundleFileType::Unknown(hash) => hash.into(),
}
}
}
impl From<BundleFileType> for Murmur64 {
fn from(t: BundleFileType) -> Murmur64 {
let hash: u64 = t.into();
Murmur64::from(hash)
}
}
impl std::fmt::Display for BundleFileType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.ext_name())
}
}
#[derive(Debug)] #[derive(Debug)]
struct BundleFileHeader { struct BundleFileHeader {
@ -515,7 +120,7 @@ pub struct BundleFile {
} }
impl BundleFile { impl BundleFile {
pub fn new(name: String, file_type: BundleFileType) -> Self { pub fn new(name: impl Into<IdString64>, file_type: BundleFileType) -> Self {
Self { Self {
file_type, file_type,
name: name.into(), name: name.into(),
@ -647,20 +252,15 @@ impl BundleFile {
Ok(w.into_inner()) Ok(w.into_inner())
} }
#[tracing::instrument(name = "File::from_sjson", skip(sjson))] #[tracing::instrument("File::from_sjson", skip(sjson, name), fields(name = %name.display()))]
pub async fn from_sjson<P, S>( pub async fn from_sjson(
name: String, name: IdString64,
file_type: BundleFileType, file_type: BundleFileType,
sjson: S, sjson: impl AsRef<str>,
root: P, root: impl AsRef<Path> + std::fmt::Debug,
) -> Result<Self> ) -> Result<Self> {
where
P: AsRef<Path> + std::fmt::Debug,
S: AsRef<str>,
{
match file_type { match file_type {
BundleFileType::Lua => lua::compile(name.clone(), sjson) BundleFileType::Lua => lua::compile(name, sjson).wrap_err("Failed to compile Lua file"),
.wrap_err_with(|| format!("Failed to compile Lua file '{}'", name)),
BundleFileType::Unknown(_) => { BundleFileType::Unknown(_) => {
eyre::bail!("Unknown file type. Cannot compile from SJSON"); eyre::bail!("Unknown file type. Cannot compile from SJSON");
} }
@ -699,10 +299,7 @@ impl BundleFile {
s s
} }
pub fn matches_name<S>(&self, name: S) -> bool pub fn matches_name(&self, name: impl Into<IdString64>) -> bool {
where
S: Into<IdString64>,
{
let name = name.into(); let name = name.into();
if self.name == name { if self.name == name {
return true; return true;

View file

@ -0,0 +1,400 @@
use color_eyre::{eyre, Result};
use serde::Serialize;
use crate::murmur::Murmur64;
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub enum BundleFileType {
Animation,
AnimationCurves,
Apb,
BakedLighting,
Bik,
BlendSet,
Bones,
Chroma,
CommonPackage,
Config,
Crypto,
Data,
Entity,
Flow,
Font,
Ies,
Ini,
Input,
Ivf,
Keys,
Level,
Lua,
Material,
Mod,
MouseCursor,
NavData,
NetworkConfig,
OddleNet,
Package,
Particles,
PhysicsProperties,
RenderConfig,
RtPipeline,
Scene,
Shader,
ShaderLibrary,
ShaderLibraryGroup,
ShadingEnvionmentMapping,
ShadingEnvironment,
Slug,
SlugAlbum,
SoundEnvironment,
SpuJob,
StateMachine,
StaticPVS,
Strings,
SurfaceProperties,
Texture,
TimpaniBank,
TimpaniMaster,
Tome,
Ugg,
Unit,
Upb,
VectorField,
Wav,
WwiseBank,
WwiseDep,
WwiseEvent,
WwiseMetadata,
WwiseStream,
Xml,
Unknown(Murmur64),
}
impl BundleFileType {
pub fn ext_name(&self) -> String {
match self {
BundleFileType::AnimationCurves => String::from("animation_curves"),
BundleFileType::Animation => String::from("animation"),
BundleFileType::Apb => String::from("apb"),
BundleFileType::BakedLighting => String::from("baked_lighting"),
BundleFileType::Bik => String::from("bik"),
BundleFileType::BlendSet => String::from("blend_set"),
BundleFileType::Bones => String::from("bones"),
BundleFileType::Chroma => String::from("chroma"),
BundleFileType::CommonPackage => String::from("common_package"),
BundleFileType::Config => String::from("config"),
BundleFileType::Crypto => String::from("crypto"),
BundleFileType::Data => String::from("data"),
BundleFileType::Entity => String::from("entity"),
BundleFileType::Flow => String::from("flow"),
BundleFileType::Font => String::from("font"),
BundleFileType::Ies => String::from("ies"),
BundleFileType::Ini => String::from("ini"),
BundleFileType::Input => String::from("input"),
BundleFileType::Ivf => String::from("ivf"),
BundleFileType::Keys => String::from("keys"),
BundleFileType::Level => String::from("level"),
BundleFileType::Lua => String::from("lua"),
BundleFileType::Material => String::from("material"),
BundleFileType::Mod => String::from("mod"),
BundleFileType::MouseCursor => String::from("mouse_cursor"),
BundleFileType::NavData => String::from("nav_data"),
BundleFileType::NetworkConfig => String::from("network_config"),
BundleFileType::OddleNet => String::from("oodle_net"),
BundleFileType::Package => String::from("package"),
BundleFileType::Particles => String::from("particles"),
BundleFileType::PhysicsProperties => String::from("physics_properties"),
BundleFileType::RenderConfig => String::from("render_config"),
BundleFileType::RtPipeline => String::from("rt_pipeline"),
BundleFileType::Scene => String::from("scene"),
BundleFileType::ShaderLibraryGroup => String::from("shader_library_group"),
BundleFileType::ShaderLibrary => String::from("shader_library"),
BundleFileType::Shader => String::from("shader"),
BundleFileType::ShadingEnvionmentMapping => String::from("shading_environment_mapping"),
BundleFileType::ShadingEnvironment => String::from("shading_environment"),
BundleFileType::SlugAlbum => String::from("slug_album"),
BundleFileType::Slug => String::from("slug"),
BundleFileType::SoundEnvironment => String::from("sound_environment"),
BundleFileType::SpuJob => String::from("spu_job"),
BundleFileType::StateMachine => String::from("state_machine"),
BundleFileType::StaticPVS => String::from("static_pvs"),
BundleFileType::Strings => String::from("strings"),
BundleFileType::SurfaceProperties => String::from("surface_properties"),
BundleFileType::Texture => String::from("texture"),
BundleFileType::TimpaniBank => String::from("timpani_bank"),
BundleFileType::TimpaniMaster => String::from("timpani_master"),
BundleFileType::Tome => String::from("tome"),
BundleFileType::Ugg => String::from("ugg"),
BundleFileType::Unit => String::from("unit"),
BundleFileType::Upb => String::from("upb"),
BundleFileType::VectorField => String::from("vector_field"),
BundleFileType::Wav => String::from("wav"),
BundleFileType::WwiseBank => String::from("wwise_bank"),
BundleFileType::WwiseDep => String::from("wwise_dep"),
BundleFileType::WwiseEvent => String::from("wwise_event"),
BundleFileType::WwiseMetadata => String::from("wwise_metadata"),
BundleFileType::WwiseStream => String::from("wwise_stream"),
BundleFileType::Xml => String::from("xml"),
BundleFileType::Unknown(s) => format!("{s:016X}"),
}
}
pub fn decompiled_ext_name(&self) -> String {
match self {
BundleFileType::Texture => String::from("dds"),
BundleFileType::WwiseBank => String::from("bnk"),
BundleFileType::WwiseStream => String::from("ogg"),
_ => self.ext_name(),
}
}
pub fn hash(&self) -> Murmur64 {
Murmur64::from(*self)
}
}
impl std::str::FromStr for BundleFileType {
type Err = color_eyre::Report;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let val = match s {
"animation_curves" => BundleFileType::AnimationCurves,
"animation" => BundleFileType::Animation,
"apb" => BundleFileType::Apb,
"baked_lighting" => BundleFileType::BakedLighting,
"bik" => BundleFileType::Bik,
"blend_set" => BundleFileType::BlendSet,
"bones" => BundleFileType::Bones,
"chroma" => BundleFileType::Chroma,
"common_package" => BundleFileType::CommonPackage,
"config" => BundleFileType::Config,
"crypto" => BundleFileType::Crypto,
"data" => BundleFileType::Data,
"entity" => BundleFileType::Entity,
"flow" => BundleFileType::Flow,
"font" => BundleFileType::Font,
"ies" => BundleFileType::Ies,
"ini" => BundleFileType::Ini,
"input" => BundleFileType::Input,
"ivf" => BundleFileType::Ivf,
"keys" => BundleFileType::Keys,
"level" => BundleFileType::Level,
"lua" => BundleFileType::Lua,
"material" => BundleFileType::Material,
"mod" => BundleFileType::Mod,
"mouse_cursor" => BundleFileType::MouseCursor,
"nav_data" => BundleFileType::NavData,
"network_config" => BundleFileType::NetworkConfig,
"oodle_net" => BundleFileType::OddleNet,
"package" => BundleFileType::Package,
"particles" => BundleFileType::Particles,
"physics_properties" => BundleFileType::PhysicsProperties,
"render_config" => BundleFileType::RenderConfig,
"rt_pipeline" => BundleFileType::RtPipeline,
"scene" => BundleFileType::Scene,
"shader_library_group" => BundleFileType::ShaderLibraryGroup,
"shader_library" => BundleFileType::ShaderLibrary,
"shader" => BundleFileType::Shader,
"shading_environment_mapping" => BundleFileType::ShadingEnvionmentMapping,
"shading_environment" => BundleFileType::ShadingEnvironment,
"slug_album" => BundleFileType::SlugAlbum,
"slug" => BundleFileType::Slug,
"sound_environment" => BundleFileType::SoundEnvironment,
"spu_job" => BundleFileType::SpuJob,
"state_machine" => BundleFileType::StateMachine,
"static_pvs" => BundleFileType::StaticPVS,
"strings" => BundleFileType::Strings,
"surface_properties" => BundleFileType::SurfaceProperties,
"texture" => BundleFileType::Texture,
"timpani_bank" => BundleFileType::TimpaniBank,
"timpani_master" => BundleFileType::TimpaniMaster,
"tome" => BundleFileType::Tome,
"ugg" => BundleFileType::Ugg,
"unit" => BundleFileType::Unit,
"upb" => BundleFileType::Upb,
"vector_field" => BundleFileType::VectorField,
"wav" => BundleFileType::Wav,
"wwise_bank" => BundleFileType::WwiseBank,
"wwise_dep" => BundleFileType::WwiseDep,
"wwise_event" => BundleFileType::WwiseEvent,
"wwise_metadata" => BundleFileType::WwiseMetadata,
"wwise_stream" => BundleFileType::WwiseStream,
"xml" => BundleFileType::Xml,
s => eyre::bail!("Unknown type string '{}'", s),
};
Ok(val)
}
}
impl Serialize for BundleFileType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let value = self.ext_name();
value.serialize(serializer)
}
}
impl From<Murmur64> for BundleFileType {
fn from(value: Murmur64) -> Self {
Self::from(Into::<u64>::into(value))
}
}
impl From<u64> for BundleFileType {
fn from(hash: u64) -> BundleFileType {
match hash {
0x931e336d7646cc26 => BundleFileType::Animation,
0xdcfb9e18fff13984 => BundleFileType::AnimationCurves,
0x3eed05ba83af5090 => BundleFileType::Apb,
0x7ffdb779b04e4ed1 => BundleFileType::BakedLighting,
0xaa5965f03029fa18 => BundleFileType::Bik,
0xe301e8af94e3b5a3 => BundleFileType::BlendSet,
0x18dead01056b72e9 => BundleFileType::Bones,
0xb7893adf7567506a => BundleFileType::Chroma,
0xfe9754bd19814a47 => BundleFileType::CommonPackage,
0x82645835e6b73232 => BundleFileType::Config,
0x69108ded1e3e634b => BundleFileType::Crypto,
0x8fd0d44d20650b68 => BundleFileType::Data,
0x9831ca893b0d087d => BundleFileType::Entity,
0x92d3ee038eeb610d => BundleFileType::Flow,
0x9efe0a916aae7880 => BundleFileType::Font,
0x8f7d5a2c0f967655 => BundleFileType::Ies,
0xd526a27da14f1dc5 => BundleFileType::Ini,
0x2bbcabe5074ade9e => BundleFileType::Input,
0xfa4a8e091a91201e => BundleFileType::Ivf,
0xa62f9297dc969e85 => BundleFileType::Keys,
0x2a690fd348fe9ac5 => BundleFileType::Level,
0xa14e8dfa2cd117e2 => BundleFileType::Lua,
0xeac0b497876adedf => BundleFileType::Material,
0x3fcdd69156a46417 => BundleFileType::Mod,
0xb277b11fe4a61d37 => BundleFileType::MouseCursor,
0x169de9566953d264 => BundleFileType::NavData,
0x3b1fa9e8f6bac374 => BundleFileType::NetworkConfig,
0xb0f2c12eb107f4d8 => BundleFileType::OddleNet,
0xad9c6d9ed1e5e77a => BundleFileType::Package,
0xa8193123526fad64 => BundleFileType::Particles,
0xbf21403a3ab0bbb1 => BundleFileType::PhysicsProperties,
0x27862fe24795319c => BundleFileType::RenderConfig,
0x9ca183c2d0e76dee => BundleFileType::RtPipeline,
0x9d0a795bfe818d19 => BundleFileType::Scene,
0xcce8d5b5f5ae333f => BundleFileType::Shader,
0xe5ee32a477239a93 => BundleFileType::ShaderLibrary,
0x9e5c3cc74575aeb5 => BundleFileType::ShaderLibraryGroup,
0x250e0a11ac8e26f8 => BundleFileType::ShadingEnvionmentMapping,
0xfe73c7dcff8a7ca5 => BundleFileType::ShadingEnvironment,
0xa27b4d04a9ba6f9e => BundleFileType::Slug,
0xe9fc9ea7042e5ec0 => BundleFileType::SlugAlbum,
0xd8b27864a97ffdd7 => BundleFileType::SoundEnvironment,
0xf97af9983c05b950 => BundleFileType::SpuJob,
0xa486d4045106165c => BundleFileType::StateMachine,
0xe3f0baa17d620321 => BundleFileType::StaticPVS,
0x0d972bab10b40fd3 => BundleFileType::Strings,
0xad2d3fa30d9ab394 => BundleFileType::SurfaceProperties,
0xcd4238c6a0c69e32 => BundleFileType::Texture,
0x99736be1fff739a4 => BundleFileType::TimpaniBank,
0x00a3e6c59a2b9c6c => BundleFileType::TimpaniMaster,
0x19c792357c99f49b => BundleFileType::Tome,
0x712d6e3dd1024c9c => BundleFileType::Ugg,
0xe0a48d0be9a7453f => BundleFileType::Unit,
0xa99510c6e86dd3c2 => BundleFileType::Upb,
0xf7505933166d6755 => BundleFileType::VectorField,
0x786f65c00a816b19 => BundleFileType::Wav,
0x535a7bd3e650d799 => BundleFileType::WwiseBank,
0xaf32095c82f2b070 => BundleFileType::WwiseDep,
0xaabdd317b58dfc8a => BundleFileType::WwiseEvent,
0xd50a8b7e1c82b110 => BundleFileType::WwiseMetadata,
0x504b55235d21440e => BundleFileType::WwiseStream,
0x76015845a6003765 => BundleFileType::Xml,
_ => BundleFileType::Unknown(Murmur64::from(hash)),
}
}
}
impl From<BundleFileType> for u64 {
fn from(t: BundleFileType) -> u64 {
match t {
BundleFileType::Animation => 0x931e336d7646cc26,
BundleFileType::AnimationCurves => 0xdcfb9e18fff13984,
BundleFileType::Apb => 0x3eed05ba83af5090,
BundleFileType::BakedLighting => 0x7ffdb779b04e4ed1,
BundleFileType::Bik => 0xaa5965f03029fa18,
BundleFileType::BlendSet => 0xe301e8af94e3b5a3,
BundleFileType::Bones => 0x18dead01056b72e9,
BundleFileType::Chroma => 0xb7893adf7567506a,
BundleFileType::CommonPackage => 0xfe9754bd19814a47,
BundleFileType::Config => 0x82645835e6b73232,
BundleFileType::Crypto => 0x69108ded1e3e634b,
BundleFileType::Data => 0x8fd0d44d20650b68,
BundleFileType::Entity => 0x9831ca893b0d087d,
BundleFileType::Flow => 0x92d3ee038eeb610d,
BundleFileType::Font => 0x9efe0a916aae7880,
BundleFileType::Ies => 0x8f7d5a2c0f967655,
BundleFileType::Ini => 0xd526a27da14f1dc5,
BundleFileType::Input => 0x2bbcabe5074ade9e,
BundleFileType::Ivf => 0xfa4a8e091a91201e,
BundleFileType::Keys => 0xa62f9297dc969e85,
BundleFileType::Level => 0x2a690fd348fe9ac5,
BundleFileType::Lua => 0xa14e8dfa2cd117e2,
BundleFileType::Material => 0xeac0b497876adedf,
BundleFileType::Mod => 0x3fcdd69156a46417,
BundleFileType::MouseCursor => 0xb277b11fe4a61d37,
BundleFileType::NavData => 0x169de9566953d264,
BundleFileType::NetworkConfig => 0x3b1fa9e8f6bac374,
BundleFileType::OddleNet => 0xb0f2c12eb107f4d8,
BundleFileType::Package => 0xad9c6d9ed1e5e77a,
BundleFileType::Particles => 0xa8193123526fad64,
BundleFileType::PhysicsProperties => 0xbf21403a3ab0bbb1,
BundleFileType::RenderConfig => 0x27862fe24795319c,
BundleFileType::RtPipeline => 0x9ca183c2d0e76dee,
BundleFileType::Scene => 0x9d0a795bfe818d19,
BundleFileType::Shader => 0xcce8d5b5f5ae333f,
BundleFileType::ShaderLibrary => 0xe5ee32a477239a93,
BundleFileType::ShaderLibraryGroup => 0x9e5c3cc74575aeb5,
BundleFileType::ShadingEnvionmentMapping => 0x250e0a11ac8e26f8,
BundleFileType::ShadingEnvironment => 0xfe73c7dcff8a7ca5,
BundleFileType::Slug => 0xa27b4d04a9ba6f9e,
BundleFileType::SlugAlbum => 0xe9fc9ea7042e5ec0,
BundleFileType::SoundEnvironment => 0xd8b27864a97ffdd7,
BundleFileType::SpuJob => 0xf97af9983c05b950,
BundleFileType::StateMachine => 0xa486d4045106165c,
BundleFileType::StaticPVS => 0xe3f0baa17d620321,
BundleFileType::Strings => 0x0d972bab10b40fd3,
BundleFileType::SurfaceProperties => 0xad2d3fa30d9ab394,
BundleFileType::Texture => 0xcd4238c6a0c69e32,
BundleFileType::TimpaniBank => 0x99736be1fff739a4,
BundleFileType::TimpaniMaster => 0x00a3e6c59a2b9c6c,
BundleFileType::Tome => 0x19c792357c99f49b,
BundleFileType::Ugg => 0x712d6e3dd1024c9c,
BundleFileType::Unit => 0xe0a48d0be9a7453f,
BundleFileType::Upb => 0xa99510c6e86dd3c2,
BundleFileType::VectorField => 0xf7505933166d6755,
BundleFileType::Wav => 0x786f65c00a816b19,
BundleFileType::WwiseBank => 0x535a7bd3e650d799,
BundleFileType::WwiseDep => 0xaf32095c82f2b070,
BundleFileType::WwiseEvent => 0xaabdd317b58dfc8a,
BundleFileType::WwiseMetadata => 0xd50a8b7e1c82b110,
BundleFileType::WwiseStream => 0x504b55235d21440e,
BundleFileType::Xml => 0x76015845a6003765,
BundleFileType::Unknown(hash) => hash.into(),
}
}
}
impl From<BundleFileType> for Murmur64 {
fn from(t: BundleFileType) -> Murmur64 {
let hash: u64 = t.into();
Murmur64::from(hash)
}
}
impl std::fmt::Display for BundleFileType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.ext_name())
}
}

View file

@ -12,8 +12,10 @@ use crate::murmur::{HashGroup, IdString64, Murmur64};
pub(crate) mod database; pub(crate) mod database;
pub(crate) mod file; pub(crate) mod file;
pub(crate) mod filetype;
pub use file::{BundleFile, BundleFileType, BundleFileVariant}; pub use file::{BundleFile, BundleFileVariant};
pub use filetype::BundleFileType;
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
enum BundleFormat { enum BundleFormat {

View file

@ -15,6 +15,7 @@ use tokio::fs;
use crate::binary::sync::ReadExt; use crate::binary::sync::ReadExt;
use crate::binary::sync::WriteExt; use crate::binary::sync::WriteExt;
use crate::bundle::file::{BundleFileVariant, UserFile}; use crate::bundle::file::{BundleFileVariant, UserFile};
use crate::murmur::IdString64;
use crate::{BundleFile, BundleFileType}; use crate::{BundleFile, BundleFileType};
const BITSQUID_LUAJIT_HEADER: u32 = 0x8253461B; const BITSQUID_LUAJIT_HEADER: u32 = 0x8253461B;
@ -117,17 +118,13 @@ where
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn compile<S, C>(name: S, code: C) -> Result<BundleFile> pub fn compile(name: impl Into<IdString64>, code: impl AsRef<str>) -> Result<BundleFile> {
where
S: Into<String>,
C: AsRef<str>,
{
let name = name.into(); let name = name.into();
let code = code.as_ref(); let code = code.as_ref();
tracing::trace!( tracing::trace!(
"Compiling '{}', {} bytes of code", "Compiling '{}', {} bytes of code",
name, name.display(),
code.as_bytes().len() code.as_bytes().len()
); );
@ -135,8 +132,8 @@ where
let state = lua::luaL_newstate(); let state = lua::luaL_newstate();
lua::luaL_openlibs(state); lua::luaL_openlibs(state);
let name = CString::new(format!("@{name}").into_bytes()) let name = CString::new(format!("@{}", name.display()).into_bytes())
.wrap_err_with(|| format!("Cannot convert name into CString: {}", name))?; .wrap_err_with(|| format!("Cannot convert name into CString: {}", name.display()))?;
match lua::luaL_loadbuffer( match lua::luaL_loadbuffer(
state, state,
code.as_ptr() as _, code.as_ptr() as _,

View file

@ -7,13 +7,22 @@ use std::str::FromStr;
use async_recursion::async_recursion; use async_recursion::async_recursion;
use color_eyre::eyre::{self, Context}; use color_eyre::eyre::{self, Context};
use color_eyre::Result; use color_eyre::Result;
use path_slash::PathBufExt;
use tokio::fs; use tokio::fs;
use crate::binary::sync::{ReadExt, WriteExt}; use crate::binary::sync::{ReadExt, WriteExt};
use crate::bundle::file::{BundleFileType, UserFile}; use crate::bundle::file::UserFile;
use crate::murmur::{HashGroup, Murmur64}; use crate::bundle::filetype::BundleFileType;
use crate::murmur::{HashGroup, IdString64, Murmur64};
/// Resolves a relative path that might contain wildcards into a list of
/// paths that exist on disk and match that wildcard.
/// This is similar to globbing in Unix shells, but with much less features.
///
/// The only wilcard character allowed is `*`, and only at the end of the string,
/// where it matches all files recursively in that directory.
///
/// `t` is an optional extension name, that may be used to force a wildcard
/// path to only match that file type `t`.
#[tracing::instrument] #[tracing::instrument]
#[async_recursion] #[async_recursion]
async fn resolve_wildcard<P1, P2>( async fn resolve_wildcard<P1, P2>(
@ -90,12 +99,12 @@ where
Ok(paths) Ok(paths)
} }
type PackageType = HashMap<BundleFileType, HashSet<PathBuf>>; type PackageType = HashMap<BundleFileType, HashSet<String>>;
type PackageDefinition = HashMap<String, HashSet<String>>; type PackageDefinition = HashMap<String, HashSet<String>>;
#[derive(Default)] #[derive(Default)]
pub struct Package { pub struct Package {
_name: String, _name: IdString64,
_root: PathBuf, _root: PathBuf,
inner: PackageType, inner: PackageType,
flags: u8, flags: u8,
@ -116,9 +125,9 @@ impl DerefMut for Package {
} }
impl Package { impl Package {
pub fn new(name: String, root: PathBuf) -> Self { pub fn new(name: impl Into<IdString64>, root: PathBuf) -> Self {
Self { Self {
_name: name, _name: name.into(),
_root: root, _root: root,
inner: Default::default(), inner: Default::default(),
flags: 1, flags: 1,
@ -129,17 +138,22 @@ impl Package {
self.values().fold(0, |total, files| total + files.len()) self.values().fold(0, |total, files| total + files.len())
} }
pub fn add_file<P: Into<PathBuf>>(&mut self, file_type: BundleFileType, name: P) { pub fn add_file(&mut self, file_type: BundleFileType, name: impl Into<String>) {
self.inner.entry(file_type).or_default().insert(name.into()); self.inner.entry(file_type).or_default().insert(name.into());
} }
#[tracing::instrument("Package::from_sjson", skip(sjson), fields(sjson_len = sjson.as_ref().len()))] #[tracing::instrument("Package::from_sjson", skip(sjson), fields(sjson_len = sjson.as_ref().len()))]
pub async fn from_sjson<P, S>(sjson: S, name: String, root: P) -> Result<Self> pub async fn from_sjson<P, S>(
sjson: S,
name: impl Into<IdString64> + std::fmt::Debug,
root: P,
) -> Result<Self>
where where
P: AsRef<Path> + std::fmt::Debug, P: AsRef<Path> + std::fmt::Debug,
S: AsRef<str>, S: AsRef<str>,
{ {
let root = root.as_ref(); let root = root.as_ref();
let name = name.into();
let definition: PackageDefinition = serde_sjson::from_str(sjson.as_ref())?; let definition: PackageDefinition = serde_sjson::from_str(sjson.as_ref())?;
let mut inner: PackageType = Default::default(); let mut inner: PackageType = Default::default();
@ -173,7 +187,11 @@ impl Package {
continue; continue;
}; };
inner.entry(t).or_default().insert(path); tracing::debug!("Adding file {}", path.display());
inner
.entry(t)
.or_default()
.insert(path.display().to_string());
} }
} }
} }
@ -192,11 +210,9 @@ impl Package {
pub fn to_sjson(&self) -> Result<String> { pub fn to_sjson(&self) -> Result<String> {
let mut map: PackageDefinition = Default::default(); let mut map: PackageDefinition = Default::default();
for (t, paths) in self.iter() { for (t, names) in self.iter() {
for path in paths.iter() { for name in names.iter() {
map.entry(t.ext_name()) map.entry(t.ext_name()).or_default().insert(name.clone());
.or_default()
.insert(path.display().to_string());
} }
} }
@ -222,11 +238,11 @@ impl Package {
for _ in 0..file_count { for _ in 0..file_count {
let t = BundleFileType::from(r.read_u64()?); let t = BundleFileType::from(r.read_u64()?);
let hash = Murmur64::from(r.read_u64()?); let hash = Murmur64::from(r.read_u64()?);
let path = ctx.lookup_hash(hash, HashGroup::Filename); let name = ctx.lookup_hash(hash, HashGroup::Filename);
inner inner
.entry(t) .entry(t)
.or_default() .or_default()
.insert(PathBuf::from(path.display().to_string())); .insert(name.display().to_string());
} }
let flags = r.read_u8()?; let flags = r.read_u8()?;
@ -239,7 +255,7 @@ impl Package {
let pkg = Self { let pkg = Self {
inner, inner,
_name: name, _name: name.into(),
_root: PathBuf::new(), _root: PathBuf::new(),
flags, flags,
}; };
@ -255,12 +271,10 @@ impl Package {
w.write_u32(0x2b)?; w.write_u32(0x2b)?;
w.write_u32(self.values().flatten().count() as u32)?; w.write_u32(self.values().flatten().count() as u32)?;
for (t, paths) in self.iter() { for (t, names) in self.iter() {
for path in paths.iter() { for name in names.iter() {
w.write_u64(t.hash().into())?; w.write_u64(t.hash().into())?;
w.write_u64(Murmur64::hash(name.as_bytes()).into())?;
let hash = Murmur64::hash(path.to_slash_lossy().as_bytes());
w.write_u64(hash.into())?;
} }
} }
@ -280,17 +294,11 @@ where
Ok(vec![UserFile::new(s.into_bytes())]) Ok(vec![UserFile::new(s.into_bytes())])
} }
// #[tracing::instrument(skip_all)]
// pub fn compile(_ctx: &crate::Context, data: String) -> Result<Vec<u8>> {
// let pkg = Package::from_sjson(data)?;
// pkg.to_binary()
// }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::path::PathBuf; use std::path::PathBuf;
use crate::BundleFileType; use crate::bundle::filetype::BundleFileType;
use super::resolve_wildcard; use super::resolve_wildcard;
use super::Package; use super::Package;

View file

@ -147,14 +147,14 @@ impl Dictionary {
Ok(()) Ok(())
} }
pub fn add(&mut self, value: String, group: HashGroup) { pub fn add(&mut self, value: impl AsRef<[u8]>, group: HashGroup) {
let long = Murmur64::from(murmurhash64::hash(value.as_bytes(), SEED as u64)); let long = Murmur64::from(murmurhash64::hash(value.as_ref(), SEED as u64));
let short = Murmur32::from(murmurhash64::hash32(value.as_bytes(), SEED)); let short = Murmur32::from(murmurhash64::hash32(value.as_ref(), SEED));
let entry = Entry { let entry = Entry {
long, long,
short, short,
value, value: String::from_utf8_lossy(value.as_ref()).to_string(),
group, group,
}; };

View file

@ -0,0 +1,162 @@
use std::fmt;
use serde::{Deserializer, Serializer};
use super::Murmur32;
// This type encodes the fact that when reading in a bundle, we don't always have a dictionary
// entry for every hash in there. So we do want to have the real string available when needed,
// but at the same time retain the original hash information for when we don't.
// This is especially important when wanting to write back the read bundle, as the hashes need to
// stay the same.
// The previous system of always turning hashes into strings worked well for the purpose of
// displaying hashes, but would have made it very hard to turn a stringyfied hash back into
// an actual hash.
#[derive(Clone, Debug, Eq)]
pub enum IdString32 {
Hash(Murmur32),
String(String),
}
impl IdString32 {
pub fn to_murmur32(&self) -> Murmur32 {
match self {
Self::Hash(hash) => *hash,
Self::String(s) => Murmur32::hash(s.as_bytes()),
}
}
pub fn display(&self) -> IdString32Display {
let s = match self {
IdString32::Hash(hash) => hash.to_string(),
IdString32::String(s) => s.clone(),
};
IdString32Display(s)
}
pub fn is_string(&self) -> bool {
match self {
IdString32::Hash(_) => false,
IdString32::String(_) => true,
}
}
pub fn is_hash(&self) -> bool {
match self {
IdString32::Hash(_) => true,
IdString32::String(_) => false,
}
}
}
impl From<String> for IdString32 {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<u32> for IdString32 {
fn from(value: u32) -> Self {
Self::Hash(value.into())
}
}
impl From<IdString32> for u32 {
fn from(value: IdString32) -> Self {
value.to_murmur32().into()
}
}
impl From<Murmur32> for IdString32 {
fn from(value: Murmur32) -> Self {
Self::Hash(value)
}
}
impl From<IdString32> for Murmur32 {
fn from(value: IdString32) -> Self {
value.to_murmur32()
}
}
impl PartialEq for IdString32 {
fn eq(&self, other: &Self) -> bool {
self.to_murmur32() == other.to_murmur32()
}
}
impl std::hash::Hash for IdString32 {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write_u32(self.to_murmur32().into());
}
}
impl serde::Serialize for IdString32 {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u32(self.to_murmur32().into())
}
}
struct IdString32Visitor;
impl<'de> serde::de::Visitor<'de> for IdString32Visitor {
type Value = IdString32;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an u32 or a string")
}
fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(IdString32::Hash(value.into()))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(IdString32::String(v.to_string()))
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(IdString32::String(v))
}
}
impl<'de> serde::Deserialize<'de> for IdString32 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_u32(IdString32Visitor)
}
}
pub struct IdString32Display(String);
impl std::fmt::Display for IdString32Display {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::fmt::UpperHex for IdString32 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::UpperHex::fmt(&self.to_murmur32(), f)
}
}
impl std::fmt::LowerHex for IdString32 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::LowerHex::fmt(&self.to_murmur32(), f)
}
}

View file

@ -0,0 +1,175 @@
use std::{fmt, path::Path};
use path_slash::PathExt as _;
use serde::{Deserializer, Serializer};
use super::Murmur64;
// This type encodes the fact that when reading in a bundle, we don't always have a dictionary
// entry for every hash in there. So we do want to have the real string available when needed,
// but at the same time retain the original hash information for when we don't.
// This is especially important when wanting to write back the read bundle, as the hashes need to
// stay the same.
// The previous system of always turning hashes into strings worked well for the purpose of
// displaying hashes, but would have made it very hard to turn a stringyfied hash back into
// an actual hash.
#[derive(Clone, Debug, Eq)]
pub enum IdString64 {
Hash(Murmur64),
String(String),
}
impl IdString64 {
pub fn to_murmur64(&self) -> Murmur64 {
match self {
Self::Hash(hash) => *hash,
Self::String(s) => Murmur64::hash(s.as_bytes()),
}
}
pub fn display(&self) -> IdString64Display {
let s = match self {
IdString64::Hash(hash) => hash.to_string(),
IdString64::String(s) => s.clone(),
};
IdString64Display(s)
}
pub fn is_string(&self) -> bool {
match self {
IdString64::Hash(_) => false,
IdString64::String(_) => true,
}
}
pub fn is_hash(&self) -> bool {
match self {
IdString64::Hash(_) => true,
IdString64::String(_) => false,
}
}
// Would love to have this as a proper `impl From`, but
// rustc will complain that it overlaps with the `impl From<Into<String>>`.
pub fn from_path(p: impl AsRef<Path>) -> Self {
Self::String(p.as_ref().to_slash_lossy().to_string())
}
}
impl From<String> for IdString64 {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<u64> for IdString64 {
fn from(value: u64) -> Self {
Self::Hash(value.into())
}
}
impl From<Murmur64> for IdString64 {
fn from(value: Murmur64) -> Self {
Self::Hash(value)
}
}
impl From<IdString64> for Murmur64 {
fn from(value: IdString64) -> Self {
value.to_murmur64()
}
}
impl From<IdString64> for u64 {
fn from(value: IdString64) -> Self {
value.to_murmur64().into()
}
}
impl Default for IdString64 {
fn default() -> Self {
Self::Hash(0.into())
}
}
impl PartialEq for IdString64 {
fn eq(&self, other: &Self) -> bool {
self.to_murmur64() == other.to_murmur64()
}
}
impl std::hash::Hash for IdString64 {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write_u64(self.to_murmur64().into());
}
}
impl serde::Serialize for IdString64 {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(self.to_murmur64().into())
}
}
struct IdString64Visitor;
impl<'de> serde::de::Visitor<'de> for IdString64Visitor {
type Value = IdString64;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an u64 or a string")
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(IdString64::Hash(value.into()))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(IdString64::String(v.to_string()))
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(IdString64::String(v))
}
}
impl<'de> serde::Deserialize<'de> for IdString64 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_u64(IdString64Visitor)
}
}
pub struct IdString64Display(String);
impl std::fmt::Display for IdString64Display {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::fmt::UpperHex for IdString64 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::UpperHex::fmt(&self.to_murmur64(), f)
}
}
impl std::fmt::LowerHex for IdString64 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::LowerHex::fmt(&self.to_murmur64(), f)
}
}

View file

@ -8,6 +8,8 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
mod dictionary; mod dictionary;
// Currently unused // Currently unused
// mod murmurhash32; // mod murmurhash32;
mod idstring32;
mod idstring64;
mod murmurhash64; mod murmurhash64;
mod types; mod types;
mod util; mod util;
@ -15,6 +17,8 @@ mod util;
pub const SEED: u32 = 0; pub const SEED: u32 = 0;
pub use dictionary::{Dictionary, Entry, HashGroup}; pub use dictionary::{Dictionary, Entry, HashGroup};
pub use idstring32::*;
pub use idstring64::*;
pub use murmurhash64::hash; pub use murmurhash64::hash;
pub use murmurhash64::hash32; pub use murmurhash64::hash32;
pub use murmurhash64::hash_inverse as inverse; pub use murmurhash64::hash_inverse as inverse;

View file

@ -119,4 +119,9 @@ fn test_hash() {
} }
#[test] #[test]
fn test_inverse() {} fn test_inverse() {
let h = hash("lua".as_bytes(), crate::murmur::SEED as u64);
let inv = hash_inverse(h, crate::murmur::SEED as u64);
assert_eq!(h, hash(&inv.to_le_bytes(), crate::murmur::SEED as u64));
assert_ne!(h, hash(&inv.to_be_bytes(), crate::murmur::SEED as u64));
}

View file

@ -150,6 +150,12 @@ impl fmt::UpperHex for Murmur32 {
} }
} }
impl fmt::LowerHex for Murmur32 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::LowerHex::fmt(&self.0, f)
}
}
impl fmt::Display for Murmur32 { impl fmt::Display for Murmur32 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:08X}", self) write!(f, "{:08X}", self)
@ -218,148 +224,3 @@ impl<'de> Deserialize<'de> for Murmur32 {
deserializer.deserialize_any(Self(0)) deserializer.deserialize_any(Self(0))
} }
} }
// This type encodes the fact that when reading in a bundle, we don't always have a dictionary
// entry for every hash in there. So we do want to have the real string available when needed,
// but at the same time retain the original hash information for when we don't.
// This is especially important when wanting to write back the read bundle, as the hashes need to
// stay the same.
// The previous system of always turning hashes into strings worked well for the purpose of
// displaying hashes, but would have made it very hard to turn a stringyfied hash back into
// an actual hash.
#[derive(Clone, Debug, Eq)]
pub enum IdString64 {
Hash(Murmur64),
String(String),
}
impl IdString64 {
pub fn to_murmur64(&self) -> Murmur64 {
match self {
Self::Hash(hash) => *hash,
Self::String(s) => Murmur64::hash(s.as_bytes()),
}
}
pub fn display(&self) -> IdString64Display {
let s = match self {
IdString64::Hash(hash) => hash.to_string(),
IdString64::String(s) => s.clone(),
};
IdString64Display(s)
}
pub fn is_string(&self) -> bool {
match self {
IdString64::Hash(_) => false,
IdString64::String(_) => true,
}
}
pub fn is_hash(&self) -> bool {
match self {
IdString64::Hash(_) => true,
IdString64::String(_) => false,
}
}
}
impl<S: Into<String>> From<S> for IdString64 {
fn from(value: S) -> Self {
Self::String(value.into())
}
}
impl From<Murmur64> for IdString64 {
fn from(value: Murmur64) -> Self {
Self::Hash(value)
}
}
impl From<IdString64> for Murmur64 {
fn from(value: IdString64) -> Self {
value.to_murmur64()
}
}
impl PartialEq for IdString64 {
fn eq(&self, other: &Self) -> bool {
self.to_murmur64() == other.to_murmur64()
}
}
impl std::hash::Hash for IdString64 {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write_u64(self.to_murmur64().into());
}
}
impl serde::Serialize for IdString64 {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(self.to_murmur64().into())
}
}
struct IdString64Visitor;
impl<'de> serde::de::Visitor<'de> for IdString64Visitor {
type Value = IdString64;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an u64 or a string")
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(IdString64::Hash(value.into()))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(IdString64::String(v.to_string()))
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(IdString64::String(v))
}
}
impl<'de> serde::Deserialize<'de> for IdString64 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_u64(IdString64Visitor)
}
}
pub struct IdString64Display(String);
impl std::fmt::Display for IdString64Display {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::fmt::UpperHex for IdString64 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::UpperHex::fmt(&self.to_murmur64(), f)
}
}
impl std::fmt::LowerHex for IdString64 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::LowerHex::fmt(&self.to_murmur64(), f)
}
}