Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,16 @@ berry-sigcheck: build/berry-sigcheck

# Self-check: the tool must read a build-id from a real ELF and match a pattern
# against it. Uses the tool's own binary as the ELF.
# The pattern is the 16-byte ELF identification header, not the bare 4-byte
# magic: a compiler may embed the magic as an immediate in its own code (clang
# does, in its generated loader checks), which makes the short pattern ambiguous.
# The OSABI byte (e_ident[7]) is wildcarded: it is 03 (GNU/Linux) with this
# toolchain but 00 (System V) with others, and pinning either breaks the other.
.PHONY: check-sigcheck
check-sigcheck: build/berry-sigcheck
@id=$$(./build/berry-sigcheck build/berry-sigcheck --build-id) || { echo "check-sigcheck: no build-id"; exit 1; }; \
[ -n "$$id" ] || { echo "check-sigcheck: empty build-id"; exit 1; }; \
./build/berry-sigcheck build/berry-sigcheck --pattern "7F 45 4C 46" >/dev/null || { echo "check-sigcheck: ELF magic did not match"; exit 1; }; \
./build/berry-sigcheck build/berry-sigcheck --pattern "7F 45 4C 46 02 01 01 ?? 00 00 00 00 00 00 00 00" >/dev/null || { echo "check-sigcheck: ELF identification header did not match"; exit 1; }; \
echo "== berry-sigcheck OK =="

# End-to-end check for the developer CLI. It drives the real binary through
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ See [docs/BUILD.md](docs/BUILD.md) for NDK details, Dobby CMake, and hook flags.

## Exported C ABI

`libBerryAPI.so` exports exactly these symbols (C, `extern "C"`):
`libBerryAPI.so` exports a small C surface (C, `extern "C"`) so a host that is
not linked against BerryAPI's C++ ABI can drive it through `dlopen` + `dlsym`.

Core lifecycle:

```c
bool BERRY_Inject();
Expand All @@ -88,6 +91,20 @@ int BERRY_ShimUnpatch(void* target);
bool BERRY_ShimCanPatch();
```

Feature surfaces, each a plain C struct of function pointers with a `size` and
an `abi_version` the host checks before calling a pointer, and each obtained
from a `BERRY_Get*Api()` that returns null before attach:

| surface | header | getter |
|---------|--------|--------|
| world seam | `berry/seam.h` | `BERRY_SetWorldSeam` |
| replay | `berry/replay_c.h` | `BERRY_GetReplayApi` |
| performance governor | `berry/governor_c.h` | `BERRY_GetGovernorApi` |
| cosmetics | `berry/cosmetic_c.h` | `BERRY_GetCosmeticApi` |

The cosmetic surface also carries the player-model hook
(`set_player_model_hook`), which is what makes cosmetics resolve to a real rig.

`BerryConfigC` is a **plain C struct** (see `include/berry/config.h`) that starts
with `uint32_t size; uint32_t abi_version;`. The library validates both and
rejects mismatches at attach time (`BERRY_ABI_VERSION` in `version.h`).
Expand Down
99 changes: 65 additions & 34 deletions core/client/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -568,29 +568,10 @@ void BerryClient::BuildPacketCapture(const ClientConfig& cfg) {
}

