- npc.profiles answers with RunicNPC's faction table, and npc.profiles.set
pushes one beside the profiles (RunicNpc_SetFactions), set first so the
profiles are checked against it; a refused table comes back as
factionsRefused and the old one is kept (D254);
- world.place with a profile takes escort (a Steam id; the player must be
on, else escort-offline), ally ({kind: clan|player, id}) and tether (the
run's own zone that holds the point, else no-zone), and gives each NPC
its orders through RunicNpc_Escort, _Ally and _Tether. All or nothing:
an order RunicNPC refuses removes every NPC the step placed (D269, D270,
D272);
- the RunicNPC API the bridge needs is 4 (overlay.toml runicnpc_api = 4).
Compiled on the Oxide rig.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
overlay.toml gains runicnpc_api = 3, the release folds it into manifest.json,
and checkPlugin.js holds it equal to RunicNpcApiNeeded in the code. The
installer's bundle carries the latest RunicNPC release that answers it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Protocol 13 gains npc.profiles, npc.profiles.set, npc.placements and
npc.placement (add, set, remove, rename, respawn), each calling RunicNPC's
API 3; the frames npc.died, npc.health and npc.placement.changed from its
hooks; runicNpc in the integrations body; world.place with a profile; the
killfeed's attackerNpc and attackerProfile; and the tally's
npcProfileKills. rg.npc reports it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
world.zone gains flags, settings, enterMessage, leaveMessage, delivery,
format and dome (PROTOCOL §19.11). Flags are checked against the live
ZoneManager's own list, read through its API call ZoneFieldListRaw() (D211),
and refused bad-option rather than dropped; settings are an allowlist of
five with bounds. Both are kept on the registry entry and re-applied when
a zone is re-created.
The bridge says a zone's enter and leave messages itself (D193), as chat
or a popup (chat when PopupNotifications is missing), and stays quiet for
5 s after re-creating a zone.
RunicGatewayDomes.cs, a new helper, patches ZoneDomes so its API can be
called without a player, and raises OnRgDomesReady after ZoneDomes' own
start; the bridge puts its domes back only after it, so none is drawn
twice. Domes are removed before their zone at expiry and revert.
hello carries zoneManager { version, flags }, integrations.zoneDomes the
ZoneDomes and helper state, and hello is sent again when ZoneManager,
ZoneDomes or either helper loads or unloads. The release ships the new
helper beside the zone helper.
Not compiled or walked on a rig: the panel's file API was down on both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Compiled on both rigs (2026-09-29): ItemCraftTask has no owner, so the crafter is ItemCrafter.owner; OnMissionSucceeded credits its third argument, the assignee, since MissionInstance has no accessor for it; OnHealingItemUse takes its target as object, because GiveEffectsTo now takes an IMedicalToolTarget, and counts only a player. GameObject.ToBaseEntity is not in scope for a plugin, so OnEntityBuilt asks GetComponent<BuildingBlock>().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
player.tally gains animalKills, weaponKills (by the killfeed's weapon
prefab name), headshots, bestPvpM/bestPveM (this interval's maximum),
apcKills, heliKills, crafted (by item category), built, repaired, healed,
rockets, explosives and missions. Every field is absent when nothing moved.
Ten new void hooks, all in ExpectedHooks: OnEntityTakeDamage (the last
player to hurt an APC or helicopter, for a crash that names nobody),
OnItemCraftFinished, OnEntityBuilt, OnStructureRepair (counted only when
the health rose by the next tick), OnHealingItemUse and OnPlayerRevive
(another player only; a syringe revive counts once), OnRocketLaunched,
OnExplosiveThrown/Dropped (a damaging TimedExplosive only) and
OnMissionSucceeded.
npcKills keeps counting animals too, so the public column does not change
meaning mid-wipe; the site takes human NPCs as the difference (D209).
WeaponOf and DistanceOf are split out of DescribeAttacker so the killfeed
and the tally read one name and one distance.
Not yet compiled on a rig: the rigs' storage is unmounted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The permission manager (PLAN_REDESIGNS section 1, D160): the site owns every
permission and group on the server, so the bridge now reads all of it.
- perm.inventory: every registered permission with the plugin that
registered it (PermissionExists(name, plugin), public on both
frameworks), every group with title, rank, parent and permissions,
every holder's direct permissions and groups (not default), and the
pairs event leases hold. Built a slice per tick, sent in pages under
the 1 MiB line cap, paged from one snapshot. rg.inventory logs it.
- perm.sync sets an existing group's title and rank and every group's
parent; an absent title or rank means "leave it" (the adopt policy's
hold). The managed list and the foreign scan are gone: the site reads
the store and decides.
Walked on both rigs: Oxide 85 permissions all owned, Carbon 104 with its
30 adminmodule.* unowned; ZoneManager owns ignoreflag.nokits.
Refs RunicGateway/Module-Rust#21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Found compiling on the rust-oxide rig: BasePlayer.activePlayerList is
Facepunch's ListHashSet, which has no ToArray, so the helper did not compile.
It now copies into a List, which it needs anyway: entering a player can eject
or kill them and change the live list mid-loop. HarmonyLib itself resolved on
Oxide with no reference directive, and the helper patched ZoneManager 3.1.14 on
both frameworks once this compiled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The bridge's half of PLAN_FIXES §6 step 2 (decisions D181-D185, docs#288).
- F13: `world.expired` carries the entity's kind as `what`. It used to write
it over the frame's own `kind`, so every expiry left filed as "zone".
- F1 (D159): `gathered` also counts OnDispenserBonus (the final-hit bonus),
OnCollectiblePickup (hemp, mushrooms, stones off the ground) and
OnGrowableGathered (farmed plants); all three are void and in ExpectedHooks.
- F3: taking down a building block you own, or one on a cupboard you are
authorised on, no longer counts toward `structures`. The frame still goes.
- F4: every remaining UserIDString is SteamIdOf(userID), so a player another
plugin spawned is never `steamId: null`.
- F8 (D184): `plugin.loaded` / `plugin.unloaded`, once the world is ready,
with the permissions that plugin added or removed (a diff of the registered
list, taken a tick after the hook).
- F12 (D181, D182): RunicGatewayZones.cs, a D168 helper shipped beside the
bridge, Harmony-postfixes ZoneManager's Zone.InitializeZone and, a tick
later, enters every connected player ZoneManager's own IsPositionInZone
places inside, through ZoneManager's own OnPlayerEnterZone. Without it (or
when it cannot find what it patches) the bridge scores its zones by
IsPositionInZone. Hello reports `zoneHelper: { state, version?, reason? }`.
- release.yml stages every helper beside the bridge and lists each .cs with
its sha256 in manifest.files; the installer and the egg place them all.
Not yet compiled: this is walked on both rigs before it merges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The step-1 rig walk (PLAYER_WALK configuration 2, 2b, 4 on both rigs) found:
- W3: Carbon's Oxide-compatible ReloadPlugin is not a reload. One call unloaded
Kits and never compiled it again; the next did nothing. Carbon now reloads
with `c.reload` on the server console (ConsoleSystem.Run), as an operator
would at the panel. Oxide keeps ReloadPlugin, falling back to `oxide.reload`.
- W4: Carbon's log is carbon/logs/Carbon.Core.log, with no *.txt in the
directory at all, so the log evidence never fired on Carbon. Chosen by name.
- W5: Carbon loads a plugin whose LoadConfig threw and raises Loaded in the
same instant. OnPluginLoaded is now noted, and the job settles on the first
log read a poll after it. Carbon's "Failed ILoadConfig" is on the word list.
- W2: Oxide's "Could not initialize plugin 'Kits v4.4.9' (JsonReaderException:
...)" is on the word list, so the outcome quotes the exception.
- W1: Oxide's idle compiler shutdown drops a queued reload with "Timed out
waiting for plugin to be compiled: X". The reload is asked for once more;
a second give-up settles at once by what is loaded.
rg.config prints the log file it follows (log=).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
F15: the read loop cast each byte to a char, decoding the site's UTF-8 as
Latin-1, so every non-ASCII announcement, title, kit name and saved config
file arrived mangled. Lines are now split as bytes and decoded whole, which
keeps a character that straddles two reads intact.
F9/F10 (D177, D178): config.write answers at once with pending, writeId and
ceilingMs, and the outcome follows as a config.outcome event. A pending
reload reads the framework log each second and rolls back the moment the
target fails to compile or initialise; a slow compile is waited for, under
a 30 s ceiling per phase. At the ceiling the plugin looks at what is loaded
before restoring anything, and a rollback waits for the restore's own
reload so the outcome says whether the plugin is back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The reconnect backoff waited on `Wake`, which `Emit` signals on every
frame, so it lasted only until the next hook fired. That was invisible
while a failed connect was silent; against a sidecar that now refuses a
plugin naming another server (D155) the phase 18 walk measured some 250
redials a second, each printing two console lines and rebuilding the
boards. It now waits on `_stopping`, the handle that already exists so a
CONNECT is not abandoned by `Emit`; Unload still ends the wait at once.
The backoff also resets only after a link has held for 10 s
(StableLinkMs). A connection closed on arrival used to reset it to 500 ms
on every accept, so a refusal loop never backed off at all.
Walked: a plugin aimed at another server's sidecar is refused every 5 s
(the ceiling), not continuously; a stable link whose sidecar restarts is
back in 0.49 s, as before; no Unload stall on reload.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Init() started LinkLoop before Loaded() read the config, so the thread's
first dial saw _config == null and used the 7799 fallback on both Oxide and
Carbon. One server per host made that its own sidecar by coincidence. With
two on a host (the installer's named instances, phase 18), the second
server dialled the FIRST server's sidecar, sat in its accept backlog, and
the moment the first server's plugin reloaded that sidecar took the second
server's connection: the website showed server "alpha" with beta's
hostname and wipe.
LoadConfigValues() now runs in Init() before the thread starts; Loaded()
keeps its log line and the lease/world/reward loads.
Walked on the phase 18 rig: three instances (two Oxide, one Carbon 7801),
each now dials its own port.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Module-rust phase 18, step 3 of docs/modules/rust/PLAN.md §34.2.7.
The environment seam (D150): when the plugin writes its default config and
RUSTLINK_SERVER_ID holds a valid id (the site's ^[a-z0-9][a-z0-9-]{0,63}$),
the file's ServerId is that value. It is read nowhere else: after that write
the file is canonical and the site's lock applies. An invalid value is logged
and ignored. This is how the egg names a server; not a message change, so the
protocol stays at 12.
The release (D145, §34.2.1): release.yml reuses servuo-plugins' release engine
unchanged (tag-only, secrets preflighted, orphan tags recovered and swept, the
installer's bundle.yml dispatched at the end). The adapter re-runs
checkPlugin.js and packages runicgateway-rust-plugin-<version>.tar.gz with a
framework-neutral layout: RunicGateway.cs, its [Info] version stamped to the
release, and a manifest.json folding in overlay.toml.
overlay.toml gains min_carbon_version = "2.0.259", the build R19 was proven on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The walk found that a hot reload of this plugin emptied every title, and
the site never re-sent them: it re-sends on a new boot id or wipe id, and a
reload is neither, because the boot id names the server process. The set
is now written to titles.json with the wipe it was pushed on, and read at
OnServerInitialized only when that wipe is still the current one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
- titles.set: a whole set of chat titles, swapped in by reference and read
by a BetterChat third-party title getter (one lookup, no socket). The
getter is registered at load and again whenever BetterChat loads.
- perm.sync: a group may carry its BetterChat style. Each field is written
through API_SetGroupField only when the game still holds what the site
last pushed (or BetterChat's default); anything else is reported as
drift. One `chat group set` per touched group saves it, because the API
never does (D143). A retired style removes the group with `chat group
remove` (D139). The report gains `chat`.
- chat.say: delivery chat|popup and an optional voice format. A popup with
PopupNotifications absent is refused `popup-unavailable`. Every line said
is logged, since the game logs neither a broadcast nor a popup.
- server.hello/status carry `integrations`; `rg.titles [steamId]` prints
what BetterChat would render for a player.
- overlay.toml: protocol 12; PopupNotifications leaves requires_plugins
(D141).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The phase 14 walk read `steamId: null` for every sleeper made on the
server: UserIDString is a cached string the game fills for a player it has
seen connect. An id the map cannot name is an own dot and a clan mate the
site can never match, so the map reads the id from userID itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Four verbs (PLAN.md §30.2), all outside EventsEnabled because none of them
changes the world:
- map.info: the map key, the picture's source (the Rust+ cache, a render
kept on disk, or none), its size, SHA-256 and slice count, the game's own
grid, and the monuments the in-game map shows. The cache is read by
reflection by name only, never enumerated (it holds the Rust+ secret).
The hash is computed on the thread pool.
- map.fetch: one 512 KiB base64 slice, encoded on the thread pool, refused
`stale` when the key or hash moved.
- map.render: the admin fallback (D109). Answers `accepted` at once and
renders on a later frame, keeping the result in data/RunicGateway/map.jpg
beside its key so a restart within the map does not render again.
- map.live: every layer at once (world events, event-owned zones/crates/NPCs,
players and sleepers, TCs and player vending machines as positions only).
The sets are kept by OnEntitySpawned/OnEntityKill and seeded by one sweep
at OnServerInitialized. MapMaxSleepers/MapMaxBases bound the two layers.
Also fixes Grid() (D119): it used a 146.3 m cell, but the game fits whole
cells to the world (20 x 150 m on a 3000 map), so rows could be one off.
It now asks MapHelper.PositionToString, the in-game map's own labeller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
- tally.open / .snapshot / .close: a run's participation tally, persisted
in tallies.json. Presence accrues every 5 s from ZoneManager's own
membership (or the whole server); kills are the last hit, gated by
killsOf and the killer being inside the area (D98); the score is
seconds, kills, or minutes + killWeight x kills (D99). Only a zone this
run opened counts (D100). Gated by EventsEnabled (D107); forgotten
seven days after opening (D108).
- kits.list: the kit source, from Kits' GetKitNames/GetKitInfo.
- perm.sync `credits`: one extra use of a kit per reward (D103), held in
credits.json and spent LAST from OnKitRedeemed, because Kits'
SetPlayerKitUses does nothing for a player with no usage record.
Withdrawn when unredeemed; reset when Kits wipes its use counts.
- chat.say: one line to the server, remembered by key for 10 minutes.
- EventsMaxRecipients / EventsMaxTallies / EventsMaxChat bounds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The link starts in Init, before the save loads. The phase 13a walk restarted
the rig mid-run: the website saw the new boot id at once, asked world.owned
into an empty world, and the plugin — finding no entity behind any net id —
pruned three live crates from its own registry. They stayed in the world,
owned by nothing, and a revert would have refused them.
world.owned, world.revert, world.monuments and every world write now answer
`not-ready` until OnServerInitialized, zones are not re-created before it,
and server.hello carries `worldReady` so the website can hold its reconcile
until the world it would be asking about exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
world.monuments, world.zone, world.place, world.revert and world.owned
(PLAN.md §28, D86-D96):
- Zones through ZoneManager's temporary zones, owned by the bridge, with a
required duration the plugin enforces itself (D96) and re-created from the
bridge's own registry on every load and every ZoneManager reload (D92),
because ZoneManager erases a plugin's zones when that plugin unloads.
- Crates and NPCs from a plugin-held allowlist (D88) at a monument by kind
and instance, or at raw coordinates (D87, D93), bounded 25/20 per step (D95).
- A registry file keyed by the website's idempotency key, since this bridge
has no at-most-once store: a repeat answers with the first call's ids.
- world.owned LOOKS by net id: crates survive a restart, NPCs do not, so a
boot id proves nothing. A wipe drops the registry whole.
- EventsEnabled gates every world write; revert, owned and monuments never
(D94). Revert refuses only a thing that exists and is not this run's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
EnsureGrant and EnsureGroupPermission passed `this` as the owner. Given an
owner, Oxide first asks PermissionExists(perm, owner) — whether THAT plugin
registered the name — and returns silently when it did not, so every
site-authored grant of another plugin's permission was a no-op on Oxide that
the report counted as applied (PLAN.md §27.6).
Both now pass null and read the store back. A grant the store does not hold
afterwards is reported in the new `notLanded` list instead of counted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
lease.list / lease.apply / lease.release and a lease.expired event. The plugin holds the allowlist (decay.scale, 18 populations, the two minimum spawn scalars, and group permissions), the seven-day ceiling (refused, never clamped) and the deadline, which it enforces on its own. Release is compare-and-set. Holds persist in the data directory with their boot: a restart ends a convar hold, while a group-permission hold survives it and is re-armed.
EventsEnabled (default off) gates apply only, and a write from the site's config editor now takes effect at once. The permission mirror defers to a leased pair: it is reported as leased, never foreign.
Grants pass a null owner: with an owner, Oxide silently refuses any permission another plugin registered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
UserIDString is filled in only when a player connects, loads as a
sleeper, or is an engine bot. A player another plugin spawned has none,
and the phase-10 walk's raid arrived with attackerId null. The website
skips the raid alert when the attacker is authorised on the cupboard,
so a null would quietly defeat that exclusion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The website tells a disbanded clan's members, but the clan board that
follows the event (and, after an outage, precedes the backlog) has
already removed that roster from its store. The game walks Members to
drop each membership and never empties the list, so it is still whole
when OnClanDisbanded fires. Part of protocol 7.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
entity.destroyed now covers doors, external walls and gates, and the
tool cupboard itself as well as building blocks. A door was never a
BuildingBlock, so a door-only raid reached nobody. The frame gains
`structure`, the cupboard's `buildingId`, and `authorized`: each Steam
id on the cupboard with whether they are online now, bounded at 64.
Both fields are absent when there is no cupboard. The structures tally
still counts building blocks only, so the leaderboard column keeps its
meaning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The `clans` board carries every clan the game will list, with its roster
and roles, plus the facts the website needs to trust it: enabled,
backend, supported, truncated, and whether the optional uMod Clans
plugin is loaded. It is re-sent on connect and on the 60-second cadence,
and a few seconds after any membership change. The five events are
clan.created, clan.disbanded, clan.member.added, clan.member.left and
clan.member.kicked.
What the game's assemblies dictated:
- The game has no enumerate-all call. The clan leaderboard is the only
listing, and the game clamps it to 100 clans. A board at that ceiling
says `truncated`, as does one that would not fit in 768 KiB (the
sidecar drops lines over 1 MiB).
- The local backend is synchronous behind ValueTask. A task that has not
completed means another backend (Nexus), which is refused with a reason
rather than waited on from the main thread.
- A clan id is not an identity, so every frame carries createdMs.
- OnClanMemberAdded hands over a bare id before the game's cache
refreshes, so the clan is read back, with a fallback to the id alone.
- uMod Clans raises OnClanDisbanded(string, List<ulong>), which has the
same name and arity as the game's hook, so the bridge types it exactly.
A member's LastSeen is never sent: it is presence (PLAN.md §23).
overlay.toml moves to protocol 6 and drops Clans from requires_plugins
(D47): Teams come from the game's own clans.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
R18. Three request/reply verbs — `config.list`, `config.read`, `config.write` —
and the first command on this bridge that writes to the game host's filesystem.
The walk is rooted at `Interface.Oxide.ConfigDirectory`, never a literal, so it
is right on Oxide, on Carbon and on a server whose operator moved it; the data
directory is never walked, because it holds live state and both frameworks'
permission stores, which is protocol 4's mirror one directory over.
A write is resolved, version-checked and parse-checked before the first byte
lands, then backed up, written and reloaded — and the reload is WATCHED. If
`OnPluginLoaded` does not arrive within four seconds the whole set is restored,
reloaded again, and reported with the tail of the newest log file. Without that
this is a web form that takes a required plugin off a production server one typo
at a time.
Four seconds is arithmetic, not taste: the worst path is two windows while a
caller holds the sidecar's ten-second RPC budget.
This plugin will not reload itself — the reload would close the link carrying
the answer — and the site composes the file's bytes, so nothing here has an
opinion about content beyond "it parses".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM