dredless 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/dredless.global.js +241 -34
- package/dist/browser/dredless.global.js.map +3 -3
- package/docs/PROTOCOL.md +5 -0
- package/docs/README.md +1 -0
- package/docs/WORLD_STATE.md +71 -0
- package/package.json +1 -1
- package/src/client/domains/index.js +34 -0
- package/src/index.d.ts +107 -1
- package/src/state/model/decoder.js +129 -39
- package/src/state/model/index.d.ts +7 -0
- package/src/state/model/state.js +53 -4
- package/src/state/world/index.d.ts +6 -0
- package/src/state/world/state.js +5 -0
package/docs/PROTOCOL.md
CHANGED
|
@@ -211,6 +211,11 @@ worldState.tileDefinition(material: number): TileDefinition | null;
|
|
|
211
211
|
modelState.itemHolders(): ItemHolderSummary[];
|
|
212
212
|
modelState.fabricators(): FabricatorSummary[];
|
|
213
213
|
modelState.players(): PlayerSummary[];
|
|
214
|
+
modelState.projectiles(): ProjectileSummary[];
|
|
215
|
+
modelState.bots(): BotSummary[];
|
|
216
|
+
modelState.rcds(): RcdSummary[];
|
|
217
|
+
modelState.balls(): BallSummary[];
|
|
218
|
+
modelState.recyclers(): RecyclerSummary[];
|
|
214
219
|
modelState.setLocalPlayerEntity(entity: number | null): void;
|
|
215
220
|
modelState.shipControls(): ShipControlSummary[];
|
|
216
221
|
modelState.shipMetadata(): ShipWorldMetadataSummary | null;
|
package/docs/README.md
CHANGED
|
@@ -19,6 +19,7 @@ In a browser, `BrowserSession` uses the ambient browser cookie jar.
|
|
|
19
19
|
| Handle disconnects and retain useful state | [Client lifecycle](CLIENT.md) |
|
|
20
20
|
| Subscribe to model, inventory, or management updates | [Event catalog](EVENTS.md) |
|
|
21
21
|
| Find an entity, machine, or nearby ship | [World and entity reads](WORLD_STATE.md) |
|
|
22
|
+
| Read projectiles, bots, balls/eggs, RCDs, or recyclers | [Special entity reads](WORLD_STATE.md#projectiles-bots-and-special-entities) |
|
|
22
23
|
| Move, aim, equip, or place a blueprint | [Player and inventory](PLAYER_INVENTORY.md) |
|
|
23
24
|
| Use a context menu or set door access | [Machines and context menus](MACHINES.md#context-menus-and-access) |
|
|
24
25
|
| Configure a loader, pusher, launcher, or navigation unit | [Machine reference](MACHINES.md) |
|
package/docs/WORLD_STATE.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
- [Choose a world](#choose-a-world)
|
|
8
8
|
- [Entities and features](#entities-and-features)
|
|
9
9
|
- [Machine and other collections](#machine-and-other-collections)
|
|
10
|
+
- [Projectiles, bots, and special entities](#projectiles-bots-and-special-entities)
|
|
10
11
|
- [Nearby ships](#nearby-ships)
|
|
11
12
|
- [Live reads and snapshots](#live-reads-and-snapshots)
|
|
12
13
|
- [Coordinates and incomplete data](#coordinates-and-incomplete-data)
|
|
@@ -105,6 +106,76 @@ not freeze them.
|
|
|
105
106
|
Materials summarize decoded blocks; they are not an authoritative ship inventory. A missing collection or component can
|
|
106
107
|
reflect incomplete decoding, not an absent server object.
|
|
107
108
|
|
|
109
|
+
## Projectiles, bots, and special entities
|
|
110
|
+
|
|
111
|
+
Every world domain exposes these collections. They return current component summaries; `get()` returns `null` when
|
|
112
|
+
the entity or requested component is absent. Use `world.entities.get(summary.entity)` for position, health, snapshots,
|
|
113
|
+
and generic interactions.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
world.projectiles.all(): ProjectileSummary[];
|
|
117
|
+
world.projectiles.get(entity: EntityRef): ProjectileSummary | null;
|
|
118
|
+
world.bots.all(): BotSummary[];
|
|
119
|
+
world.bots.get(entity: EntityRef): BotSummary | null;
|
|
120
|
+
world.rcds.all(): RcdSummary[];
|
|
121
|
+
world.rcds.get(entity: EntityRef): RcdSummary | null;
|
|
122
|
+
world.balls.all(): BallSummary[];
|
|
123
|
+
world.balls.get(entity: EntityRef): BallSummary | null;
|
|
124
|
+
world.recyclers.all(): RecyclerSummary[];
|
|
125
|
+
world.recyclers.get(entity: EntityRef): RecyclerSummary | null;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
These arrays also appear in world/model snapshots. Standalone models expose `projectiles()`, `bots()`, `rcds()`,
|
|
129
|
+
`balls()`, and `recyclers()`. Entity handles recognize the `projectile`, `bot`, `rcd`, `ball`, and `recycler` features.
|
|
130
|
+
An egg has public type `"egg"` and a `ball` component with `isEgg: true`; the balls collection includes both kinds.
|
|
131
|
+
|
|
132
|
+
| Component | Exposed state |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| `projectile` | RGB `color`/`colorCss`, `botStyle`, `indicatorCount`, `spriteItemId`/`spriteItemName`, raw `state`. |
|
|
135
|
+
| `bot` | Existing class/identifier fields, `classificationSource: "heuristic"`, health, nullable combat display state, and nullable `beams`. |
|
|
136
|
+
| `rcd` | Sandbox/flux variant and variant code, corresponding item ID, stored item identity/count, beam vector, raw `state`. |
|
|
137
|
+
| `ball` | Nullable `health`, radius in world units, `rubber`, `isEgg`, raw `state`. |
|
|
138
|
+
| `recycler` | Stored item identity/count. Identified from the recycler marker in a ship interior. |
|
|
139
|
+
|
|
140
|
+
Projectile `botStyle` selects the official bot-bullet sprite; it does not prove ownership or identify the shooter.
|
|
141
|
+
`indicatorCount` is the number of orbiting visual markers. `spriteItemId` is the item image used by the official
|
|
142
|
+
renderer when present, not a reliable identification of the ammunition or its damage. Projectile lifetimes follow
|
|
143
|
+
normal entity updates/removals; no local projectile simulation or trajectory prediction is performed.
|
|
144
|
+
|
|
145
|
+
`bot.combat` exposes packed RGBA `bodyColorRgba` and `weaponColorRgba`, `aimAngle` in radians, `thrusterMask`,
|
|
146
|
+
`alternateSprite`, `beamEffectEnabled`, and raw combat `state`. The official disabled-angle sentinel becomes `null`.
|
|
147
|
+
RGBA uses `0xRRGGBBAA`; projectile and outfit colors use RGB. Thruster mask bits select visual thrusters and do not
|
|
148
|
+
provide thrust force. Beam enablement describes an effect, not whether a target is taking damage.
|
|
149
|
+
|
|
150
|
+
`bot.beams` and the entity's `combatBeams` feature expose the same replicated three-beam component:
|
|
151
|
+
`{entity, beams: [{angle, length, active, colorRgba}, ...], state}`. Angles are radians relative to the entity's
|
|
152
|
+
orientation; lengths are in world units, and `active` means the length is positive. The renderer begins these beams
|
|
153
|
+
at the object's radius, so `length` is the extension beyond that radius. Wire angles are divided by `1000`, lengths
|
|
154
|
+
by `10`. The beam component is decoded separately from expando-box sizing.
|
|
155
|
+
|
|
156
|
+
Bot class names remain inferred from component combinations and values, rather than an authoritative server type.
|
|
157
|
+
Entities with health alone are no longer automatically treated as bots. Projectiles, balls, and RCDs are excluded
|
|
158
|
+
from bot/crate inference. Server AI decisions, target identity, and weapon damage are not inferred from display fields.
|
|
159
|
+
|
|
160
|
+
Ball identification uses the decoded ball component, including objects described in-game as boulders. It does not
|
|
161
|
+
depend on a particular HP value or the previous boulder exclusion heuristic. Balls may have health, including rubber
|
|
162
|
+
balls; `health` is `null` only when that component is absent. `radius` comes from the size component divided by `20`.
|
|
163
|
+
`rubber` is `true` for rubber and `false` for rock. The official renderer tints rubber dark gray (`#222222`) and
|
|
164
|
+
rock white. This flag is independent of health/destructibility.
|
|
165
|
+
|
|
166
|
+
An RCD's `beam` is `{offsetX, offsetY, active}`. The vector uses world axes and world units, scaled from the wire by
|
|
167
|
+
`1/5`; its endpoint is the RCD's position plus that vector. The official renderer chooses sandbox for variant code
|
|
168
|
+
zero and flux otherwise. Stored items are reported without assuming that every item is valid fuel or assigning a
|
|
169
|
+
fuel capacity. Generic `entity.use()`/tip interactions remain available; there is no dedicated RCD programming API.
|
|
170
|
+
|
|
171
|
+
Recyclers expose the replicated held item without inventing a processing timer. Body/appearance-only entities and
|
|
172
|
+
other special objects may still have generic handles and raw component tables without a dedicated summary. An
|
|
173
|
+
annihilator is a block material in the official client, so it is represented through tiles rather than this collection.
|
|
174
|
+
|
|
175
|
+
Footprints and the block index are coarse occupancy aids, not collision polygons or navigation meshes. Check
|
|
176
|
+
`footprint.source`: `"default"` and `"heuristic"` dimensions are estimates, and hover outlines describe highlights.
|
|
177
|
+
Helm/comms occupancy retains its existing inference rules; these collections do not establish player-to-machine links.
|
|
178
|
+
|
|
108
179
|
## Nearby ships
|
|
109
180
|
|
|
110
181
|
```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dredless",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A fully-featured and from-scratch headless drednot.io client implementation written in pure js.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/certainhuman/dredless#readme",
|
|
@@ -427,6 +427,11 @@ export class WorldDomain {
|
|
|
427
427
|
this.entities = new EntityCollection(client, scope);
|
|
428
428
|
this.machines = new MachineCollection(client, scope);
|
|
429
429
|
this.players = new PlayerCollection(client, scope);
|
|
430
|
+
this.projectiles = new EntitySummaryCollection(client, scope, "projectiles");
|
|
431
|
+
this.bots = new EntitySummaryCollection(client, scope, "bots");
|
|
432
|
+
this.rcds = new EntitySummaryCollection(client, scope, "rcds");
|
|
433
|
+
this.balls = new EntitySummaryCollection(client, scope, "balls");
|
|
434
|
+
this.recyclers = new EntitySummaryCollection(client, scope, "recyclers");
|
|
430
435
|
this.blocks = new BlockCollection(client, scope);
|
|
431
436
|
this.materials = new MaterialCollection(client, scope);
|
|
432
437
|
}
|
|
@@ -569,6 +574,24 @@ export class PlayerCollection {
|
|
|
569
574
|
}
|
|
570
575
|
}
|
|
571
576
|
|
|
577
|
+
class EntitySummaryCollection {
|
|
578
|
+
constructor(client, scope, method) {
|
|
579
|
+
this.client = client;
|
|
580
|
+
this.scope = scope;
|
|
581
|
+
this.method = method;
|
|
582
|
+
this.component = {projectiles: "projectile", bots: "bot", rcds: "rcd", balls: "ball", recyclers: "recycler"}[method];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
all() {
|
|
586
|
+
return worldStateFor(this.client, this.scope)?.model[this.method]() || [];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
get(entity) {
|
|
590
|
+
const id = entityIdOf(entity);
|
|
591
|
+
return worldStateFor(this.client, this.scope)?.model.entity(id)?.contents?.[this.component] || null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
572
595
|
export class BlockCollection {
|
|
573
596
|
constructor(client, scope) {
|
|
574
597
|
this.client = client;
|
|
@@ -1413,6 +1436,7 @@ export class ShipHandle {
|
|
|
1413
1436
|
}
|
|
1414
1437
|
|
|
1415
1438
|
const ENTITY_COMPONENT_ALIASES = new Map([
|
|
1439
|
+
["combatbeams", "combatBeams"],
|
|
1416
1440
|
["hoveroutline", "hoverOutline"],
|
|
1417
1441
|
["outline", "hoverOutline"],
|
|
1418
1442
|
["itemholder", "itemHolder"],
|
|
@@ -1465,6 +1489,12 @@ const MACHINE_COMPONENTS = [
|
|
|
1465
1489
|
];
|
|
1466
1490
|
|
|
1467
1491
|
const ENTITY_FEATURES = [
|
|
1492
|
+
"combatBeams",
|
|
1493
|
+
"projectile",
|
|
1494
|
+
"bot",
|
|
1495
|
+
"rcd",
|
|
1496
|
+
"ball",
|
|
1497
|
+
"recycler",
|
|
1468
1498
|
"health",
|
|
1469
1499
|
"position",
|
|
1470
1500
|
"outline",
|
|
@@ -1531,6 +1561,10 @@ function sourceWithProperty(contents, names, property) {
|
|
|
1531
1561
|
}
|
|
1532
1562
|
|
|
1533
1563
|
function entityPublicType(summary, contents) {
|
|
1564
|
+
if (contents?.projectile) return "projectile";
|
|
1565
|
+
if (contents?.rcd) return "rcd";
|
|
1566
|
+
if (contents?.ball) return contents.ball.isEgg ? "egg" : "ball";
|
|
1567
|
+
if (contents?.recycler) return "recycler";
|
|
1534
1568
|
const machine = entityMachineType(contents);
|
|
1535
1569
|
if (machine) return machine;
|
|
1536
1570
|
if (contents?.player || summary?.category === "player") return "player";
|
package/src/index.d.ts
CHANGED
|
@@ -932,6 +932,11 @@ export interface WorldDomain {
|
|
|
932
932
|
entities: EntityCollection;
|
|
933
933
|
machines: MachineCollection;
|
|
934
934
|
players: PlayerCollection;
|
|
935
|
+
projectiles: EntitySummaryCollection<ProjectileSummary>;
|
|
936
|
+
bots: EntitySummaryCollection<BotSummary>;
|
|
937
|
+
rcds: EntitySummaryCollection<RcdSummary>;
|
|
938
|
+
balls: EntitySummaryCollection<BallSummary>;
|
|
939
|
+
recyclers: EntitySummaryCollection<RecyclerSummary>;
|
|
935
940
|
blocks: BlockCollection;
|
|
936
941
|
materials: MaterialCollection;
|
|
937
942
|
|
|
@@ -976,6 +981,12 @@ export interface PlayerCollection {
|
|
|
976
981
|
current(): PlayerSummary | null;
|
|
977
982
|
}
|
|
978
983
|
|
|
984
|
+
/** Current component summaries; get() returns null for a missing entity or component. */
|
|
985
|
+
export interface EntitySummaryCollection<T> {
|
|
986
|
+
all(): T[];
|
|
987
|
+
get(entity: EntityRef): T | null;
|
|
988
|
+
}
|
|
989
|
+
|
|
979
990
|
export interface BlockCollection {
|
|
980
991
|
all(): BlockSummary[];
|
|
981
992
|
|
|
@@ -1065,6 +1076,11 @@ export interface MachineCollection {
|
|
|
1065
1076
|
}
|
|
1066
1077
|
|
|
1067
1078
|
export type EntityPublicType =
|
|
1079
|
+
| "projectile"
|
|
1080
|
+
| "rcd"
|
|
1081
|
+
| "ball"
|
|
1082
|
+
| "egg"
|
|
1083
|
+
| "recycler"
|
|
1068
1084
|
| "loader"
|
|
1069
1085
|
| "pusher"
|
|
1070
1086
|
| "launcher"
|
|
@@ -1556,6 +1572,11 @@ export interface CargoEjectorSummary {
|
|
|
1556
1572
|
}
|
|
1557
1573
|
|
|
1558
1574
|
export interface EntityContentsSummary {
|
|
1575
|
+
combatBeams?: CombatBeamsSummary;
|
|
1576
|
+
projectile?: ProjectileSummary;
|
|
1577
|
+
rcd?: RcdSummary;
|
|
1578
|
+
ball?: BallSummary;
|
|
1579
|
+
recycler?: RecyclerSummary;
|
|
1559
1580
|
itemHolder?: ItemHolderSummary;
|
|
1560
1581
|
expandoBox?: ExpandoBoxSummary;
|
|
1561
1582
|
hoverOutline?: HoverOutlineSummary;
|
|
@@ -1591,7 +1612,7 @@ export interface EntityContentsSummary {
|
|
|
1591
1612
|
|
|
1592
1613
|
export interface EntitySummary {
|
|
1593
1614
|
entity: number;
|
|
1594
|
-
category: "placed_entity" | "loose_item" | "untyped_holder" | "metadata" | "player" | "ship_control" | "blueprint_preview" | "entity";
|
|
1615
|
+
category: "placed_entity" | "loose_item" | "untyped_holder" | "metadata" | "player" | "ship_control" | "blueprint_preview" | "projectile" | "ball" | "rcd" | "entity";
|
|
1595
1616
|
typeId: number | null;
|
|
1596
1617
|
typeName: string | null;
|
|
1597
1618
|
markerTypeId: number | null;
|
|
@@ -1789,6 +1810,11 @@ export interface WorldSnapshot {
|
|
|
1789
1810
|
transforms: TransformSummary[];
|
|
1790
1811
|
machines: MachineSummary;
|
|
1791
1812
|
players: PlayerSummary[];
|
|
1813
|
+
projectiles: ProjectileSummary[];
|
|
1814
|
+
bots: BotSummary[];
|
|
1815
|
+
rcds: RcdSummary[];
|
|
1816
|
+
balls: BallSummary[];
|
|
1817
|
+
recyclers: RecyclerSummary[];
|
|
1792
1818
|
shipControls: ShipControlSummary[];
|
|
1793
1819
|
commsBubbles: CommsBubbleSummary[];
|
|
1794
1820
|
tiles?: Tile[];
|
|
@@ -1883,6 +1909,12 @@ export class ModelState {
|
|
|
1883
1909
|
|
|
1884
1910
|
players(): PlayerSummary[];
|
|
1885
1911
|
|
|
1912
|
+
projectiles(): ProjectileSummary[];
|
|
1913
|
+
bots(): BotSummary[];
|
|
1914
|
+
rcds(): RcdSummary[];
|
|
1915
|
+
balls(): BallSummary[];
|
|
1916
|
+
recyclers(): RecyclerSummary[];
|
|
1917
|
+
|
|
1886
1918
|
shipControls(): ShipControlSummary[];
|
|
1887
1919
|
|
|
1888
1920
|
shipMetadata(): ShipWorldMetadataSummary | null;
|
|
@@ -1904,6 +1936,11 @@ export interface ModelSnapshot {
|
|
|
1904
1936
|
blocks?: BlockSummary[];
|
|
1905
1937
|
transforms: TransformSummary[];
|
|
1906
1938
|
players: PlayerSummary[];
|
|
1939
|
+
projectiles: ProjectileSummary[];
|
|
1940
|
+
bots: BotSummary[];
|
|
1941
|
+
rcds: RcdSummary[];
|
|
1942
|
+
balls: BallSummary[];
|
|
1943
|
+
recyclers: RecyclerSummary[];
|
|
1907
1944
|
shipControls: ShipControlSummary[];
|
|
1908
1945
|
machines: MachineSummary;
|
|
1909
1946
|
tables: unknown[];
|
|
@@ -2017,6 +2054,75 @@ export interface BotSummary {
|
|
|
2017
2054
|
identifier: string;
|
|
2018
2055
|
typeA: number | null;
|
|
2019
2056
|
typeB: number | null;
|
|
2057
|
+
classificationSource: "heuristic";
|
|
2058
|
+
health: HealthSummary;
|
|
2059
|
+
beams: CombatBeamsSummary | null;
|
|
2060
|
+
combat: BotCombatSummary | null;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
export interface CombatBeamsSummary {
|
|
2064
|
+
entity: number;
|
|
2065
|
+
/** Three replicated beams; angles are radians, lengths are world units. */
|
|
2066
|
+
beams: { angle: number; length: number; active: boolean; colorRgba: number }[];
|
|
2067
|
+
state: ModelRecord;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
export interface BotCombatSummary {
|
|
2071
|
+
/** Packed RGBA, unlike the RGB projectile and outfit colors. */
|
|
2072
|
+
bodyColorRgba: number;
|
|
2073
|
+
weaponColorRgba: number;
|
|
2074
|
+
/** Radians; the official -7 sentinel becomes null. */
|
|
2075
|
+
aimAngle: number | null;
|
|
2076
|
+
thrusterMask: number;
|
|
2077
|
+
alternateSprite: boolean;
|
|
2078
|
+
beamEffectEnabled: boolean;
|
|
2079
|
+
state: ModelRecord;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
export interface ProjectileSummary {
|
|
2083
|
+
entity: number;
|
|
2084
|
+
/** Packed RGB trail/tint color. */
|
|
2085
|
+
color: number;
|
|
2086
|
+
colorCss: string;
|
|
2087
|
+
/** Selects the official bot-bullet sprite; not an ownership guarantee. */
|
|
2088
|
+
botStyle: boolean;
|
|
2089
|
+
/** Count of orbiting visual markers. No inferred damage or ammo semantics. */
|
|
2090
|
+
indicatorCount: number;
|
|
2091
|
+
spriteItemId: number | null;
|
|
2092
|
+
spriteItemName: string | null;
|
|
2093
|
+
state: ModelRecord;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
export interface BallSummary {
|
|
2097
|
+
entity: number;
|
|
2098
|
+
health: HealthSummary | null;
|
|
2099
|
+
/** World units, when the size component is available. */
|
|
2100
|
+
radius: number | null;
|
|
2101
|
+
/** True for rubber, false for rock; independent of health/destructibility. */
|
|
2102
|
+
rubber: boolean;
|
|
2103
|
+
isEgg: boolean;
|
|
2104
|
+
state: ModelRecord;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
export interface RcdSummary {
|
|
2108
|
+
entity: number;
|
|
2109
|
+
variant: "sandbox" | "flux";
|
|
2110
|
+
/** Official renderer selects sandbox for zero and flux otherwise. */
|
|
2111
|
+
variantCode: number;
|
|
2112
|
+
itemId: 121 | 122;
|
|
2113
|
+
storedItemId: number | null;
|
|
2114
|
+
storedItemName: string | null;
|
|
2115
|
+
storedItemCount: number | null;
|
|
2116
|
+
/** Vector from the RCD in world axes, in world units. */
|
|
2117
|
+
beam: { offsetX: number; offsetY: number; active: boolean };
|
|
2118
|
+
state: ModelRecord;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
export interface RecyclerSummary {
|
|
2122
|
+
entity: number;
|
|
2123
|
+
storedItemId: number | null;
|
|
2124
|
+
storedItemName: string | null;
|
|
2125
|
+
storedItemCount: number | null;
|
|
2020
2126
|
}
|
|
2021
2127
|
|
|
2022
2128
|
export interface FabricatorSummary {
|
|
@@ -141,6 +141,10 @@ export const MODEL_TABLE_SPECS = new Map([
|
|
|
141
141
|
[11, {name: "numeric_sparse", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36})}],
|
|
142
142
|
[12, {name: "numeric_sparse", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32})}],
|
|
143
143
|
[14, {name: "size_state", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 32: 40, 64: 44})}],
|
|
144
|
+
[15, {
|
|
145
|
+
name: "combat_beams",
|
|
146
|
+
fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 32: 40, 64: 44, 128: 48, 256: 52})
|
|
147
|
+
}],
|
|
144
148
|
[16, {
|
|
145
149
|
name: "label_numeric_state",
|
|
146
150
|
read(reader, record, mask) {
|
|
@@ -158,29 +162,25 @@ export const MODEL_TABLE_SPECS = new Map([
|
|
|
158
162
|
if (mask & 16) record.q36 = (record.q36 || 0) + reader.readFieldDelta();
|
|
159
163
|
}
|
|
160
164
|
}],
|
|
161
|
-
[17, {
|
|
165
|
+
[17, {
|
|
166
|
+
name: "ball",
|
|
167
|
+
packedBeforeValues: true,
|
|
168
|
+
packedBits: [{bit: 1, offset: 21}, {bit: 2, offset: 22}]
|
|
169
|
+
}],
|
|
162
170
|
[18, {
|
|
163
171
|
name: "mob_combat_state",
|
|
172
|
+
// Official reader packs flags 64/2048 before numeric deltas. Raw offsets
|
|
173
|
+
// follow that reader's layout, rather than treating flag bits as values.
|
|
174
|
+
packedBeforeValues: true,
|
|
175
|
+
packedBits: [{bit: 64, offset: 77}, {bit: 2048, offset: 78}],
|
|
176
|
+
defaults: {
|
|
177
|
+
q20: -1, q24: 0, q28: 0, q32: 0, q36: 0, q40: -9417217,
|
|
178
|
+
q44: 0, q48: -100, q52: 0, q56: 10, q60: 10, q64: 1, q68: 1, q72: 100
|
|
179
|
+
},
|
|
164
180
|
fields: numericFields({
|
|
165
|
-
2: 20,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
16: 32,
|
|
169
|
-
32: 36,
|
|
170
|
-
64: 40,
|
|
171
|
-
128: 44,
|
|
172
|
-
256: 48,
|
|
173
|
-
512: 52,
|
|
174
|
-
1024: 56,
|
|
175
|
-
2048: 60,
|
|
176
|
-
4096: 64,
|
|
177
|
-
8192: 68,
|
|
178
|
-
16384: 72,
|
|
179
|
-
32768: 76,
|
|
180
|
-
65536: 80,
|
|
181
|
-
131072: 84,
|
|
182
|
-
524288: 88,
|
|
183
|
-
4194304: 92
|
|
181
|
+
2: 20, 4: 24, 8: 28, 16: 32, 32: 36, 128: 40,
|
|
182
|
+
256: 44, 512: 48, 1024: 52, 4096: 56, 8192: 60,
|
|
183
|
+
16384: 64, 32768: 68, 65536: 72
|
|
184
184
|
})
|
|
185
185
|
}],
|
|
186
186
|
[19, {name: "motion_state", packedBits: [{bit: 4, offset: 29}], ...TWO_FIELD_SPEC}],
|
|
@@ -214,7 +214,13 @@ export const MODEL_TABLE_SPECS = new Map([
|
|
|
214
214
|
fields: numericFields({1: 20, 2: 24}),
|
|
215
215
|
blobs: [{bit: 4, offset: 28}, {bit: 8, offset: 40}]
|
|
216
216
|
}],
|
|
217
|
-
[24, {
|
|
217
|
+
[24, {
|
|
218
|
+
name: "projectile_state",
|
|
219
|
+
defaults: {q20: 16777215, q24: 1, q28: 1},
|
|
220
|
+
packedBeforeValues: true,
|
|
221
|
+
packedBits: [{bit: 2, offset: 33}],
|
|
222
|
+
fields: numericFields({1: 20, 4: 24, 8: 28})
|
|
223
|
+
}],
|
|
218
224
|
[25, {
|
|
219
225
|
name: "zone_label",
|
|
220
226
|
orderedValues: true,
|
|
@@ -279,7 +285,7 @@ export const MODEL_TABLE_SPECS = new Map([
|
|
|
279
285
|
defaults: {q20: 0, q24: 0, q28: 0, q32: 0, q36: 0, q40: 0, q44: 0, q48: 0, q52: 0, q56: 0, q60: 0, q64: 0},
|
|
280
286
|
fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 32: 40, 64: 44, 128: 48, 256: 52, 512: 56, 1024: 60, 2048: 64})
|
|
281
287
|
}],
|
|
282
|
-
[59, {name: "
|
|
288
|
+
[59, {name: "rcd", fields: numericFields({1: 20, 2: 24, 4: 28, 8: 32})}],
|
|
283
289
|
[60, {name: "fluid_tank", packedBits: [{bit: 4, offset: 29}], ...TWO_FIELD_SPEC}],
|
|
284
290
|
[61, {name: "shield_charge", ...TWO_FIELD_SPEC}],
|
|
285
291
|
[62, {name: "flag_state", packedBits: [{bit: 1, offset: 21}], fields: []}],
|
|
@@ -358,7 +364,7 @@ export const WIRE_TAG_TABLES = new Map([
|
|
|
358
364
|
[48, 12],
|
|
359
365
|
[49, 13],
|
|
360
366
|
[50, 14],
|
|
361
|
-
[51,
|
|
367
|
+
[51, 15],
|
|
362
368
|
[52, 16],
|
|
363
369
|
[74, 17],
|
|
364
370
|
[80, 17],
|
|
@@ -790,45 +796,120 @@ export function summarizeBot(entity, {
|
|
|
790
796
|
combatRecord,
|
|
791
797
|
motionRecord,
|
|
792
798
|
table10Record,
|
|
793
|
-
|
|
799
|
+
beamRecord,
|
|
794
800
|
itemCrate,
|
|
795
801
|
player,
|
|
796
802
|
shipControl
|
|
797
803
|
}) {
|
|
798
|
-
if (!health || itemCrate || player || shipControl
|
|
804
|
+
if (!health || itemCrate || player || shipControl) return null;
|
|
799
805
|
const className = botClassFromState({
|
|
800
806
|
health,
|
|
801
807
|
table2Record,
|
|
802
808
|
smallRecord,
|
|
803
809
|
combatRecord,
|
|
804
810
|
table10Record,
|
|
805
|
-
|
|
811
|
+
beamRecord
|
|
806
812
|
});
|
|
813
|
+
// Health alone is not enough to identify an arbitrary entity as a bot.
|
|
814
|
+
if (className === "bot") return null;
|
|
807
815
|
return {
|
|
808
816
|
entity,
|
|
809
817
|
className,
|
|
818
|
+
classificationSource: "heuristic",
|
|
819
|
+
health,
|
|
820
|
+
beams: summarizeCombatBeams(entity, beamRecord),
|
|
821
|
+
combat: combatRecord ? {
|
|
822
|
+
bodyColorRgba: (combatRecord.q20 ?? -1) >>> 0,
|
|
823
|
+
weaponColorRgba: (combatRecord.q40 ?? -9417217) >>> 0,
|
|
824
|
+
aimAngle: combatRecord.q44 === -70 ? null : numberOrNull(combatRecord.q44, 10),
|
|
825
|
+
thrusterMask: combatRecord.q60 ?? 10,
|
|
826
|
+
alternateSprite: Boolean(combatRecord.q77),
|
|
827
|
+
beamEffectEnabled: Boolean(combatRecord.q78),
|
|
828
|
+
state: cloneRecord(combatRecord)
|
|
829
|
+
} : null,
|
|
810
830
|
identifier: botIdentifierFromState({combatRecord, motionRecord, smallRecord}),
|
|
811
831
|
typeA: health.state?.q28 ?? null,
|
|
812
832
|
typeB: health.state?.q32 ?? null
|
|
813
833
|
};
|
|
814
834
|
}
|
|
815
835
|
|
|
816
|
-
export function
|
|
817
|
-
return
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
836
|
+
export function summarizeProjectile(entity, record) {
|
|
837
|
+
if (!record) return null;
|
|
838
|
+
const color = record.q20 ?? 16777215;
|
|
839
|
+
const itemId = record.q28 ?? 1;
|
|
840
|
+
return {
|
|
841
|
+
entity,
|
|
842
|
+
color,
|
|
843
|
+
colorCss: colorToCss(color),
|
|
844
|
+
botStyle: Boolean(record.q33),
|
|
845
|
+
indicatorCount: record.q24 ?? 1,
|
|
846
|
+
spriteItemId: itemId === 0 ? null : itemId,
|
|
847
|
+
spriteItemName: itemId === 0 ? null : entityNameFromType(itemId),
|
|
848
|
+
state: cloneRecord(record)
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function summarizeCombatBeams(entity, record) {
|
|
853
|
+
if (!record) return null;
|
|
854
|
+
return {
|
|
855
|
+
entity,
|
|
856
|
+
beams: [0, 1, 2].map((index) => {
|
|
857
|
+
const length = (record[qKey(32 + index * 4)] ?? 0) / 10;
|
|
858
|
+
return {
|
|
859
|
+
angle: (record[qKey(20 + index * 4)] ?? 0) / 1000,
|
|
860
|
+
length,
|
|
861
|
+
active: length > 0,
|
|
862
|
+
colorRgba: (record[qKey(44 + index * 4)] ?? 0) >>> 0
|
|
863
|
+
};
|
|
864
|
+
}),
|
|
865
|
+
state: cloneRecord(record)
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export function summarizeBall(entity, record, health = null, sizeRecord = null) {
|
|
870
|
+
if (!record) return null;
|
|
871
|
+
return {
|
|
872
|
+
entity,
|
|
873
|
+
health,
|
|
874
|
+
radius: numberOrNull(sizeRecord?.q20, 20),
|
|
875
|
+
rubber: Boolean(record.q21),
|
|
876
|
+
isEgg: Boolean(record.q22),
|
|
877
|
+
state: cloneRecord(record)
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export function summarizeRcd(entity, record, itemHolderRecord) {
|
|
882
|
+
if (!record) return null;
|
|
883
|
+
const variant = (record.q20 ?? 0) === 0 ? "sandbox" : "flux";
|
|
884
|
+
const item = itemSummary(itemHolderRecord?.q20, itemHolderRecord?.q24);
|
|
885
|
+
return {
|
|
886
|
+
entity,
|
|
887
|
+
variant,
|
|
888
|
+
variantCode: record.q20 ?? 0,
|
|
889
|
+
itemId: variant === "sandbox" ? 121 : 122,
|
|
890
|
+
storedItemId: item.itemId,
|
|
891
|
+
storedItemName: item.itemName,
|
|
892
|
+
storedItemCount: item.count,
|
|
893
|
+
beam: {
|
|
894
|
+
offsetX: (record.q28 ?? 0) / 5,
|
|
895
|
+
offsetY: (record.q32 ?? 0) / 5,
|
|
896
|
+
active: Boolean(record.q28 || record.q32)
|
|
897
|
+
},
|
|
898
|
+
state: cloneRecord(record)
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export function summarizeRecycler(entity, marker, itemHolderRecord) {
|
|
903
|
+
if (!marker) return null;
|
|
904
|
+
const item = itemSummary(itemHolderRecord?.q20, itemHolderRecord?.q24);
|
|
905
|
+
return {entity, storedItemId: item.itemId, storedItemName: item.itemName, storedItemCount: item.count};
|
|
825
906
|
}
|
|
826
907
|
|
|
827
|
-
export function botClassFromState({health, table2Record, smallRecord, combatRecord, table10Record,
|
|
908
|
+
export function botClassFromState({health, table2Record, smallRecord, combatRecord, table10Record, beamRecord}) {
|
|
828
909
|
if (isYellowMineGuardBotState({health, smallRecord, combatRecord, table2Record})) return "yellow-mine-guard";
|
|
829
910
|
if (isShieldHelperBotState({health, table2Record})) return "shield-helper";
|
|
830
911
|
if (isShieldMasterBotState({health, table2Record})) return "shield-master";
|
|
831
|
-
if (isLazerEnthusiastBotState({health, table2Record, table10Record,
|
|
912
|
+
if (isLazerEnthusiastBotState({health, table2Record, table10Record, beamRecord})) return "lazer-enthusiast";
|
|
832
913
|
if (isCowardBossState({health})) return "the-coward";
|
|
833
914
|
if (isOrangeFoolBotState({combatRecord})) return "orange-fool";
|
|
834
915
|
if (isZombieBossBotState({health, table2Record, combatRecord})) return "zombie-boss";
|
|
@@ -842,6 +923,7 @@ export function botClassFromState({health, table2Record, smallRecord, combatReco
|
|
|
842
923
|
if (isAquaShielderBotState({health, table2Record, combatRecord})) return "aqua-shielder";
|
|
843
924
|
if (table2Record) return "table2-bot";
|
|
844
925
|
if (combatRecord) return "combat-bot";
|
|
926
|
+
if (beamRecord) return "beam-bot";
|
|
845
927
|
return "bot";
|
|
846
928
|
}
|
|
847
929
|
|
|
@@ -895,14 +977,14 @@ export function isAquaShielderBotState({health, table2Record, combatRecord}) {
|
|
|
895
977
|
);
|
|
896
978
|
}
|
|
897
979
|
|
|
898
|
-
export function isLazerEnthusiastBotState({health, table2Record, table10Record,
|
|
980
|
+
export function isLazerEnthusiastBotState({health, table2Record, table10Record, beamRecord}) {
|
|
899
981
|
return (
|
|
900
982
|
health?.state?.q28 === 140 &&
|
|
901
983
|
health?.state?.q32 === 155 &&
|
|
902
984
|
table2Record?.q20 === 300 &&
|
|
903
985
|
table2Record?.q24 === -900 &&
|
|
904
986
|
Boolean(table10Record) &&
|
|
905
|
-
Boolean(
|
|
987
|
+
Boolean(beamRecord)
|
|
906
988
|
);
|
|
907
989
|
}
|
|
908
990
|
|
|
@@ -1515,6 +1597,10 @@ export function entityFootprint(entity) {
|
|
|
1515
1597
|
}
|
|
1516
1598
|
|
|
1517
1599
|
export function entityLabel(entity) {
|
|
1600
|
+
if (entity?.projectile) return "Projectile";
|
|
1601
|
+
if (entity?.ball) return entity.ball.isEgg ? "Egg" : "Ball";
|
|
1602
|
+
if (entity?.rcd) return entity.rcd.variant === "sandbox" ? "Sandbox RCD" : "Flux RCD";
|
|
1603
|
+
if (entity?.recycler) return "Recycler";
|
|
1518
1604
|
if (entity?.category === "metadata" && entity?.typeName) return `Metadata ${entity.typeName}`;
|
|
1519
1605
|
if (entity?.blueprintPreview) return `Blueprint Preview (${entity.blueprintPreview.itemName ?? entity.blueprintPreview.itemId ?? "item"})`;
|
|
1520
1606
|
if (entity?.category === "loose_item" && entity?.itemHolder?.itemName) return `Loose ${entity.itemHolder.itemName}`;
|
|
@@ -1546,6 +1632,10 @@ export function entityLabel(entity) {
|
|
|
1546
1632
|
}
|
|
1547
1633
|
|
|
1548
1634
|
export function entityCategory(entity) {
|
|
1635
|
+
if (entity?.projectile) return "projectile";
|
|
1636
|
+
if (entity?.ball) return "ball";
|
|
1637
|
+
if (entity?.rcd) return "rcd";
|
|
1638
|
+
if (entity?.recycler) return "placed_entity";
|
|
1549
1639
|
if (entity?.player) return "player";
|
|
1550
1640
|
if (entity?.blueprintPreview) return "blueprint_preview";
|
|
1551
1641
|
if (entity?.shipControl) return "ship_control";
|
|
@@ -2,6 +2,13 @@ export {
|
|
|
2
2
|
FabricatorType,
|
|
3
3
|
ModelState,
|
|
4
4
|
decodeModelData,
|
|
5
|
+
type ProjectileSummary,
|
|
6
|
+
type BotCombatSummary,
|
|
7
|
+
type CombatBeamsSummary,
|
|
8
|
+
type BotSummary,
|
|
9
|
+
type RcdSummary,
|
|
10
|
+
type BallSummary,
|
|
11
|
+
type RecyclerSummary,
|
|
5
12
|
type ModelRecord,
|
|
6
13
|
type ModelSnapshot,
|
|
7
14
|
type EntitySummary,
|