void BerryClient::BuildCosmetics(const ClientConfig& cfg) {
// Cosmetic asset data loads first: a pack names geometry/texture handles, so
// the handles must exist before the pack's assets are registered (otherwise a
// pack asset would resolve to a retired handle and draw nothing).
if (!cfg.cosmeticAssetDirectory.empty()) {
cosmeticAssets_ = std::make_unique<CosmeticAssetLoader>(
fs_.get(), cfg.cosmeticAssetDirectory);
const auto report = cosmeticAssets_->LoadDirectory();
Log::Info("BerryClient: cosmetic assets: %zu meshes, %zu textures, "
"%zu refused%s",
report.meshes, report.textures, report.failed,
report.truncated ? " (truncated)" : "");
}

// Model packs register into the catalog the wardrobe reads. Loading after the
// assets means a pack's handles resolve; a pack that names a handle no asset
// supplied still registers (the asset simply draws nothing), which keeps a
// missing texture from discarding a whole cape.
if (!cfg.cosmeticPackDirectory.empty()) {
packs_->SetRoot(cfg.cosmeticPackDirectory);
const PackLoadReport report = packs_->LoadDirectory();
Log::Info("BerryClient: cosmetic packs: %zu loaded, %zu refused%s",
report.packsLoaded, report.packsFailed,
report.truncated ? " (truncated)" : "");
// Asset data + model packs: one code path with the runtime reload the C ABI
// host drives, so attach and reload can never drift.
if (!cfg.cosmeticAssetDirectory.empty() || !cfg.cosmeticPackDirectory.empty()) {
ReloadCosmeticPacks(cfg.cosmeticPackDirectory, cfg.cosmeticAssetDirectory);
}

// Author the built-in clips into the animator, so the built-in cape flaps and
Expand All @@ -605,23 +586,73 @@ void BerryClient::BuildCosmetics(const ClientConfig& cfg) {
// leaves the skeleton source fail-closed (nothing resolves), which is the
// correct default for a build whose layout is unknown.
if (cfg.playerModelRenderTarget && cfg.playerModelBones.valid()) {
playerModelSkeleton_->SetReader(cfg.playerModelBones);
playerModelHook_ = std::make_unique<PlayerModelHook>();
playerModelHook_->SetReader(cfg.playerModelBones);
if (playerModelHook_->Install(cfg.playerModelRenderTarget,
HookManager::Instance().Backend())) {
Log::Info("BerryClient: player-model hook installed");
} else {
Log::Warn("BerryClient: player-model hook refused; cosmetics resolve no "
"bones");
playerModelHook_.reset();
}
InstallPlayerModelHook(cfg.playerModelRenderTarget, cfg.playerModelBones);
} else {
Log::Info("BerryClient: no player-model target/reader; cosmetics resolve "
"no bones (fail closed)");
}
}

size_t BerryClient::ReloadCosmeticPacks(const std::string& packDir,
const std::string& assetDir) {
// Asset data first: a pack names geometry/texture handles, so the handles
// must exist before the pack's assets are registered, or a pack asset would
// resolve to a retired handle and draw nothing. This mirrors BuildCosmetics.
if (!assetDir.empty()) {
if (!cosmeticAssets_) {
cosmeticAssets_ = std::make_unique<CosmeticAssetLoader>(fs_.get(), assetDir);
}
cosmeticAssets_->LoadDirectory(assetDir);
}
size_t registered = 0;
if (!packDir.empty()) {
if (!packs_) packs_ = std::make_unique<CosmeticPackLoader>(fs_.get());
packs_->SetRoot(packDir);
const PackLoadReport report = packs_->LoadDirectory(packDir);
registered = report.assetsRegistered;
Log::Info("BerryClient: cosmetic reload: %zu packs loaded, %zu refused, "
"%zu assets",
report.packsLoaded, report.packsFailed, report.assetsRegistered);
}
// A reload can change the catalog the wardrobe reads, so drop any cached
// resolve: a cached item composed against a now-retired asset would be stale.
if (cosmetics_) cosmetics_->ClearResolveCache();
return registered;
}

bool BerryClient::InstallPlayerModelHook(void* target,
const PlayerModelBoneReader& reader) {
if (target == nullptr) return false;
if (!reader.valid()) return false;
if (playerModelHook_) return false; // already installed; clear it first
if (!playerModelSkeleton_) return false;

// The hook refreshes the reader's cache from the rendered pose, so the
// skeleton source resolves bones the game actually computed. Bind the source
// first, so a bone is resolvable the instant the detour is live.
playerModelSkeleton_->SetReader(reader);
auto hook = std::make_unique<PlayerModelHook>();
hook->SetReader(reader);
if (!hook->Install(target, HookManager::Instance().Backend())) {
// A refused install leaves the source bound to a reader nothing refreshes;
// clear it so the source fails closed rather than reading a stale cache.
playerModelSkeleton_->SetReader(PlayerModelBoneReader{});
Log::Warn("BerryClient: player-model hook refused; cosmetics resolve no bones");
return false;
}
playerModelHook_ = std::move(hook);
Log::Info("BerryClient: player-model hook installed (runtime)");
return true;
}

void BerryClient::ClearPlayerModelHook() {
if (playerModelHook_) playerModelHook_->Uninstall();
playerModelHook_.reset();
// Unbind the reader too: with no hook refreshing it, the cache would go
// stale, and the correct fail-closed state is "resolve nothing".
if (playerModelSkeleton_) playerModelSkeleton_->SetReader(PlayerModelBoneReader{});
}

void BerryClient::RegisterBuiltinClips() {
if (!animator_) return;
// The cape's flap: the back bone lifts and rolls a little over a loop. Keys
Expand Down
8 changes: 8 additions & 0 deletions core/lifecycle/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ bool Engine::Bootstrap(const BerryConfigC& cfg) {
if (cfg.script_data_directory) {
internal.scriptDataDirectory = cfg.script_data_directory;
}
if (cfg.cosmetic_pack_directory) {
internal.cosmeticPackDirectory = cfg.cosmetic_pack_directory;
}
if (cfg.cosmetic_asset_directory) {
internal.cosmeticAssetDirectory = cfg.cosmetic_asset_directory;
}
ApplyConfig(internal);
Log::Info("BerryAPI v%s bootstrapped (abi %u)", BERRY_VERSION_STRING,
(unsigned)BERRY_ABI_VERSION);
Expand Down Expand Up @@ -242,6 +248,8 @@ bool Engine::Attach() {
ccfg.voiceRoomSecret = cfg_.voiceRoomSecret;
ccfg.lanIface = cfg_.lanIface;
ccfg.scriptDataDirectory = cfg_.scriptDataDirectory;
ccfg.cosmeticPackDirectory = cfg_.cosmeticPackDirectory;
ccfg.cosmeticAssetDirectory = cfg_.cosmeticAssetDirectory;
client_ = std::make_unique<BerryClient>();
if (!client_->Attach(*this, ccfg)) {
Log::Error("BerryClient failed to attach; detaching modules");
Expand Down
6 changes: 6 additions & 0 deletions core/modules/cosmetic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,12 @@ bool CosmeticService::Unequip(CosmeticSlot slot) {
return local_.Unequip(slot);
}

void CosmeticService::ClearLocalLoadout() {
std::lock_guard<std::mutex> lk(mutex_);
local_.Clear();
animClock_.store(0.0f);
}

std::string CosmeticService::Equipped(CosmeticSlot slot) const {
std::lock_guard<std::mutex> lk(mutex_);
return local_.Equipped(slot);
Expand Down
Loading
Loading