@spawnite/room 0.0.0-stage → 0.1.0-beta.17

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/LICENSE.md ADDED
@@ -0,0 +1,169 @@
1
+ # Spawnite License 1.0
2
+
3
+ <https://spawnite.com/license/1.0>
4
+
5
+ Copyright (c) 2026 Daniel Zarinski and David Zarinski.
6
+
7
+ ## Summary
8
+
9
+ This summary is not part of the terms and does not replace them.
10
+
11
+ - Using the Spawnite engine, tools and development room is free. Building and testing your games and their tools is always allowed, on your machines and through the services and agents that work for you, including local multiplayer.
12
+ - Games that include the software are made public only through Spawnite. To publish one anywhere else, ask us at dev@spawnite.com. We decide each request case by case, and a yes is a written license that says what it covers.
13
+ - Your game's own code, assets and earnings share are yours, including what you write yourself inside a copied plugin. These terms do not reach them, and you may take them to any engine.
14
+ - A plugin the engine ships as readable source, in a package's `readable` folder, is yours to read, copy into your game and change. A copy stays under these terms.
15
+ - Everything else ships closed: use it in your games, read it, format it, step through it and patch your installed copy to build and fix them, but don't rebuild it in source or use it to build, train or test anything that competes.
16
+ - You may not redistribute the software on its own or inside another engine or SDK, run it or the development room as a hosting service, or build a competing engine or platform.
17
+ - If Spawnite stops hosting games for 90 days, you may publish and run your own games elsewhere on the versions you have.
18
+ - No royalty. Templates and kits carry a license per file: most are MIT-0, and a copied plugin stays under these terms.
19
+
20
+ ## Acceptance
21
+
22
+ These terms offer their licenses only to a person who accepts them, except Player Permission and the permission to download a copy to read these terms. You accept by choosing an acceptance control that shows these terms, such as one Spawnite's tools or website present, by sending the licensor written acceptance that names this version, or by developing, testing or publishing a game with the software, which these terms come with. If you accept for an entity, you confirm you may bind it. Downloading or installing a package, receiving it as a dependency, or visiting a page does not accept these terms by itself, and gives no license but Player Permission.
23
+
24
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses, including the Limitations, whether or not an activity they name would otherwise need copyright or patent permission.
25
+
26
+ ## Copyright License
27
+
28
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to Distribution License, make changes and new works based on the software according to Changes and New Works License, and copy and change source-available parts according to Source-Available Parts.
29
+
30
+ ## Permitted Purposes
31
+
32
+ These are the permitted purposes:
33
+
34
+ - development operations for your games and their tools, including private testing with the development room server, but not by making a game available to the public;
35
+ - making a game available to the public through Spawnite, as Games describes; and
36
+ - studying the software and publishing explanations, reviews, criticism and security findings about it, without publishing the closed parts, what anyone recovers of their source, or more of a source-available part than short quotations. This purpose grants no permission to develop a competing product.
37
+
38
+ ## Games
39
+
40
+ You may supply the software as a functional part of a game, bundled with the game or loaded from a shared engine release Spawnite provides, but not as a reusable engine, framework or SDK for others. You may make a game that includes or requires any part of the software available to the public only through Spawnite, which includes its delivery to and running in players' browsers and apps. Making it available to the public any other way, including on your own website or on another store or platform, is not a permitted purpose.
41
+
42
+ To publish a game outside Spawnite, ask the licensor at dev@spawnite.com. The licensor decides each request on its own case and need not grant any. A request, an acknowledgement or a discussion grants nothing. A permission takes effect only as a written license from the licensor that names the games, the versions of the software, where they may be published, whether and where a room server may run for them, how long it lasts and any charge, and it overrides these terms only as far as it says.
43
+
44
+ Publishing your game's source code, such as in a public repository, is not making the game available to the public, as long as it holds no closed part of the software and every copy of a source-available part in it keeps its notice and stays under these terms.
45
+
46
+ ## Player Permission
47
+
48
+ Anyone may receive, cache and run the copies of the software delivered to play a game made public as Games, a written license from the licensor or If Spawnite Stops Hosting Games allows. This covers what a browser or an app does to play the game, and gives no permission to extract those copies or use them for anything else.
49
+
50
+ ## Your Game
51
+
52
+ You keep the ownership of your game's original code, assets and other content, and of what you write yourself in a copy of a source-available part, without copying the software. These terms give the licensor no rights in them and assign it nothing, and do not limit what you do with them apart from the software. You may use them with any other engine and on any other platform. In a copy of a source-available part in your game, what comes from the software, changed or not, stays part of the software. Code does not become part of the software because it calls the software, sits in the same file as it, or ships beside it. Your game's saves, world data, screenshots, recordings and other outputs are yours.
53
+
54
+ Templates, kits, starter code and examples carry the license their files name. A file that names the MIT No Attribution License (MIT-0) is under MIT-0, not these terms. A file that names these terms is a source-available part. Third-party material in them stays under its own license. Moving, renaming or changing a file's notice does not change its license. What a game earns on Spawnite is governed by Spawnite's creator terms, not this license, and this license charges no royalty or fee.
55
+
56
+ ## Distribution License
57
+
58
+ The licensor grants you an additional copyright license to distribute copies of the software, with changes permitted by these terms, but only as part of a game according to Games, and to your company and private testers for a permitted purpose. Giving the software to a service that processes it only for you and for your permitted purposes, such as a private code repository, a continuous-integration service or an AI coding service, is not distributing it.
59
+
60
+ ## Changes and New Works License
61
+
62
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose, as follows. You may change your installed copy of a closed part, such as by a patch, to build, test and fix your games, but you may not write a closed part's implementation into your own source code, or distribute a changed closed part except as part of a game according to Games. The software in a change or new work stays under these terms, and distributing the change or new work distributes that software. No grant in these terms permits developing a competing product.
63
+
64
+ ## Source-Available Parts
65
+
66
+ The licensor grants you an additional copyright license to read the source-available parts, to copy them into a game or a tool for your games, and to change those copies, for any permitted purpose. You may distribute a copy, changed or not, only as part of a game according to Games, to your company and private testers, and as Games allows for your game's source. You may give a copy, changed or not, to people you choose who have accepted these terms, for a game or a contribution you are working on together, privately or through a channel Spawnite provides for sharing plugins and kits. You may not make a copy available to the public any other way, such as in a public collection of engine code, a package registry or a website. Each recipient gets its license from the licensor, not from you.
67
+
68
+ ## Contributions
69
+
70
+ A contribution is a change to the software you expressly offer the licensor for inclusion in it, such as by a pull request or a message that says so. Uploading a game to publish or test it, sending a bug report or a bug bundle, and letting the licensor run your game are not contributions. You keep the ownership of your contribution, and grant the licensor a non-exclusive, perpetual, irrevocable, worldwide, royalty-free license, under your copyright and any patent claims you can license that the contribution infringes, to use, change, distribute and sublicense it as part of the software. The grant covers the contribution alone, not your game's other code, assets or confidential information. Nothing in these terms requires you to offer one.
71
+
72
+ ## Limitations
73
+
74
+ Even for a permitted purpose, you may not:
75
+
76
+ - distribute the software except as Distribution License, Source-Available Parts and Games allow;
77
+ - provide others a hosted or managed service that gives reusable access to a substantial part of the software's engine, authoring, simulation or development room features; development operations, private testing, play under Games and Player Permission, and what If Spawnite Stops Hosting Games allows are not such a service;
78
+ - run the development room server, or any other part of the software, to serve the players of a game made available to the public, except as a written license from the licensor or If Spawnite Stops Hosting Games allows;
79
+ - rebuild or try to rebuild the implementation of a closed part in source form, or use such a rebuild from anyone else, except within development operations or a right Rights the Law Keeps preserves; reading, formatting, stepping through and patching the closed parts within development operations is allowed, and you may not publish or share what you recover;
80
+ - use the software, its expression, its outputs or its observed behaviour to develop, train, specify, validate or provide a competing product, including a machine-learning model that is or powers one, such as by generating specifications, training data or comparison tests for it; or
81
+ - direct, authorise or knowingly help another person or an automated agent to do what these terms forbid you.
82
+
83
+ These limits do not forbid development operations, ordinary reviews, criticism and benchmarks, porting or rebuilding your own games on another engine, or plugins, kits and tools that work only with the software. They claim nothing in general knowledge, ideas or independently written software.
84
+
85
+ ## Competition
86
+
87
+ A competing product is a game engine product that substitutes for the Spawnite engine, or a service that substitutes for Spawnite's service of publishing or hosting games for independent creators. Different interfaces, technical platforms, prices or distribution methods do not stop a product from competing, and a free product can compete. Marketing a product as a substitute for the software or for Spawnite is evidence that it competes.
88
+
89
+ A game does not compete because players choose it over another game or service, or because its players make, share or play levels and other content inside it. A plugin, kit or tool that works only with the software does not compete because it replaces one optional plugin or development tool.
90
+
91
+ ## If Spawnite Stops Hosting Games
92
+
93
+ This section takes effect if, for 90 consecutive days, Spawnite accepts no new game or new version of a game from creators generally, or makes no game of a creator other than the licensor available for public play, or if the licensor publicly announces that Spawnite will permanently stop either. A few demonstration, test or selected games staying available does not stop the first two from taking effect.
94
+
95
+ Then, for every version of the software the licensor published under these terms before that date, including your changes to those versions:
96
+
97
+ - Games no longer limits where you make your games available;
98
+ - you, or a service provider working for you, may run the development room server to serve your games' players, and doing so is not a hosted or managed service under Limitations; and
99
+ - a service that does only this, for your own games, does not compete with Spawnite.
100
+
101
+ This release is automatic, permanent, needs no announcement or further permission, stays in effect if Spawnite later resumes hosting, and cannot be taken away by a later version of these terms. It does not permit offering an engine, or a publishing or hosting platform for independent creators, and gives no access to systems the licensor never published.
102
+
103
+ ## Changes to These Terms
104
+
105
+ The licensor may publish new versions of these terms, each with a new version number and its own URL. A new version applies to versions of the software the licensor publishes after it, and to your use of earlier versions 30 days after the licensor gives notice on spawnite.com and by email to the address on your Spawnite account, if you have one. No new version can narrow Your Game, add a royalty or fee, narrow development operations for your games under Permitted Purposes, narrow Source-Available Parts for copies already in your games, narrow a written license the licensor granted, or narrow If Spawnite Stops Hosting Games. A new version does not make anything you did before it a violation.
106
+
107
+ ## Other Software
108
+
109
+ The software includes components that others license under their own terms, listed in the THIRD-PARTY-NOTICES file that comes with it. Those terms, not these, govern those components. These terms neither replace nor restrict a permission a third-party license gives, in source, compiled or bundled form, and the licensor naming material source-available or closed does not override that license.
110
+
111
+ ## Notices
112
+
113
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. When you distribute source files of a source-available part, keep their notices with them. In a compiled, minified or bundled game, you may put the notices in a license file or a legal notices page that ships with the game. Compiling, minifying or moving a notice does not change the license.
114
+
115
+ ## Patent License
116
+
117
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
118
+
119
+ ## Rights the Law Keeps
120
+
121
+ These terms do not restrict a right the law does not let the parties restrict by agreement, such as to observe, study or test how the software works, to make a backup copy, to correct errors, or to decompile it so that an independently created program can interoperate with it, but only to the extent and for the purposes that law requires. These terms do not waive a fair use or fair dealing defence to a copyright claim, and that defence alone does not excuse a breach of these terms. They claim no copyright in ideas, principles, functionality or methods of operation, and no obligation in them applies where the law forbids it.
122
+
123
+ ## No Other Rights
124
+
125
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses. No trademark permission is granted.
126
+
127
+ ## Patent Defense
128
+
129
+ If you, or an entity you act for, start a lawsuit or other proceeding claiming that the software infringes a patent, your patent license for the software ends immediately. A private report, a support message or a defence or counterclaim in a proceeding the licensor started against you does not end it.
130
+
131
+ ## Violations
132
+
133
+ The licensor may end your licenses for a material violation of these terms by written notice that names what you did and the term it breaks. Your licenses end only if you do not stop the violation and take practical steps to correct its effects within 32 days of receiving the notice; a violation you correct in time ends nothing. The licensor may end your licenses at once, by written notice, for knowingly distributing the software as an engine or knowingly developing or providing a competing product. Doing what Rights the Law Keeps preserves is not a violation.
134
+
135
+ When your licenses end, you must stop what needed them. Ending your licenses does not take your game's own content, does not affect material under other licenses, and does not end a contribution license you granted. Games you made available through Spawnite before your licenses ended may stay available, and keep earning, as Spawnite's creator terms provide.
136
+
137
+ ## No Liability
138
+
139
+ As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
140
+
141
+ ## Definitions
142
+
143
+ The **licensor** is Daniel Zarinski and David Zarinski, and their successors.
144
+
145
+ The **software** is the software the licensor makes available under these terms: the Spawnite engine, schema, CLI, MCP server, dev server, devtools, UI kit, testing tools and development room server, with their type definitions and documentation, in every form the licensor provides them, including the engine release Spawnite serves to players' browsers and apps, and every copy and adaptation of their expression.
146
+
147
+ The **source-available parts** are the files of the software under a `readable/` folder of a package, and any other file of the software whose notice names these terms as source available, with every copy, excerpt, change, translation and compiled or bundled form of them, whether or not it keeps the notice. The **closed parts** are every other part of the software the licensor provides, including its compiled, minified and type definition files, its documentation, and the engine release Spawnite serves to players. "Closed" names the permissions these terms give, not whether a file can be read.
148
+
149
+ **Development operations** are what is reasonably needed to develop, test, profile, secure, diagnose and maintain your games and the tools you make for them: reading, formatting, stepping through, tracing, patching and running the software, headless or not, replays, compatibility tests and temporary instrumentation, by you and by the people, services and automated agents working for you, on machines you control or that run for you, such as rented cloud machines, continuous-integration services and AI coding services. You must not let those working for you reuse the software for their own purposes, develop a competing product, or train a general-purpose model on it.
150
+
151
+ **Spawnite** is the game service the licensor operates at spawnite.com, including its apps and the embeds and links it provides for games published on it.
152
+
153
+ A **game** is software that includes or requires any part of the software to be played, with the content it ships with. A level, asset, save or recording you share on its own is not a game.
154
+
155
+ To **make a game available to the public** is to let anyone play it other than you, your company and private testers. **Private testers** are people you choose one by one, who reach the game through access you control for testing. Testers may be recruited in public, but access open to everyone who signs up, asks, pays or follows a link is public. Calling a release a test, preview or beta does not make it private.
156
+
157
+ The **development room server** is the room server the licensor distributes for development and testing, and every copy and permitted adaptation of it, wherever it runs and whatever it is called.
158
+
159
+ A **game engine product** is a reusable environment for developers to make, simulate or render games they develop independently of each other. A game, and the tools made only to develop or test that game, are not game engine products because they hold simulation, rendering or editing features, and neither is a plugin, kit or tool that works only with the software.
160
+
161
+ A **product** can be a good or service, or a combination of them.
162
+
163
+ **You** refers to the individual or entity accepting these terms.
164
+
165
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
166
+
167
+ **Your licenses** are all the licenses granted to you for the software under these terms.
168
+
169
+ **Use** means accessing, running, copying, changing, analysing or otherwise employing the software, or employing its outputs or observed behaviour.
package/README.md CHANGED
@@ -1,3 +1,153 @@
1
- # Temporary Holding Version
1
+ # @spawnite/room
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The development room: the server for one multiplayer room on your own machine, which plays a game exactly as the platform's rooms do. It builds the game's world headless, steps it 60 times a second, and streams it to every connected player as the engine's replication protocol: a snapshot on join, then a delta at the game's send rate, 60 times a second unless its `room` settings or `SEND_RATE` ask for 30. A player moves nothing but their own character, through the inputs and walks they send. The world stands the map's rocks and trees, the scatter that blocks a walker, baked from the engine's shapes table, so the room stops a character where her page does. The room bakes its navmesh at start from the same ground and rocks, so a walk finds the path her page finds.
4
+
5
+ ## Run it
6
+
7
+ `pnpm nx serve @spawnite/room` builds the engine and the room, then starts the demo room on port 8787: six monsters that chase the nearest player's character and eight coins. The room prints one JSON line when it starts, one per join and leave, one as a player's connection drops (`dropped`) and as she comes back to her character (`rejoined`), and a heartbeat every 15 seconds:
8
+
9
+ ```text
10
+ {"kind":"heartbeat","room":"local","players":0,"steps":900,"step":900,"stepMilliseconds":{"mean":0.192,"max":25.595},"memoryMegabytes":89.6,"heapMegabytes":{"used":41.2,"afterCollection":36.8,"limit":4144},"stepShare":{"mean":0.012,"max":1.536},"slowSystems":[],"skippedSteps":0,"behindSeconds":0,"cpuShare":0.031,"stepMillisecondsPerPlayer":0.192,"systemFaults":{},"entities":31,"sentBytes":{"total":0,"max":0,"words":0},"sendMilliseconds":{"mean":0,"max":0},"clampedCoordinates":0,"refusedShots":{},"fellBehind":0,"inputs":{"starved":0,"late":0}}
11
+ ```
12
+
13
+ `sentBytes.words` is the part of `total` that each page's own word took: her acknowledgement and her own traits, beside the delta every page shares. `sendMilliseconds` is one send's time, from the dump to the last socket written, over the sends since the last heartbeat that had a player to send to: what a higher send rate costs the room. A room whose scene declares a timeline with parts also logs a heartbeat as each part ends, so no heartbeat's steps span two parts. `step` is the steps the room has run since it started, so a tool lines a heartbeat up with the room's timeline at whatever speed the room ran. `sentBytes` holds the bytes the room sent its players since the last heartbeat, as the payloads of its frames: over all of them as `total`, and the most to one as `max`.
14
+
15
+ `memoryMegabytes` is the process's resident memory. `heapMegabytes` is its JavaScript heap: `used` now, `afterCollection` as the last full collection left it, which is what the room keeps from step to step, and `limit`, the most V8 lets it hold. A room whose kept heap climbs from heartbeat to heartbeat holds something it never lets go, and dies of an out-of-memory error when `afterCollection` reaches `limit`. The first time a full collection leaves the heap past three quarters of the limit, the room logs a `heapNearLimit` line with the same figures, since that death runs no handler of the room's and logs nothing of its own:
16
+
17
+ ```text
18
+ {"kind":"heapNearLimit","room":"local","heapMegabytes":{"used":201.7,"afterCollection":196.3,"limit":259}}
19
+ ```
20
+
21
+ A room that keeps a recording, as `ROOM_REPLAY` sets, adds two fields. `checkpointMilliseconds` is the longest replay checkpoint it took since the last heartbeat, and 0 where it took none. The room counts a checkpoint in the step that took it, so `stepMilliseconds` includes it. `ownStepMilliseconds` has the same `mean` and `max` with each step's checkpoint taken out, which reads as the game's own step, as a room that records nothing runs it:
22
+
23
+ ```text
24
+ {"kind":"heartbeat","room":"local","players":1,"steps":60,"step":1200,"stepMilliseconds":{"mean":1.021,"max":31.5},"memoryMegabytes":221.4,"heapMegabytes":{"used":98.5,"afterCollection":84.1,"limit":4144},"sentBytes":{"total":5120,"max":5120,"words":540},"sendMilliseconds":{"mean":0.31,"max":1.2},"clampedCoordinates":0,"refusedShots":{},"fellBehind":0,"inputs":{"starved":0,"late":0},"checkpointMilliseconds":30.1,"ownStepMilliseconds":{"mean":0.519,"max":1.4}}
25
+ ```
26
+
27
+ `clampedCoordinates` counts the coordinates the stream held to the world limit, 10 km from the origin along each axis, since the last heartbeat: an entity past it that the game should keep inside. The first time an entity is held in a heartbeat's window, the room also logs a `clamped` line naming it by its dump key:
28
+
29
+ ```text
30
+ {"kind":"clamped","room":"local","entity":"0/0/2/monster-0"}
31
+ ```
32
+
33
+ `refusedShots` counts the shots the room refused since the last heartbeat, by the name of the rule that refused each, such as `{"origin":2,"cover":1}`: `armed`, `rate`, `origin` and `cover` are the engine's rules, `weapon` is a weapon the world does not hold, and any other name is the game's own rule. A page runs its player ahead of the room, so a count that climbs in honest play says a rule is too tight for the game. The room also logs a `shotRefused` line for each, naming the shooter's character, the weapon, the rule and its reason, up to four of each player's between heartbeats:
34
+
35
+ ```json
36
+ {
37
+ "kind": "shotRefused",
38
+ "room": "local",
39
+ "player": "15",
40
+ "weapon": "blaster",
41
+ "rule": "origin",
42
+ "reason": "Fired from 4.12 m from the middle of the shooter's body, past the 2.5 m allowed."
43
+ }
44
+ ```
45
+
46
+ `inputs` counts, over every player since the last heartbeat, the ticks the room stepped a character without an input of hers as `starved`, and her ticks it dropped for coming after it stepped them as `late`. A player who joins and has sent nothing yet counts as starved too. Two counts that climb in honest play say her pages run too short a lead for the room's links.
47
+
48
+ A page may send two `input` messages for each of the room's ticks on average, and 120, `inputMessageBurst`, at once, counted on the room's ticks so a page keeps up with a room a tool runs fast. The room drops each input message past that and keeps her connection. It reads a frame that begins `{"type":"input"`, as the engine's page and the bot write one, as an input before it parses it, so a flood costs it no parse, and counts any other input once parsed. The first drop in each heartbeat's window logs an `inputFlooded` line naming her character:
49
+
50
+ ```text
51
+ {"kind":"inputFlooded","room":"local","player":"15"}
52
+ ```
53
+
54
+ `fellBehind` counts the players whose connection the room ended since the last heartbeat because they fell behind. A page that reads the room's sends slower than the room sends them leaves the rest waiting in the room's memory. Once more than `startRoom`'s `maxQueuedBytes`, 1 MB by default, has waited for one page for 30 seconds running, `queueOverSeconds`, the room ends her connection. It logs a `fellBehind` line with her player, her name and the bytes that waited, then a `dropped` line, and keeps her character for her key as for any drop:
55
+
56
+ ```text
57
+ {"kind":"fellBehind","room":"local","player":"15","name":"Ada","queuedBytes":1000412}
58
+ ```
59
+
60
+ The room sends every connection a WebSocket ping frame every 5 seconds, `socketPingSeconds`, which a browser and a `ws` client answer on their own. It ends a connection it has not heard from, by a pong or a message, for 15 seconds, `silenceSeconds`, logs a `silent` line with her player and her name, then a `dropped` line, and keeps her player for her key as for any drop. It ends a connection that sends no join within 10 seconds, `joinSeconds`, and holds at most two connections that have not joined for each of its seats, `unjoinedPerSeat`, ending the oldest past that. A connection counts as a player for `emptySeconds` only once a join seats her. While a tool holds the room's clock, the room sends no deltas, so at each ping it also tells every page its pace, as a `pong`, and a page that waits on the room's word hears it. The room reads these times on the wall's clock, whatever a tool does to its own. A continue's stand-in sockets are none of these connections.
61
+
62
+ ```text
63
+ {"kind":"silent","room":"local","player":"15","name":"Ada"}
64
+ ```
65
+
66
+ The environment sets the rest:
67
+
68
+ | Variable | What it sets | Default |
69
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- |
70
+ | `HOST` | The interface the room listens on. The default keeps it off the network; `0.0.0.0` opens it to every interface, such as for a phone on the same network. | `127.0.0.1` |
71
+ | `MAX_PLAYERS` | The most players the room holds at once, 1 to 50, over the scene's `room` settings. A join beyond it closes with code 4004, but a join the platform's seats confirmed is admitted whatever the count. | the scene's, else `20` |
72
+ | `SEND_RATE` | The room's sends a second, 60 or 30, over the scene's `room` settings: each send carries the world's changes and each player's word on her own character, and the welcome names the rate. | the scene's, else `60` |
73
+ | `PORT` | The port the room serves WebSocket on. | `8787` |
74
+ | `ROOM_HEARTBEAT_SECONDS` | Seconds between heartbeats. The cli sets `1` for a room `spawnite play profile` starts, so it reads the room's steps second by second, and one over the speed for a run of `spawnite play room`, so each heartbeat covers a second of play. | `15` |
75
+ | `ROOM_EDITS` | `1` applies a client's devtools edit of any entity's prop, a number clamped to its declared range; `0` ignores every edit. Off unless set, whatever the `HOST`: a proxy or a tunnel on the same machine makes a public room look like loopback. `1` also turns on the engine's dev warnings, such as a walker's `CharacterCapsuleTrait` cut to fit. The arena's `spawnite.room` sets `1`. | `0` |
76
+ | `ROOM_INVULNERABLE` | `1` makes the character of every player that joins `InvulnerableTrait`, so the damage the engine deals leaves her be, for `spawnite play profile --invulnerable`; `0` leaves them as the game has them. | `0` |
77
+ | `ROOM_DIRECTOR` | `1` lets a tool hold the room's clock over HTTP at `/__director` on the room's port, as `spawnite play` does: `GET` reads it, and a `POST` of `{ "action": "pause" }`, `{ "action": "resume", "speed": 2 }` or `{ "action": "step", "steps": 60 }` holds it, runs it again at a speed over 0 and up to 16, or runs that many steps at once, up to 3600. Each answers `{ "paused", "speed", "step" }`. A `POST` of `{ "action": "set", "subject": "[controlled]", "traits": { "transform": { "position": [1, 0, 2] } } }` writes each trait, by the name a dump files it under and in the shape a dump writes it, onto every entity the subject names, a dump key or a selector, between two steps, as `spawnite play set` does: a walker whose transform it writes stands there at once. It answers the clock and the `entities` it wrote, or 400 naming a subject no entity answers to or a trait no module registers. A recording room records the write, and a continue takes it again on its step. Held, the room runs no step and its time stands still while its players stay joined; running, its time goes at the speed. Each player's budgets, of shots, walks, bag verbs and game messages, fill on the room's time, so what she sends a held room past her budget is refused rather than landing all at once as it runs again. It answers a tool on this machine alone: a request from another host, or one a page from another site sends, is refused with 403. Off unless set, whatever the `HOST`, as `ROOM_EDITS` is. | `0` |
78
+ | `ROOM_REPLAY` | `1` keeps a recording under `.replays/<run>/rooms/<run>` in the working directory while a player is in the room, as a session the dev server files the pages that play in it beside: every action it takes, logged with the step it took it after, and a checkpoint of its whole world every 600 steps, so `spawnite replay rerun`, `state` and `check` continue any moment of it exactly. A game's `spawnite.room` sets `1`; a production room never does. | `0` |
79
+ | `ROOM_CONTINUE` | A plan file the cli writes: the room serves nobody, takes `MAX_PLAYERS`, `SEND_RATE`, `ROOM_EDITS`, `ROOM_INVULNERABLE` and its rejoin hold from the recorded run's `room.json` rather than its own environment, writes the plan's checkpoint back, takes the recorded actions again as fast as it steps, writes its report where the plan says, and exits. `ROOM_GAME` names the scene. | none |
80
+ | `ROOM_SEED` | The seed the world's random draws start from, a whole number from 0, so two rooms on one seed draw alike, as `spawnite play room --seed` sets it. | a fresh one each start |
81
+ | `ROOM_WATCH` | `0` keeps the room from starting afresh when a file its scene loaded changes, as `spawnite play room` sets it, so an edit never ends a measured run; `1` restarts it. | `1` |
82
+ | `ROOM_SAVES` | `0` keeps no save, so every player starts afresh, as every room the cli starts for `spawnite play`, its playtests and its profiles sets it unless `--room-env ROOM_SAVES=1` asks for saves; `1` keeps each player's save of a game that declares one in `.spawnite/saves` in the working directory, the game's folder, as [Saves](#saves) says. | `1` |
83
+ | `ROOM_START_SAVE` | A save file, by its path, that each player starts from in place of her own, once: what she stores after is kept for the room's session, so a rejoin keeps her progress, and nothing is written to the file. `spawnite play start --save` sets it for a room game. | none |
84
+ | `ROOM_HEAP_SNAPSHOT` | When the room writes a heap snapshot, which Chrome's DevTools Memory panel loads, and logs a `heapSnapshot` line with its path: `exit` as it stops, before it closes, or a share of the heap limit over 0 and under 1, such as `0.6`, the first time a full collection leaves the heap past it. One snapshot a start. Writing it holds the room for as long as the write takes, 25 s for a 616 MB heap on a desktop. `spawnite play room --heap-snapshot` sets it. | none |
85
+ | `ROOM_HEAP_SNAPSHOT_FOLDER` | The folder `ROOM_HEAP_SNAPSHOT` writes into, made where it is missing. `spawnite play room` sets the cli's `heap-snapshots` folder in the system's temp folder. | the system's temp folder |
86
+ | `ROOM_ID` | The id the room reports under. | `local` |
87
+ | `ROOM_GAME` | The game's scene, read against the working directory, which is `packages/room` under `nx serve`: a source file, such as a `.tsx`, which the room loads through the game's own Vite, whose config it loads with Vite's `runner` config loader so that `node --watch` on the room sees no temporary file, or a server half's `.mjs` module, which `spawnite build --server` writes and Node imports with no Vite. The room mounts the component the file exports under its name headless on the room's world. On a source file it restarts on the scene afresh when a file the scene loaded changes, logging a `restarted` line that names the file, unless `ROOM_WATCH` is `0`; a half never changes. The file may also export the game's `plugins`, a `bot` that the room's bots play by, and a `timeline`, whose measures the room reads after every step and logs as they change. Each player's character spawns as the scene's `<CharacterSpawn>` says: at its `position`, or round it where another character stands there, with its `facing`, `health`, `stats`, movement, body and the `weapons` she holds. The room refuses a file that exports `movement`: the `<CharacterSpawn>` sets the movement of every player's character. | the room's own demo scene |
88
+
89
+ The `started` line names the seconds between its heartbeats, as `"heartbeatSeconds":15`, and, for a room that records, its run, as `"run":"20260927-174326-l4hl"`, which `spawnite replay check --run` takes. For a scene whose game's Vite config loads the engine's plugin, it names the game's build, as `"build":"a1b2c3d4e5f6"`, which each welcome names and the room compares with each join's, as the conversation below says. For a scene that exports a `timeline`, it names the timeline's shape, as `"timeline":{"measures":{"wave":"gauge","kills":"counter"},"parts":["wave"],"over":true}`, and the room logs a `timeline` line before its first step with every measure's value, at step 0, then one after each step where a value changed, with the ones that changed and, where the run's end changed, `over`:
90
+
91
+ ```text
92
+ {"kind":"timeline","room":"local","step":0,"values":{"wave":0,"kills":0},"over":false}
93
+ {"kind":"timeline","room":"local","step":1206,"values":{"wave":1}}
94
+ ```
95
+
96
+ [Measuring runs](https://wiki.spawnite.com/engine/testing/runs/) says how `spawnite play room` reads them. A process that starts the room with an IPC channel, as the cli does, stops it by sending `"stop"`: the room closes its recording and exits, where Windows would end it on a signal without running its handler.
97
+
98
+ The `started` line also names each stage of the room's start in whole milliseconds, so a slow start says where its time went, as `"stageMilliseconds":{"loaded":320,"imported":2385,"mounted":394,"navMeshBaked":108,"voiceOpened":8,"listening":5}` for Holdfast's scene through its Vite. The stages run in that order: `loaded`, from the process's start to the room's, Node and the room's own modules; `imported`, opening the scene's source, which starts a game's Vite for a source file, and importing its module and the engine's mount; `mounted`, the scene mounted headless, its navmesh bakes aside; `navMeshBaked`, every ground's navmesh, the ones the scene's World bakes as it mounts and the ones the room bakes after; `voiceOpened`, the media server a hosted room starts, near 0 here; and `listening`, its port opened. The room's own demo scene names no `imported` and no `mounted`.
99
+
100
+ ## What the room guarantees
101
+
102
+ A room promises the following whatever its game's code does.
103
+
104
+ **A system's throw costs that system its tick, never the room.** A room the platform hosts, which the hosted room's process starts with `startRoom`'s `deployed`, catches a throw from any of the world's systems at the system and runs the systems after it. No environment variable sets `deployed`, so a development room never becomes one. The system runs again next tick, as a Roblox, Unity or Godot script that errored does. Each throw logs a `systemFaulted` line, once for each of the system's messages between heartbeats and at most four, `faultLinesPerSystem`, so a system that throws every tick writes four lines a heartbeat at most:
105
+
106
+ ```text
107
+ {"kind":"systemFaulted","room":"local","system":"waves.spawn","tick":5120,"streak":1,"message":"Cannot read properties of undefined (reading 'x')","stack":"TypeError: ..."}
108
+ ```
109
+
110
+ `streak` counts the ticks in a row the system threw. The heartbeat's `systemFaults` counts every throw since the last heartbeat by system, such as `{"waves.spawn":900}`: a count equal to `steps` says the system fails every tick. A development room and a headless world catch nothing, so a creator's agent sees the throw at once.
111
+
112
+ **A room that has to close stores every player's save first.** A throw outside the systems closes any room: the step's own work, the send, the saves, a timeline's read, the heartbeat, or one the room's process hands to `Room.fail`, as the hosted room does with every uncaught throw and rejection. A system's throw in a development room closes it too. The room logs a `failed` line with the steps it ran, the message and the stack. It stores every player's save as a stop does, within `stopSaveSeconds`, and closes each page with 4012, the engine's `roomFailedCloseCode`. The reason is JSON such as `{"saved":true}`, which says whether her save landed. It then frees its seats with the platform and calls `onFailure`, which exits the process with 1. Her page takes a new seat at once where it can, which loads that save, and otherwise joins the same address again as a new player.
113
+
114
+ **A room that cannot hold sixty steps a second says so, and never spirals.** Each turn of the room's loop runs the steps the time since the last turn buys, at most `maximumCatchUpSteps`, 6, as a page's loop caps a hitch at `maximumFrameSeconds`. It drops the time past them, so its world runs slower than the wall and each page re-paces off its pongs. It never runs more steps to catch up, so a slow step cannot make the next turn slower. The heartbeat reports the cost:
115
+
116
+ - `stepShare` is the step's time as a share of a tick's 16.7 ms, its mean and worst. Past 1, the room cannot keep up.
117
+ - `slowSystems` names each system whose mean took more than `slowSystemShare`, a tenth of a tick, with its mean and worst shares, slowest first.
118
+ - `skippedSteps` is the steps' worth of time the room dropped, and `behindSeconds` is the seconds in which it dropped at least a step's time.
119
+ - `cpuShare` is the process's CPU time over the wall's, as a share of one core, its collections included. `stepMillisecondsPerPlayer` is the step's mean over the players seated, counting at least one, so an empty room reads its whole cost.
120
+
121
+ After `behindLogSeconds`, 5 seconds in a row behind, the room logs a `behind` line with the seconds, the steps skipped and the slow systems of the last second, then a `caughtUp` line once a second passes with nothing dropped. The room lowers no rate and sheds no player: it reports, and the platform judges.
122
+
123
+ **A view that keeps what it adds is named.** A room mounts its game's scene headless and draws no frame, so a view that adds objects on an event and clears them only on a frame keeps every one. The heartbeat counts the world's `entities` and, for a mounted scene, its `sceneObjects`. Once the objects grow at `sceneGrowthHeartbeats`, 4 heartbeats in a row, while the entities do not, the room logs one `sceneGrowing` line. The line names the parent whose children grew most, by the names or types from the scene down to it:
124
+
125
+ ```text
126
+ {"kind":"sceneGrowing","room":"local","heartbeats":4,"sceneObjects":5210,"entities":48,"parent":{"path":"Group > tracers","children":5184}}
127
+ ```
128
+
129
+ ## Saves
130
+
131
+ A game whose scene file exports `save`, the declaration `defineSave` made in `src/save.ts` and the one `<Game save>` takes, saves each player in the room by checkpoints, as [the saves page](https://wiki.spawnite.com/engine/structure/saves/#saves-in-a-room) says: every player's record read at one tick, each that changed uploaded, and all committed at once through the room's store, so what the game moved between two players is in both saves or in neither. One checkpoint is on its way at a time, and the room starts 12 a minute at most. It takes one every 60 seconds while any record changed, every 2 minutes while it holds one player, within 5 seconds of a game's `requestSave(world)`, at once for an ask, a takeover or the last player's leave, and as the room stops. The room loads her save as she joins, before her character spawns, restores it after her spawn, so a saved value wins over a spawn default, then, once every `onPlayerReady` has run, reads her record once as a trial and measures it, as JSON and gzipped, as the game's version reads it after its migration steps: a trial that fails or a record past the size limit refuses the join and leaves the other players as they were. Her leave is read in the tick she leaves, before her character goes, and the checkpoint that holds it frees her seat. A record that cannot be read twice in a row, a record past the platform's ceiling, a commit the store fences or calls invalid, and a checkpoint 45 seconds old with no commit each end the room: every page closes with 4012 and `saved: false`, and each player goes back to the last checkpoint. A record past the size limit is committed with the room's, then its player is removed by her ordinary leave, whose commit holds her out for 10 minutes. Her character's `PlayerSaveTrait` reads `saved`, `unsaved`, `saving` or `refused`, and each failure logs a `saveFailed` line.
132
+
133
+ A development room keeps each save as a JSON file in `.spawnite/saves`, by the name the page sends, and commits it through the same checkpoints, so a restart or a rejoin under the same name restores her progress. A commit writes every player's file it names, or none: it writes each to a draft, puts the list of drafts in place as `.commit.json`, then moves each over its file, and a stop or a move that fails leaves the list, which the room finishes before it loads or commits again. Until then each load fails and says why, so no load reads one player's file beside another's from the commit before. A second page under a name already playing is refused with 4007, since both would write one file. The file is the name, its characters but letters, digits, `_` and `-` as `_`, then a hash of the whole name, as `Ada-3f2a9c1e0b7d4a65.json`, so `Ada` and `ada` keep apart on a file system blind to case. `ROOM_SAVES=0` keeps none. A room that records, `ROOM_REPLAY=1`, keeps with each join the record it restored, so a continue restores the same with no store. A save the room cannot load closes her join with 4009 and writes nothing over it, and a save a newer version of the game wrote closes it with 4010; each reason is JSON such as `{"key":"game","retry":false}`, and the room logs a `saveFailed` line with what failed. A save that declares the world's scope, or names `levels`, `loot` or `random`, stops the room as it starts: a room's world is shared, so one player's save must not restore it.
134
+
135
+ A room the platform runs keeps each save on the platform instead.
136
+
137
+ ## Bots
138
+
139
+ `dist/bots.js` plays a room with bots and no browser, as `spawnite play room` runs it: each a player that joins over the room's WebSocket, sends the input for the ticks to the room's next send at each of its sends, 20 a second of the room's time, naming the room's next tick two sends ahead, turns to the nearest living entity with health, or keeps to the one she chose where the scene's `bot` names a skill, walks to it round walls along the room's navmesh or strafes beside it, or walks to the place the scene's `bot` names, fires at it the first weapon her character holds of those the `bot` names, and sends the game messages its `play` sends. It loads the scene `ROOM_GAME` names as the room does, a source file through the game's own Vite and a server half's `.mjs` through Node, for its `bot` and for the traits its modules register, plays the room at `ROOM_URL` with `BOT_COUNT` bots, 1 by default, for `BOT_SECONDS`, 60 by default, and prints each bot's report as a JSON line of kind `bot`. `BOT_SEED` starts each bot's own random stream, which the scene bot's `skill` draws her aim's stray from, mixed with which bot she is, and `spawnite play room` sets it to the run's seed. `BOT_SKILL` sets numbers over that skill as JSON, or `none` for a perfect aim. With `BOT_JOIN_FILE` set, they load the scene at once and join once that file exists, as `spawnite play start --bots` has them join after its page. `BOT_LATENCY`, `BOT_JITTER` and `BOT_STALL` play each bot over a slow link, as `spawnite play room --latency`, `--jitter` and `--stall` set them: a round trip and a jitter in milliseconds, and a stall as `<milliseconds>/<every seconds>`, such as `500/10`. Each message she sends and each the room sends her waits half the round trip, in order. A page in development plays over the same link from its address, as [Multiplayer](https://wiki.spawnite.com/platform/multiplayer/#play-it-on-a-slow-connection) says. [Multiplayer](https://wiki.spawnite.com/platform/bots/) says what a scene's `bot` holds.
140
+
141
+ ## The conversation
142
+
143
+ A client opens a WebSocket and sends a `join` with the player's name and `protocolVersion`, the engine's version of these messages. A join that names another, or none, closes with 4006, whose reason names both, as `{"room":"protocol 6","page":"protocol 5"}`, before the room asks for her seat or records the join; the engine's page reads it as the room's verdict and does not join again. The room takes the join on its next tick: it makes her player entity, runs each plugin's `onPlayerJoin`, where the `characters` plugin spawns her character unless the game set `characters({ autoSpawn: false })`, restores her save and runs each `onPlayerReady`. A hook that throws refuses the join: a development room throws, and a deployed one destroys what her join spawned, frees her seat and closes her with 4014, `joinRefusedCloseCode`, whose reason names the plugin. At its next send the room answers a `welcome` carrying its own `protocolVersion`, her player entity's id as `player`, her character's id as `character` where she has one, the whole world, whether it takes devtools edits, `composition`, the step its world composed, which a page of another composes refuses, `tick`, the room's tick the snapshot stands after: its steps counted from 0, the last one the snapshot holds the result of, `predicted`, the traits her acknowledgements carry, and `rewound`, her values of them exact at the snapshot, and `sendRate`, the room's sends a second, which her page draws others by. A page that meets a welcome of another version closes the socket before it takes it. The room sends binary frames, the engine's codec, to a join that asks for `"wire": "binary-7"`, as the engine's client does. A binary frame holds one message or several one after another, each spelling its own strings: at each send, a page's frame holds the delta every page shares and then her own word on her character, her acknowledgement and, to a page that reads `owned` messages, her own traits, so the two leave in one write. JSON text holds one message a frame. Any other join gets the same messages as JSON text, to read as they arrive: one that names no wire or `null`, `"json"`, or a codec version this room does not speak. A join whose name holds half of a character, a lone UTF-16 surrogate, closes with 4000. The welcome also carries `rejoin`, a key for this player: a later join that sends it as its own `rejoin` gets the same player back, with whatever she plays at that tick, while the room holds her, as the close below says. Each welcome gives a new key, made from a secret the room keeps and the count of keys before it, so no page guesses another's and a continue makes the ones the room made. A key the room does not keep joins her as a new player. A join may carry `build`, the page's build, which the engine's Vite plugin reads from the game's code and the engine's as it builds the page. A room on a game's scene reads its own the same way, from the code it runs, and names it in each welcome as `build`. The room admits a join that names another build and logs a `buildMismatch` line with both, as `{"kind":"buildMismatch","room":"arena","build":"a1b2c3d4e5f6","page":"0f9e8d7c6b5a"}`. A join that names none, or `null`, logs nothing, as from a dev server's page or a bot. So does every join to a room with no build, such as the demo room, whose welcome names none. The client sends JSON text, but for its input messages, which go as bytes in the same codec where its join asked for the binary wire; the room reads either for any message. Every frame carries the world at the stream's precision: positions and speeds to the centimetre, rotations to a thousandth of each component, and a position past 10 km from the origin along an axis held at 10 km. Every delta after that builds on that snapshot. Beside what changed, a delta carries `events`: every event the room's world held since the last send, in the order they landed, each as `{ entity, name, value }`, the entity by the dump's key and the value as the dump writes it, with an entity it names by its key too. A delta with none leaves the field out, and no hash covers it. A delta's `hash` and a welcome snapshot's are the hash of the world they leave her with, where the room hashes its sends: while it records, for a continue and a page replay to match its sends by, and in a room `startRoom` starts with `hashSends`. Elsewhere, as in every room the platform hosts, they are 0, since the hash stringifies the whole world at each send and nothing in a page's play reads it. A client lands each event on its copy of the entity and takes it off as the next delta lands. The client then sends an `input` message after each frame's steps, carrying the input of each tick the frame stepped: at 60 frames a second, one tick a message. Its `tick` names the room's tick its first input is meant for, any whole tick from 0, and its own fields are that input: `intent`, `steering`, `heading`, and `strafe`, `pitch` and `jump` where they are set, and the game's held inputs: `buttons`, a bit for each button the welcome's composition lists that she holds, left out for none, and `axes`, one whole step for each of its axes, 0 its rest, below 0 under it and above 0 over it, 254 steps across the axis's range split by each side's span, left out where all rest. Its `next` lists the input of each tick after it in the same shape, 0 to 11 of them, empty for a frame of one tick, so a message holds 1 to 12 ticks, `maximumInputTicks`. A message with more ticks, no `next`, a tick that is not a whole number from 0, a bit past the game's buttons, or axes of another count or past their steps, closes the connection with 4000. The room writes the held inputs a game declares `seenByAll` into her `seenInput` trait, which every page holds. The room writes each tick's input into her character as it steps the tick, whatever a game system wrote there since the last tick, as her page writes it. The room files each tick's input at the tick it names and steps every player's character once a tick, never waiting for one and never stepping one twice. A tick it holds her input for, it steps her on it. A tick it holds none for, it steps her on her last input without the jump for 15 ticks, `inputRepeatTicks`, then on a still input that keeps her heading and stops the path she walks. It drops a tick's input whose tick it has stepped already and makes the newest such input what it repeats. Such an input's jump runs once, on the room's next tick, where it comes up to 17 ticks late, the engine's `lateEdgeTicks`, for a tick newer than every one of hers the room has stepped or heard late, so a copy neither repeats a jump nor adds one; a jump later than that is dropped with its tick. It drops a tick's input more than 60 ticks ahead, `inputAheadTicks`, and a second copy of a tick it holds, so the first copy wins. It sends each delta every tick at 60 sends a second and every 2 ticks at 30, the engine's `readSendTicks(sendRate)`, and after it an `ack` to each client naming, as `sequence`, the ticks it has stepped, whatever it stepped her on: one more than the tick it last stepped, and the number her page gives its move for that tick, so the client can check where the room has her against where the same tick left her on its end. The acknowledgement is her own, apart from the delta every client shares, and belongs to the same send: on the binary wire it follows the delta in the same frame, and in JSON it comes as the next frame. To a join that carries `"baseline": true`, as the engine's page sends, each acknowledgement is its whole word on her character at that tick: `owned`, what changed of the traits it sends her alone, her `inventory`, her `equipment` and her `cooldowns`, as a delta writes one entity's traits, and `rewound`, her `transform`, `velocity`, `turn` and `jump` after the room stepped that tick, every number exact rather than rounded as the stream rounds. Each is left out where nothing changed since the last acknowledgement that carried it. `grants` names each predicted trait a rule set with `grantPredicted` since the last acknowledgement that carried any, so her page counts the correction it brings as the grant; it is left out for none. To any other join, such as a bot's, the acknowledgement carries the number alone, and after it a client whose character's own traits changed since it was last told gets an `owned` message with what changed. No other client's stream, and no hash, carries them. A checkpoint keeps, for each player, whether she asked and what her last acknowledgement carried. A room run records its shape, the engine's `recordingFormat`, which a change to these messages raises, and a continue of a run of another shape refuses at its start. An `item` message asks the room for one verb on the client's own bag or worn items: `move` (with a `count`, a split), `use` (with a `target` by the dump's key), `equip`, `unequip` or `drop`. The room runs it on her character, and the next acknowledgement or `owned` message carries what it made. An intent with a slot that is not a whole number from 0, a count that is not one from 1, an equipment slot the engine does not have or an action it does not know closes the connection with 4000; a slot her bag does not have is the verb's to refuse, and changes nothing. An `edit` message carries one prop of one entity's trait, by the dump's names, as the devtools edit it. Where `ROOM_EDITS` allows, the room writes it, and the write streams to every client with the rest of the world. A number is clamped to its prop's declared range and refused where the prop declares none, but for the World's day clock, `dayClock`, which declares no ranges: its `hour` wraps into the day, so 42 is 18, and its `realSecondsPerCycle` is refused where the clock cannot run on it, as `<World dayLengthSeconds>` refuses one. A boolean is gated by declaration the same way: it is refused where the trait's behaviour declares no ranges, as Jump's `grounded` is. A well-formed edit the room refuses, on a refusing room or of an entity, trait or prop it does not hold, is ignored, and the connection stays open. A `walk` names a destination, or `null` to stop, and `tick`, the room's tick her client started it on. The room walks her character along its own navmesh from that tick, at once where it has passed it. Each player may start eight walks at once and five a second after that, counted on her ticks the same way on both ends. A walk past that budget stops her, as it stops her on her own page. A walk the room's navmesh refuses, as the engine's `walkTo` refuses one, stops her too, since her page dropped her last walk for it, and logs a `walkRefused` line naming her character and the refusal, such as `{"kind":"walkRefused","room":"local","character":"15","reason":"destination-off-mesh"}`, within the allowance of four between heartbeats that her dropped messages share. A `message` carries one of the game's own messages: a name the scene registered and a payload in that name's shape. The room drops one whose name is not registered, whose payload is of another shape, or one of whose text fields runs past the engine's `maximumTextBytes`, 4,096 bytes of UTF-8, whatever its `maxLength`, and logs the drop with the reason `unregistered`, `malformed` or `tooLarge`, and the connection stays open; it drops one past the name's rate unlogged, and a malformed one spends its credit too. The rest wait for the next step, where the game's systems read them with the character who sent each. While a round's restart mounts the room's scene again, the room steps no more and holds every message but a `ping` and voice, a join, a leave and a close among them, and takes each in the order it came once the scene has mounted, before its next step; a `pong` meanwhile carries `rate` 0. A `press` carries a press of a message the scene registered as predicted: `tick`, the tick her page ran it on, `ordinal`, its place from 0 to 3 among her presses of that tick, the name and the payload. The room runs it on that tick, after her presses before it, within the name's credit, which her character's `pressCredit` trait counts on the room's ticks: a burst of two, then the name's rate. It ignores a press at or under the last one of hers it took, so a second copy runs nothing and its first answer stands; it runs one whose tick it has stepped on its next tick, where the press comes up to 17 ticks late, charged once on that step and earning credit only to its own tick, drops a later one as `late`, ignores one 60 or more ticks ahead, and drops one of another shape as `malformed`. To a join that carries `"presses": true`, as the engine's page sends, each acknowledgement carries `presses`, its answer on each press it settled since the last, at most 64: `{ tick, ordinal, status }`, with `reason` and `detail` for a refusal, the reason for a drop, and `ranTick`, the tick that ran it, for a press that ran late. A `message` under a predicted name, from a bot, is a press on the room's next tick that nothing answers. A checkpoint keeps each player's last press, the presses waiting, the answers not yet sent and a late jump waiting for the next tick; a rejoin starts them over and keeps her credit on her character. A `shot` names a weapon the world registered, the room's step the page drew as it fired, an origin and a unit direction. It may carry `id`, the page's own whole number for it, and `hits`, the keys of the targets each pellet hit on the page's screen. The room runs the shot's rules, the engine's and the game's, as the [weapon page](https://wiki.spawnite.com/engine/behaviours/weapon/) lists them, then judges it: against where each target stood at the step, held to the player's latency as the room measures it from her shots and at most a second back, or by the hits the page claims where the game trusts the page. A refused shot that carries an `id` is answered with `shotRefused`, naming the `id`, the rule and its reason; a shot with none is answered with nothing. The room keeps each player's latency and last shots on her character, as the engine's `LatencyTrait` and `FiredShotsTrait`, and its last second of sends as the world's `RewindTrait`, so a checkpoint keeps them and a continue judges each shot as the room did. A `ping` carries the client's clock, and the room answers it at once with a `pong` that sends that time back with the room's step, to a thousandth of a step, so the client can estimate the room's step and the round trip. The pong also carries what her input met since the last pong, which her page paces its lead by: `early`, the fewest ticks ahead of its first tick that one of her input messages came, negative for one that came late, and `earlyTick`, that message's first tick, both left out where none came; `late`, her ticks it dropped for coming after it stepped them; and `starved`, the ticks it stepped her on without an input of hers, each left out for none. It also carries `refusedShotOrigins`, the player's own shots refused for their origin since she joined, left out for none, and `rate`, the room's steps for each step of the wall's time where a tool holds its clock at other than real time: 0 held, 4 at speed 4, and left out at 1, so a page names the step it saw at any speed. The engine's client pings once a second from its join, and at once as each welcome lands. As a tool pauses, resumes or steps the room's clock, the room also sends every page that has pinged a `pong` at once, as the answer to a ping sent then: the time the page's last ping carried, plus the wall's time since it landed. The page takes the new pace from it as from any answer, rather than up to a second later at its next ping. A `leave` says the player left on purpose: on its next tick the room runs each plugin's `onPlayerLeave`, takes her player out, frees her seat with the platform, and closes her socket with 1000. On any other close, the room holds her player on its next tick, runs each plugin's `onPlayerDisconnect`, and stands her character still, which the `characters` plugin marks with the engine's `DisconnectedTrait` trait, for a join with her key to take back, which runs each `onPlayerReconnect`: 15 seconds, `closedHoldSeconds`, for a socket closed with a close frame, as a browser closes one on a reload, a closed tab or a navigation, and 30 seconds, `brokenHoldSeconds`, for one that ended with no close frame, as a network that went or the room's own end of a silent or slow connection does. A game's `rejoinHoldSeconds` in `defineRoom` sets one hold for both, up to 300 seconds. A key that names a player who has left joins as a new player once her last save has stored, so the join loads it; in a hosted room her spent ticket is refused with 4007 and her page takes a new seat. A game that destroys her character leaves her player connected, with no character until the game gives her one. A game's `kick` takes her out after the step that asked, stores her save and closes her with 4013, `kickedCloseCode`, whose reason is the game's sentence, and her page does not join again. The `dropped` line names the hold as `holdSeconds`. The room counts the hold in its steps, so a continue lets her go on the same step: a recorded close notes `broken` where no close frame came, and a checkpoint keeps each held player, her key, the secret and the accounts that left. After the hold, the room runs each plugin's `onPlayerLeave`, takes her player out and frees her seat with the platform, telling it how long it held her, so a seat her reload asked for again during the hold stays for her late page. `startRoom`'s `rejoinSeconds` sets one hold for every drop in place of the two, for a test or a tool, and 0 takes her player out as her connection closes.
144
+
145
+ A development room runs no voice, which the platform runs for a published game. It answers a page's voice `open` with `unavailable`, and passes every other voice signal by. The page then says, in its Audio settings, that voice runs when the game plays on the platform, and a game's voice hooks read as a room where nobody speaks.
146
+
147
+ The room closes a connection whose frame is not a client message with code 4000, an `edit` whose value is not a finite number or a boolean, such as a string or `null`, among them, an `input` whose `next` is not a list of 0 to 11 ticks, each with a two-number `intent`, a `steering` flag and a finite `heading`, one whose `tick` is not a whole number from 0, a `walk` whose `tick` is not one, and a `press` whose `tick` is not a whole number from 0 or whose `ordinal` is not one from 0 to 3. It closes one that sends an input, an edit, a walk or a ping before joining with 4001, one that joins twice with 4002, one whose player a later join took back with her key while this connection was still open with 4005, one that sent `leave` with 1000, so the page it belonged to stops, one whose `build` is not 1 to 64 letters, digits, dots, dashes and underscores with 4000, and one that joins a full room with 4004, whose reason is JSON such as `{"players":4,"maxPlayers":4}`: the players it holds, each whose character it keeps among them, and the most it may. A join with the key of a character the room keeps is never refused as full. A frame the room fails to handle closes its connection with 4500 and leaves the room running. A frame over 32 KiB, the engine's `maximumFrameBytes`, eight times `maximumTextBytes`, closes it with 1009 before the room reads it; the engine's page refuses such a message before it sends it, and reads a 1009 as its own message too large rather than the room gone. The engine's `armed` rule refuses a shot from a character with the engine's `DisarmedTrait` trait, as it refuses a downed character's. A connection more than `maxQueuedBytes` behind the room's sends for 30 seconds ends with no close frame, since a close frame would wait behind the bytes its page is not reading, so the page sees 1006 and rejoins. So does a connection the room has not heard from for 15 seconds, and one that sends no join within 10. A room that stops closes every connection with 1001, WebSocket's going-away code. A room that stops on an error closes each with 4012, `roomFailedCloseCode`, after storing every player's save, and her page takes a new seat at once, as [the room's guarantees](#what-the-room-guarantees) say. A room whose process is killed, as Node's watch mode does on Windows, drops them, and the client sees 1006. On either code, and when a page opens before its room is listening, the engine's client tries the room again after 1, 1, 2, 4 and then every 8 seconds, each gap up to half shorter at random, until it answers, then joins it again with its rejoin key. It stops after a minute with no socket open. A reconnect that the room closes with 4007 or 4004 joins once more without its key, on a new seat where the platform hosts the room, as a new player whose save the room loads. A joined page that hears nothing from the room for 15 seconds ends its socket and does the same. The messages are in the engine's replication protocol.
148
+
149
+ Run `pnpm nx test @spawnite/room` to run its tests.
150
+
151
+ ## License
152
+
153
+ The Spawnite License 1.0, in [LICENSE.md](LICENSE.md). [License](https://wiki.spawnite.com/platform/license/) on the wiki says in plain words what it lets you do. The code this package bundles from other projects keeps its own licenses, listed in `THIRD-PARTY-NOTICES` beside the built files.
@@ -0,0 +1,2 @@
1
+ import{createRequire as e,registerHooks as t}from"node:module";import{existsSync as n,readFileSync as r,realpathSync as i,renameSync as a,rmSync as o,writeFileSync as s}from"node:fs";import*as c from"node:path";import{join as l}from"node:path";import*as u from"@spawnite/engine/internal/room";import{CharacterCapsuleTrait as d,ClientMessageType as f,ClientVoiceSignal as p,ControlledTrait as ee,DevWorldTrait as te,EquipmentSlot as m,GroundTrait as ne,HealthTrait as h,HitZonesTrait as re,InputTrait as g,ItemAction as _,JoinRefusal as v,LeaveReason as y,MeasureKind as ie,MessagesTrait as b,NavMeshBakeTimeTrait as ae,NavMeshTrait as oe,PathTrait as se,PlayerSaveStatus as ce,PluginsTrait as le,PoseTrait as ue,PressDropReason as de,PressResultKind as fe,PressStatus as x,ReplayRecordKind as pe,RewindTrait as me,Role as he,RoomActionKind as S,RoomMessageType as ge,RoomSourceTrait as _e,RoomVoiceSignal as ve,SaveScope as ye,ShotJudge as be,ShotJudgedEvent as xe,Simulation as Se,StepProfileTrait as Ce,TransformTrait as we,VoiceDirection as Te,WalkRefusal as Ee,WeaponKind as C,WeaponNumber as De,WeaponsTrait as Oe,WireFormat as ke,accumulateSteps as Ae,addWorldReleaseHook as je,agreeRewoundTraits as Me,appendRoomActions as Ne,applyEdit as Pe,applyItemIntent as Fe,bakeGroundNavMesh as Ie,beginsAsBinaryInput as Le,buildGroundMesh as Re,buildMismatchCloseCode as ze,canPackPastLimit as w,captureControls as Be,captureRewoundTraits as Ve,captureWorld as He,castCoverRay as Ue,checkRecordingFormat as We,collectEvents as Ge,commitControls as Ke,commitQueryRemovals as qe,compareBlobs as Je,compareImages as Ye,composeSave as Xe,compressPieceFile as Ze,control as Qe,copyAxes as $e,copyStepInput as et,createGameWorld as tt,createHeadlessGame as nt,createRecordWriter as rt,createSaveSession as it,createTickPress as T,createWalkCredit as at,dealDamage as ot,decodeClientMessage as st,defaultCharacterCapsule as ct,deliverMessage as lt,deliverPress as ut,derivePredictionSeed as dt,describeProtocol as ft,diffSnapshots as pt,diffTraits as mt,disconnectPlayer as ht,dressGround as gt,dropMessagesFrom as _t,dumpEntities as vt,dumpKey as yt,dumpOwnedTraits as bt,emitEvent as xt,encodeRoomMessage as St,engineBlobs as E,engineTraits as Ct,equalJson as wt,fanPellet as D,findController as Tt,findDumpedEntity as Et,findEntityKeys as Dt,findFreeSeat as Ot,findPlayerCharacter as kt,findRewindSends as O,findShooterMiddle as At,finishLeave as jt,fixedStepSeconds as k,handleKicks as Mt,handleRemadePlayers as Nt,hashEvents as Pt,hashSnapshot as Ft,heartbeatIntervalSeconds as It,isAngleStep as Lt,isBodyDeclared as Rt,isItemCount as zt,isNumbers as A,isPayloadShaped as j,isPhysicsSettings as Bt,isPlugin as M,isRecord as Vt,isSaveDeclaration as Ht,isSpared as Ut,isTextTooLarge as Wt,isTrait as Gt,isVectorStep as Kt,isWellFormedChanges as qt,isWellFormedText as N,isolateSystemFaults as Jt,joinPlayer as Yt,joinRefusedCloseCode as Xt,joinRoomFrames as Zt,kickedCloseCode as Qt,lateEdgeTicks as $t,launchProjectile as en,leavePlayer as tn,leftCloseCode as P,listRoomCheckpoints as nn,loadRapier as rn,loadRecast as an,maximumAckPresses as on,maximumFrameBytes as sn,maximumFrameSeconds as cn,maximumInputTicks as ln,maximumPressesPerTick as F,measureLatency as un,measureLength as dn,measureShotDistance as fn,movementRewoundTraits as pn,nameRoomRun as mn,nameTraits as hn,outlineTimeline as gn,pieceMilliseconds as _n,pingIntervalSeconds as vn,playingElsewhereCloseCode as yn,pressAheadTicks as bn,pressBurst as xn,protocolVersion as Sn,readCheckpointValue as Cn,readControlled as wn,readFailure as Tn,readInputLayout as En,readListedPlugins as I,readPlayerRecords as Dn,readPredictionSeed as On,readReplayMeta as L,readRoomActions as kn,readRoomCheckpoint as An,readRoomRunMeta as jn,readSceneReloads as R,readSendTicks as Mn,readSentFacing as Nn,readSentPlace as Pn,readSentZones as Fn,readStepComposition as In,readStepProfile as Ln,readTimeline as Rn,readWeaponNumber as z,readWirePayload as zn,reconnectPlayer as Bn,recordFiredShot as Vn,recordRewindSend as Hn,recordingFormat as Un,releaseForLevel as Wn,requireAuthority as Gn,reseatWorld as Kn,resolveRoomPieceFolder as qn,resolveShotJudge as Jn,resolveShotRules as Yn,restoreControls as Xn,restorePlayerRecords as Zn,restoreWorld as Qn,roomActionsFile as $n,roomCheckpointSteps as er,roomFailedCloseCode as tr,roomFullCloseCode as nr,roomSaveErrors as rr,roundSnapshot as ir,roundTo as B,routeInput as ar,saveNewerCloseCode as or,saveNotLoadedCloseCode as sr,scaleZoneDamage as cr,seatRefusedCloseCode as lr,seedPrediction as ur,seedRandom as dr,setRewindStep as fr,setRoomCharacterOptions as pr,setWorldTick as mr,spawnPlayer as hr,spendWalkCredit as gr,stepWorld as _r,takeEventFigures as vr,takeEvents as yr,takeSaveRequest as br,takeStepWindow as xr,tiePlayerCharacter as Sr,versionsOf as Cr,walkBurst as wr,walkTo as Tr,writeCheckpointValue as Er,writeEntityTraits as Dr,writeHeldInputs as Or,writePlayerSaveStatus as kr,writeReplayChunk as Ar,writeReplayMeta as jr,writeRoomCheckpoint as Mr,writeRoomRunMeta as Nr,writeSessionMeta as Pr}from"@spawnite/engine/internal/room";import{Quaternion as Fr,Ray as Ir,Vector3 as Lr}from"three";import{GCProfiler as Rr,getHeapStatistics as zr}from"node:v8";import{PerformanceObserver as Br}from"node:perf_hooks";import{createHash as Vr,createHmac as Hr,randomBytes as Ur}from"node:crypto";import{createServer as Wr}from"node:http";import{defaultRoomPlayers as Gr,defaultRoomSendRate as Kr,findRoomSettingsProblems as qr}from"@spawnite/schema";import{WebSocket as V,WebSocketServer as Jr}from"ws";import*as Yr from"@spawnite/engine/rounds";import{isIPv4 as Xr}from"node:net";import{mkdir as Zr,readFile as Qr,rm as $r,stat as ei,writeFile as ti}from"node:fs/promises";import{promisify as ni}from"node:util";import{gzip as ri,gzipSync as ii}from"node:zlib";import{createQuery as ai}from"koota";import{pathToFileURL as H}from"node:url";var oi=.75,si=e=>Math.round(e/2**20*10)/10;function ci(e,t=oi){let n=0,r=!1,i=()=>{let{used_heap_size:e,heap_size_limit:t}=zr();return{used:si(e),afterCollection:n,limit:si(t)}},a=new Rr;a.start();let o=new Br(()=>{let{statistics:o}=a.stop();a.start();for(let{gcType:a,afterGC:s}of o){if(a!==`MarkSweepCompact`)continue;n=si(s.heapStatistics.usedHeapSize);let o=i();r||n<t*o.limit||(r=!0,e(o))}});return o.observe({entryTypes:[`gc`]}),{read:i,stop:()=>{o.disconnect(),a.stop()}}}var U=/* @__PURE__ */ function(e){return e.Started=`started`,e.Joined=`joined`,e.Left=`left`,e.Dropped=`dropped`,e.Rejoined=`rejoined`,e.Kicked=`kicked`,e.JoinRefused=`joinRefused`,e.FellBehind=`fellBehind`,e.Silent=`silent`,e.Failed=`failed`,e.Heartbeat=`heartbeat`,e.Clamped=`clamped`,e.MessageDropped=`messageDropped`,e.ShotRefused=`shotRefused`,e.WalkRefused=`walkRefused`,e.Emptied=`emptied`,e.Timeline=`timeline`,e.Restarted=`restarted`,e.BuildMismatch=`buildMismatch`,e.SaveFailed=`saveFailed`,e.JoinFailed=`joinFailed`,e.InputFlooded=`inputFlooded`,e.HeapNearLimit=`heapNearLimit`,e.HeapSnapshot=`heapSnapshot`,e.SystemFaulted=`systemFaulted`,e.Behind=`behind`,e.CaughtUp=`caughtUp`,e.SceneGrowing=`sceneGrowing`,e}({}),li=/* @__PURE__ */ function(e){return e.Unregistered=`unregistered`,e.Malformed=`malformed`,e.TooLarge=`tooLarge`,e.MissingEntity=`missingEntity`,e}({});function ui(){let e=performance.now();return()=>{let t=performance.now(),n=Math.round(t-e);return e=t,n}}var di=e=>{console.log(JSON.stringify(e))};function fi(e){let t=e.replace(/^::ffff:/,``);return Xr(t)&&t.startsWith(`127.`)||e===`::1`||e===`localhost`}var pi=3600;function mi(e){if(!(Number.isFinite(e)&&e>0&&e<=16))throw RangeError(`The room cannot run at speed ${e}: give a number over 0 and up to 16, where 1 is real time.`)}function hi(e){if(!(Number.isInteger(e)&&e>=1&&e<=3600))throw RangeError(`The room cannot run ${e} steps: give a whole number from 1 to ${pi}, a minute of its time.`)}function gi(e){let t;try{t=JSON.parse(e)}catch{throw RangeError(`The body is not JSON.`)}let n=typeof t==`object`&&t?Reflect.get(t,`action`):void 0,r=e=>typeof t==`object`&&t?Reflect.get(t,e):void 0;switch(n){case`pause`:return{action:n};case`resume`:{let e=r(`speed`);return e===void 0?{action:n}:(typeof e!=`number`&&mi(NaN),{action:n,speed:e})}case`step`:{let e=r(`steps`);return typeof e!=`number`&&hi(NaN),{action:n,steps:e}}case`set`:{let e=r(`subject`),t=r(`traits`);if(typeof e!=`string`||e.trim()===``)throw RangeError(`A set names its subject: a dump key, or a selector such as [monster.kind=colossus].`);if(typeof t!=`object`||!t||Array.isArray(t)||Object.keys(t).length===0)throw RangeError(`A set writes traits: an object of each trait by its dump name, such as { "transform": { "position": [1, 0, 2] } }.`);return{action:n,subject:e,traits:t}}default:throw RangeError(`"${String(n)}" is no action: ask for pause, resume with a speed, step with a number of steps, or set with a subject and traits.`)}}function _i(e){if(!fi(e.socket.remoteAddress??``))return!1;let t=e.headers.origin;if(t===void 0)return!0;try{return fi(new URL(t).hostname.replace(/^\[|\]$/g,``))}catch{return!1}}var vi=class extends Error{};function yi(e,t,n){e.statusCode=t,e.setHeader(`content-type`,t===200?`application/json`:`text/plain; charset=utf-8`),e.end(n)}function bi(e,t,n){if((t.url??``).split(`?`)[0]!==`/__director`)return!1;if(!_i(t))return yi(n,403,`The director answers a tool on this machine alone, not another host or a page's script from another site.`),!0;if(t.method===`GET`)return yi(n,200,JSON.stringify(e.read())),!0;if(t.method!==`POST`)return yi(n,405,`Ask with GET to read the clock, or POST to act on it.`),!0;let r=[];return t.on(`data`,e=>r.push(e)),t.on(`end`,async()=>{try{let t=gi(Buffer.concat(r).toString(`utf8`)),i=t.action===`pause`?e.pause():t.action===`resume`?e.resume(t.speed):t.action===`step`?await e.step(t.steps):await e.set(t.subject,t.traits);yi(n,200,JSON.stringify(i))}catch(e){yi(n,e instanceof RangeError||e instanceof vi?400:500,e instanceof Error?e.message:String(e))}}),!0}function xi(e){return{...e,count:e.burst}}function Si(e,t){return e.count=Math.min(e.burst,e.count+(t-e.counted)/1e3*e.perSecond),e.counted=t,e.count<1?!1:(e.count--,!0)}function Ci(e){return{presses:e,pressMark:null,pressQueue:[],pressAnswers:[]}}function wi({tick:e,ordinal:t,name:n,fraction:r,entity:i,level:a}){return(i===void 0||typeof i==`string`)&&(a===void 0||Number.isSafeInteger(a)&&a>=0)&&(r===void 0||typeof r==`number`&&r>=0&&r<1)&&Number.isSafeInteger(e)&&e>=0&&Number.isInteger(t)&&t>=0&&t<F&&typeof n==`string`}function Ti(e,t){return!t||e.tick>t.tick||e.tick===t.tick&&e.ordinal>t.ordinal}function Ei(e,t){e.presses&&e.pressAnswers.length<on&&e.pressAnswers.push(t)}function Di({world:e,player:t,press:n,next:r,level:i}){let{tick:a,ordinal:o,name:s,payload:c,fraction:l=0,entity:u}=n,d=e.get(b)?.get(s);if(!d?.predicted||!Ti(n,t.pressMark))return`ignored`;if(t.pressMark={tick:a,ordinal:o},a+$t<r)return Ei(t,{tick:a,ordinal:o,status:x.Dropped,reason:de.Late}),`dropped`;if((n.level??0)!==i)return Ei(t,{tick:a,ordinal:o,status:x.Dropped,reason:de.LevelChanged}),`dropped`;if(a>=r+bn)return`ignored`;let f=xn+Math.ceil(d.messagesPerSecond),p=0;for(let e of t.pressQueue)e.name===s&&p++;return p>=f?`ignored`:j(c,d.shape)?(t.pressQueue.push({tick:a,ordinal:o,name:s,payload:c,fraction:l,level:n.level??0,...u!==void 0&&{entity:u}}),`queued`):(Ei(t,{tick:a,ordinal:o,status:x.Dropped,reason:de.Malformed}),`dropped`)}function Oi(e,t,{name:n,payload:r},i,a){let{pressMark:o}=t,s=o&&o.tick===i?o.ordinal+1:0;return s>=F||o&&o.tick>i?`ignored`:Di({world:e,player:t,press:{tick:i,ordinal:s,name:n,payload:r,level:a},next:i,level:a})}function ki(e,t,n,{tick:r,level:i},a){let{pressQueue:o}=t;for(;o.length>0&&o[0].tick<=r;){let s=o.shift();if(!s)continue;let c=s.level===i?n(s):de.LevelChanged;if(typeof c==`string`){Ei(t,{tick:s.tick,ordinal:s.ordinal,status:x.Dropped,reason:c});continue}if(!c?.isAlive())continue;let l=e.get(b)?.get(s.name)?.shape??{},u=zn(e,l,s.payload);if(u.missing!==void 0){Ei(t,{tick:s.tick,ordinal:s.ordinal,status:x.Dropped,reason:de.MissingEntity,detail:u.missing});continue}let d=T({entity:c,name:s.name,payload:u.payload,origin:{tick:s.tick,ordinal:s.ordinal},result:fe.None,fraction:s.fraction});ut(e,d),a.push({player:t,press:d,tick:r})}}function Ai(e){for(let{player:t,press:n,tick:r}of e){let{origin:e,status:i,reason:a,detail:o}=n;Ei(t,{...e,status:i??x.Ran,...a!==void 0&&{reason:a},...o!==void 0&&{detail:o},...r!==e.tick&&{ranTick:r}})}e.length=0}var ji=1e3;function Mi({folder:e,game:t,port:n,invulnerable:r=!1,maxPlayers:i,sendRate:a,allowEdits:o,rejoinSeconds:s,warn:c}){let u=/* @__PURE__ */ new Date,d={id:mn(u),game:t,startedAt:u.toISOString(),startedAtEpoch:u.getTime(),port:n,pid:process.pid,format:Un,...r&&{invulnerable:!0},maxPlayers:i,sendRate:a,allowEdits:o,...s!==void 0&&{rejoinSeconds:s}},f=l(e,d.id),p=!1,ee=e=>{Nr(f,d),Pr(f,{id:d.id,startedAt:d.startedAt,startedAtEpoch:d.startedAtEpoch,room:{port:d.port,pid:d.pid},...e!==void 0&&{endedAt:e}})},te=()=>Math.max(0,Math.floor((Date.now()-d.startedAtEpoch)/_n)),m=!1,ne=0,h=[],re=/* @__PURE__ */ new Set,g=e=>{re.add(e),e.finally(()=>re.delete(e))},_=Promise.resolve(void 0),v=-1,y=()=>{if(h.length===0)return;let e=h;h=[];try{Ne(f,d.id,e,Math.max(0,v))}catch(e){c(`Replay: the room's actions were not written: ${String(e)}`)}},ie=setInterval(y,ji);ie.unref();let b=({blobs:e,...t})=>{y();let n=te();if(v>=0&&n>v){let e=v;g(Ze(qn(f,d.id,e),$n).catch(e=>c(`Replay: a piece of the room's actions was not compressed: ${String(e)}`)))}v=Math.max(v,n);let r=v,i=Buffer.from(JSON.stringify({...t,actions:ne}));_=_.then(n=>Mr({folder:f,run:d.id,piece:r,step:t.step,json:i,blobs:e,base:n}).catch(e=>(c(`Replay: the room's checkpoint at step ${t.step} was not written: ${String(e)}`),n))),g(_.then(()=>void 0))};return{run:d.id,listen(e){let t=new Date(Date.now());d.port=e,d.startedAt=t.toISOString(),d.startedAtEpoch=t.getTime(),p=!0;try{ee()}catch(e){c(`Replay: the room's session was not written: ${String(e)}`)}},startsPiece(){return m&&te()>v},get active(){return m},resume(e){m=!0,b(e)},pause(e){b(e),m=!1},record(e){h.push({...e,index:ne++})},checkpoint:b,async close(){if(clearInterval(ie),y(),await Promise.all(re),p)try{ee(Date.now())}catch(e){c(`Replay: the room's session was not marked ended: ${String(e)}`)}}}}var Ni=/* @__PURE__ */ function(e){return e.None=`none`,e.Loaded=`loaded`,e.Failed=`failed`,e}({}),W=/* @__PURE__ */ function(e){return e.Moved=`moved`,e.Fenced=`fenced`,e.Stale=`stale`,e.Limited=`limited`,e.Invalid=`invalid`,e.Failed=`failed`,e}({}),Pi=16777216,Fi=4194304,Ii=2*Pi,Li=2*Fi;function Ri(e,t){let n=rr(t,!0),r=Xe(t,I(e));for(let{owner:e,save:t}of r)t.scope===ye.World&&e!==`the game`&&n.push(`${e}'s save has scope "world", and this game has rooms: a room's world is shared, so one player's save must not restore it. Give it scope "player".`);if(n.length>0)throw Error(n.join(` `));return Object.fromEntries(r.map(({key:e,save:t})=>[e,t.version]))}function zi(e,t){return Object.keys(t).find(n=>Object.hasOwn(e,n)&&e[n]<t[n])}var Bi=/^[!-~]{1,64}$/,Vi=1024;function Hi(e){for(let[t,n]of Object.entries(e)){if(!Bi.test(t))return`The save names the key ${JSON.stringify(t)}, and the save worker takes 1 to 64 printable ASCII characters: it was not stored. Rename the kit or the key.`;if(!Number.isInteger(n)||n<0)return`The save names version ${n} for ${t}, and a version is a whole number from 0: it was not stored.`}let t=JSON.stringify(e).length;if(t>Vi)return`The save's versions take ${t} characters, past the ${Vi} the save worker keeps beside a body: it was not stored. Save fewer kits.`}var Ui=123;function Wi(e){let t=Array.from(e);for(;Buffer.byteLength(t.join(``))>Ui;)t=t.slice(0,-1);return t.join(``)}function Gi(e){let t=Array.from(e.key);for(;;){let n=JSON.stringify({...e,key:t.join(``)});if(Buffer.byteLength(n)<=Ui)return n;t=t.slice(0,-1)}}var Ki=ni(ri);function G(e){return`${(e/2**20).toFixed(2)} MiB`}var qi=/* @__PURE__ */ function(e){return e.Fits=`fits`,e.PastLimit=`past-limit`,e}({});async function Ji(e,t){let n=Buffer.byteLength(e);if(n>33554432)return{problem:`The save is ${G(n)} as JSON, past the platform's ceiling of ${G(Ii)}.`};let r=new Uint8Array(await Ki(e));if(r.byteLength>8388608)return{problem:`The save is ${G(r.byteLength)} gzipped, past the platform's ceiling of ${G(Li)}.`};let i=n>16777216||r.byteLength>4194304;return{body:{json:e,gzipped:r,versions:t},size:i?`past-limit`:`fits`}}function Yi(e){let t=Buffer.byteLength(e);if(t>16777216)return`Her save is ${G(t)} as JSON, past the ${G(Pi)} a save may take: her seat is refused until the game's next version trims it.`;if(!w(t))return;let n=ii(e).byteLength;if(n>4194304)return`Her save is ${G(n)} gzipped, past the ${G(Fi)} a save may take: her seat is refused until the game's next version trims it.`}async function Xi(e){try{if((await ei(e)).size>33554432)return{status:Ni.Failed,reason:`${e} is past the platform's ceiling of ${G(Ii)}.`,retry:!1};let t=await Qr(e,`utf8`),n;try{n=JSON.parse(t)}catch{return{status:Ni.Failed,reason:`${e} is no JSON: delete it, or mend it by hand.`,retry:!1}}let r=typeof n==`object`&&n?Cr(n):{};return{status:Ni.Loaded,json:t,versions:r}}catch(t){return t.code===`ENOENT`?{status:Ni.None}:{status:Ni.Failed,reason:`${e} did not read: ${t instanceof Error?t.message:String(t)}`,retry:!0}}}function Zi(){let e=/* @__PURE__ */ new Map,t=/* @__PURE__ */ new Map;return{upload(t,n){let r=Ur(9).toString(`base64url`);return e.set(r,{key:t,body:n}),Promise.resolve({stored:!0,receipt:r})},answerApplied(e){let n=t.get(e.id);if(n!==void 0)return n===Qi(e)?$i:ea},read(t){let n=/* @__PURE__ */ new Map;for(let r of t.heads){if(r.receipt===void 0)continue;let t=e.get(r.receipt);if(t?.key!==r.key)return null;n.set(r.key,t.body)}return n},apply(n){t.set(n.id,Qi(n));for(let t of n.heads)t.receipt!==void 0&&e.delete(t.receipt)}}}function Qi({heads:e}){return JSON.stringify(e.map(({key:e,receipt:t,release:n,holdOut:r})=>[e,t??null,n??!1,r??!1]))}var $i={outcome:W.Moved},ea={outcome:W.Invalid,reason:`The commit's id is one this store applied to another request.`},ta={outcome:W.Invalid,reason:`A receipt the commit names is not one this store answered.`};function na(e){let t=/* @__PURE__ */ new Map,n=Zi();return{load:async n=>{let r=t.get(n);return r?{status:Ni.Loaded,json:r.json,versions:r.versions}:Xi(e)},upload:(e,t)=>n.upload(e,t),commit:e=>{let r=n.answerApplied(e);if(r)return Promise.resolve(r);let i=n.read(e);if(i===null)return Promise.resolve(ta);for(let[e,n]of i)t.set(e,n);return n.apply(e),Promise.resolve($i)}}}var ra=`.commit.json`;function ia(e){let t=l(e,ra),i;try{i=JSON.parse(r(t,`utf8`))}catch(e){if(e.code===`ENOENT`)return;throw e}for(let[t,r]of i)n(l(e,t))&&a(l(e,t),l(e,r));o(t)}function aa(e){let t=e=>`${e.replace(/[^\p{L}\p{N}_-]/gu,`_`).slice(0,32)}-${Vr(`sha256`).update(e).digest(`hex`).slice(0,16)}.json`,n=Zi(),r=t=>`A commit in ${e} is not finished: ${t instanceof Error?t.message:String(t)} Free the file, or delete ${ra} to keep each file as it is.`;return{load:n=>{try{ia(e)}catch(e){return Promise.resolve({status:Ni.Failed,reason:r(e),retry:!0})}return Xi(l(e,t(n)))},upload:(e,t)=>n.upload(e,t),async commit(r){let i=n.answerApplied(r);if(i)return i;let o=n.read(r);if(o===null)return ta;let s=Ur(6).toString(`hex`),c=[],u=`${ra}.${s}.tmp`;try{ia(e),await Zr(e,{recursive:!0});for(let[n,{json:r}]of o){let i=t(n),a=`${i}.${s}.tmp`;c.push([a,i]),await ti(l(e,a),r)}await ti(l(e,u),JSON.stringify(c)),a(l(e,u),l(e,ra))}catch(t){for(let t of[u,...c.map(([e])=>e)])await $r(l(e,t),{force:!0}).catch(()=>void 0);return{outcome:W.Failed,reason:`The saves in ${e} were not written: ${t instanceof Error?t.message:String(t)}`}}n.apply(r);try{ia(e)}catch{}return $i}}}var oa=60,K=12,sa=16,ca=250,la=4e3,ua=/* @__PURE__ */ function(e){return e.Seated=`seated`,e.Leaving=`leaving`,e}({});function da(e){return Vr(`sha256`).update(e).digest(`hex`)}var fa=class{key;player;loaded;session;state=`seated`;committed;leave;holdOut=!1;failedReads=0;#e;#t;#n=ce.Saved;constructor({key:e,player:t,loaded:n,loadedText:r,session:i}){this.key=e,this.player=t,this.loaded=n,this.session=i,this.committed=r===null?null:da(r)}bindPlayer(e){this.#t=e,kr(e,this.#n)}attach(e){this.#e=e,this.session.attachCharacter(e)}readRecord(){return pa(()=>this.session.read())}readLeave(){let e=this.#e,t=pa(()=>e?this.session.detachCharacter(e):this.session.read());return t===null||`problem`in t?t:(this.#e=void 0,this.leave=t,t)}readDestroyed(e){if(this.#e!==e)return null;let t=pa(()=>this.session.detachCharacter(e));return t!==null&&!(`problem`in t)&&(this.#e=void 0),t}setStatus(e){this.#n=e,this.#t&&kr(this.#t,e)}};function pa(e){let t;try{t=e()}catch(e){return{problem:{key:``,owner:`the save`,message:e instanceof Error?e.message:String(e)}}}return t===null?null:t.ok?{text:JSON.stringify(t.record),versions:Cr(t.record)}:{problem:t.problem}}var ma=/* @__PURE__ */ function(e){return e.Unreadable=`unreadable`,e.Overdue=`overdue`,e.Fenced=`fenced`,e.Invalid=`invalid`,e}({}),ha=class{#e;#t;#n=/* @__PURE__ */ new Map;#r;#i=null;#a=null;#o=null;#s=null;#c=null;#l=/* @__PURE__ */ new Set;#u=!1;#d=!1;#f=/* @__PURE__ */ new Set;constructor(e){this.#e=e,this.#t=e.now??(()=>performance.now()),this.#r=this.#t()}get saves(){return this.#n}listLeaving(){return[...this.#n.values()].filter(e=>e.state===`leaving`).map(e=>e.key)}get busy(){return this.#s!==null}async settleLeave(e){let t=this.#n.get(e);t?.state===`leaving`&&this.#l.has(t)&&await this.#s}add(e){let t=this.#n.get(e.key);t&&t!==e&&(e.committed=t.committed),this.#n.set(e.key,e)}leave(e){this.#n.get(e.key)===e&&e.leave&&(e.state=`leaving`)}forget(e){this.#n.get(e.key)===e&&this.#n.delete(e.key)}request(e=0){let t=this.#t()+e*1e3;this.#a=this.#a===null?t:Math.min(this.#a,t)}tick(){if(this.#d||this.#u)return;let e=this.#t(),t=this.#e.dueSeconds??45;if(this.#c!==null&&e-this.#c>t*1e3){this.#x({reason:`overdue`,message:`A checkpoint read ${t} seconds ago has not committed: the room ends, and every player goes back to the last checkpoint.`});return}if(this.#s)return;let n=this.#n.size;if(n===0){this.#r=e,this.#i=null;return}let r=this.#e.intervalSeconds??60,i=n>1?r:r*2;if(e-this.#r>=i*1e3&&(this.#i??=e),this.#a!==null&&e>=this.#a&&(this.#i??=e),this.#i===null||this.#o!==null&&e-this.#o<r*1e3/12)return;let a=this.#p();if(a===null)return;this.#r=e,this.#i=null,this.#a=null;let o=a.filter(e=>e.release||e.digest!==e.save.committed);o.length!==0&&this.#m(o,e)}#p(){let e=[];for(let t of this.#n.values()){if(t.state===`leaving`&&t.leave){e.push({save:t,...t.leave,digest:da(t.leave.text),release:!this.#e.isTakenOver?.(t.key)});continue}let n=t.readRecord();if(n!==null){if(`problem`in n)return t.failedReads++,this.#C(t.key,n.problem.message),t.failedReads>=2&&this.#x({reason:`unreadable`,message:`${t.key}'s record could not be read twice in a row: the room ends, and every player goes back to the last checkpoint. ${n.problem.message}`,unreadable:{key:t.key,owner:n.problem.key,reason:n.problem.message}}),null;t.failedReads=0,e.push({save:t,...n,digest:da(n.text),release:!1})}}return e}#m(e,t){this.#o=t,this.#c=t;for(let{save:t}of e)t.setStatus(ce.Saving);let n=this.#h(e).catch(e=>(this.#C(``,`A checkpoint stopped: ${e instanceof Error?e.message:String(e)}`),!1));this.#s=n,n.then(()=>{this.#s===n&&(this.#s=null,this.#c=null)})}async#h(e){this.#l=new Set(e.filter(({release:e})=>e).map(({save:e})=>e));try{return await this.#g(e)}finally{this.#l=/* @__PURE__ */ new Set}}async#g(e){let t=await this.#_(e);if(t===null)return!1;for(;;){if(!await this.#v(t))return!1;let e=await this.#b(t);if(e===W.Moved)break;if(e!==W.Stale)return!1;for(let e of t)e.receipt=void 0}for(let e of t)e.body&&(e.save.committed=e.digest),e.release?this.#n.delete(e.save.key):e.save.setStatus(ce.Saved);for(let e of t)e.pastLimit&&!e.release&&this.#e.onPastLimit(e.save);return!0}async#_(e){let t=[];for(let n of e){if(n.digest===n.save.committed){t.push({...n,body:null,pastLimit:!1});continue}let e=Hi(n.versions)??void 0,r=e?{problem:e}:await Ji(n.text,n.versions);if(`problem`in r)return this.#x({reason:`unreadable`,message:`${n.save.key}'s record cannot be stored: ${r.problem}`,unreadable:{key:n.save.key,owner:``,reason:r.problem}}),null;t.push({...n,body:r.body,pastLimit:r.size===qi.PastLimit})}return t}async#v(e){let t=e.filter(e=>e.body&&e.receipt===void 0);return(await Promise.all(Array.from({length:16},async()=>{for(;;){let e=t.shift();if(!e)return!0;if(!await this.#y(e))return!1}}))).every(Boolean)}async#y(e){let{store:t}=this.#e;for(let n=1;;n++){if(this.#d||this.#u)return!1;let r=await t.upload(e.save.key,e.body).catch(e=>({stored:!1,retry:!0,reason:`The store threw: ${e instanceof Error?e.message:String(e)}`}));if(r.stored)return e.receipt=r.receipt,!0;if(this.#C(e.save.key,r.reason),!r.retry)return this.#x({reason:`unreadable`,message:`The store refused ${e.save.key}'s record for good: ${r.reason}`,unreadable:{key:e.save.key,owner:``,reason:r.reason}}),!1;await this.#S(ga(n,r))}}async#b(e){let{store:t}=this.#e,n={id:Ur(16).toString(`base64url`),heads:e.map(({save:e,receipt:t,release:n})=>({key:e.key,...t!==void 0&&{receipt:t},...n&&{release:!0},...n&&e.holdOut&&{holdOut:!0}}))};for(let e=1;;e++){if(this.#d||this.#u)return W.Failed;let r=await t.commit(n).catch(e=>({outcome:W.Failed,reason:`The store threw: ${e instanceof Error?e.message:String(e)}`}));if(r.outcome===W.Moved)return W.Moved;if(this.#C(``,r.reason),r.outcome===W.Stale)return W.Stale;if(r.outcome===W.Fenced||r.outcome===W.Invalid)return this.#x({reason:r.outcome===W.Fenced?`fenced`:`invalid`,message:`The checkpoint's commit was refused as ${r.outcome}: ${r.reason} The room ends, and every player goes back to the last checkpoint.`}),r.outcome;await this.#S(ga(e,r))}}closeUnreadable;async close(){if(this.#u||this.#d||(await this.#s,this.#u||this.#d))return!1;let e=[];for(let t of this.#n.values()){let n=t.state===`leaving`?t.leave??null:t.readRecord();if(n!==null){if(`problem`in n)return this.#C(t.key,n.problem.message),this.closeUnreadable={key:t.key,owner:n.problem.key,reason:n.problem.message},!1;e.push({save:t,...n,digest:da(n.text),release:!0})}}if(e.length===0)return!0;let t=this.#h(e);return this.#s=t,this.#c=this.#t(),t}stop(){this.#d=!0;for(let e of[...this.#f])e()}#x(e){if(this.#u)return;this.#u=!0;for(let e of[...this.#f])e();let{log:t,room:n}=this.#e;t({kind:U.SaveFailed,room:n,player:e.unreadable?.key??``,reason:e.message}),this.#e.onEnd(e)}#S(e){return new Promise(t=>{if(this.#d||this.#u)return t();let n=setTimeout(()=>r(),e),r=()=>{clearTimeout(n),this.#f.delete(r),t()};this.#f.add(r)})}#C(e,t){let{log:n,room:r}=this.#e;n({kind:U.SaveFailed,room:r,player:e,reason:t})}};function ga(e,t){return Math.max(Math.min(250*2**(e-1),la),(t.retryAfterSeconds??0)*1e3)}function _a({name:e}){return typeof e==`string`}function va({credits:e,message:{name:t},now:n},{messagesPerSecond:r}){let i=e.get(t);return i||(i=xi({burst:2,perSecond:r,counted:n}),e.set(t,i)),i.perSecond=r,Si(i,n)}function ya(e){return Vt(e)&&Object.values(e).some(e=>typeof e==`string`&&Wt(e))}function ba(e){let{world:t,character:n,message:r}=e,{name:i,payload:a}=r,o=t.get(b)?.get(i);if(!o)return li.Unregistered;if(!va(e,o))return;if(!j(a,o.shape))return ya(a)?li.TooLarge:li.Malformed;let s=zn(t,o.shape,a);if(s.missing!==void 0)return li.MissingEntity;lt(t,{entity:n,name:i,payload:s.payload})}var xa=k*1e3;Math.round(cn/k);var Sa=.1,Ca=e=>Math.round(e/k);function wa({onBehind:e,onCaughtUp:t},n){let r=n,i=n,a=0,o=!1,s=0,c=0,l=0,u=0;function d(n){a>=k||!o&&n?(c++,l++,u+=a,l===5&&e({seconds:l,skippedSteps:Ca(u)})):(l>=5&&t({seconds:l,skippedSteps:Ca(u)}),l=0,u=0),r+=1e3,a=0,o=!1}return{note(e,t){let n=t>=k,c=e-i,l=c>=1e3?t/c:0,u=t;for(;e-r>=1e3;){let e=r+1e3-Math.max(r,i),t=l*Math.max(e,0);a+=t,u-=t,d(n)}i=e,a+=u,o=!0,s+=t},take(){let e={skippedSteps:Ca(s),behindSeconds:c};return s=0,c=0,e}}}function Ta(e){return e.filter(({meanMilliseconds:e})=>e/xa>Sa).sort((e,t)=>t.meanMilliseconds-e.meanMilliseconds).map(({name:e,meanMilliseconds:t,worstMilliseconds:n})=>({system:e,share:{mean:B(t/xa,3),max:B(n/xa,3)}}))}function Ea(){let e=process.cpuUsage(),t=performance.now();return()=>{let n=process.cpuUsage(e),r=performance.now(),i=r>t?(n.user+n.system)/1e3/(r-t):0;return e=process.cpuUsage(),t=r,B(i,3)}}var Da=`A thrown value with no readable text.`;function Oa(e){try{let{message:t,code:n,label:r}=Tn(e),i=e instanceof Error?e.stack:void 0;return{message:String(t),...i!==void 0&&{stack:String(i)},...n!==void 0&&{code:n},...r.call!==void 0&&{call:r.call},...r.entity!==void 0&&{entity:r.entity.key}}}catch{return{message:Da}}}function ka(e,t){let n={},r=/* @__PURE__ */ new Map;return{note({system:i,tick:a,streak:o,error:s}){n[i]=(n[i]??0)+1;let c=Oa(s),{message:l}=c,u=r.get(i)??/* @__PURE__ */ new Set;u.has(l)||u.size>=4||(u.add(l),r.set(i,u),e({kind:U.SystemFaulted,room:t,system:i,tick:a,streak:o,...c}))},take(){let e=n;return n={},r.clear(),e}}}function Aa(e){let t=-1;return e.traverse(()=>t++),t}function ja(e,t){let n=[],r=e;for(;r&&r!==t;)n.unshift(r.name||r.type),r=r.parent;return n.join(` > `)}function Ma(e){let t=/* @__PURE__ */ new Map;return e.traverse(e=>t.set(e,e.children.length)),t}function Na(e){let t,n=0,r=Ma(e);return{note(i){let a=Aa(e),o=t!==void 0&&a>t.objects&&i<=t.entities;if(t={objects:a,entities:i},!o)return n=0,r=Ma(e),{sceneObjects:a};if(n++,n!==4)return{sceneObjects:a};let s,c=0;for(let[t,n]of Ma(e)){let i=n-(r.get(t)??0);i<=c||(c=i,s={path:ja(t,e),children:n})}return{sceneObjects:a,growing:{heartbeats:n,...s&&{parent:s}}}}}}function Pa(e){return A(e)&&e.length===3&&e.every(Number.isFinite)}var Fa={least:.5,most:2},Ia=64,La=16;function Ra(e){return Array.isArray(e)&&e.length<=Ia&&e.every(e=>Array.isArray(e)&&e.length<=La&&e.every(e=>typeof e==`string`&&N(e)))}function za({weapon:e,step:t,origin:n,direction:r,id:i,hits:a}){return typeof e==`string`&&Number.isFinite(t)&&Pa(n)&&Pa(r)&&dn(...r)>=Fa.least&&dn(...r)<=Fa.most&&(i===void 0||Number.isSafeInteger(i)&&Number(i)>=0)&&(a===void 0||Ra(a))}var Ba=new Lr,Va=new Lr,q={ray:new Ir,range:0,feet:new Lr,body:ct},Ha=[],Ua={ray:q.ray,range:0,target:null,through:Ha},Wa=new Lr,Ga=new Lr,Ka=ai(h,we),J={shooter:null};function Y(e){for(let[t,n]of Yn(e.world,e.weapon)){let r=n(e);if(r!==void 0)return{rule:t,reason:r}}return null}function qa({world:e,character:t,shot:n,now:r}){let i=e.get(me)?.step??0,a=e.get(Oe)?.get(n.weapon),o=a&&{world:e,shooter:t,claim:n,weapon:a,landed:i,now:r,step:n.step},s=o?Y(o):{rule:`weapon`,reason:`The world holds no weapon named "${n.weapon}".`};return o&&!s&&Ja(o),Vn(t,{weapon:n.weapon,id:n.id??null,named:n.step,judged:o?.step??n.step,landed:i,at:r,refusal:s}),un(e,t,n.step),s}function Ja(e){let{world:t,shooter:n,claim:r,weapon:i}=e;if(!At(n,Ba))return;q.ray.origin.fromArray(r.origin),q.ray.direction.fromArray(r.direction).normalize();let a=z(i,n,De.Damage),o=z(i,n,De.Range);if(i.kind===C.Projectile){en(Gn(t),{origin:q.ray.origin,direction:q.ray.direction,weapon:{...i,damage:a,range:o-q.ray.origin.distanceTo(Ba),speed:z(i,n,De.Speed),zoneDamage:z(i,n,De.ZoneDamage)},weaponName:r.weapon,shooter:n});return}let s=Jn(t,i)===be.Page;oo(e),io.copy(q.ray.direction);let c=z(i,n,De.Pellets),l=z(i,n,De.Pierce),u=z(i,n,De.Spread),d=z(i,n,De.ZoneDamage);for(let e=0;e<c;e++){D({direction:io,pellet:e,pellets:c,spread:u},q.ray.direction);let f={shooter:n,weapon:r.weapon,shot:r.id??null,data:i.data??null,damage:a,zoneDamage:d,range:o,pierce:l};s?mo(t,f,r.hits?.[e]):uo(t,f)}}var Ya=[],Xa=[],Za=[],Qa=[],$a=[],eo=[],X=0,to=[],no=[],ro=[],io=new Lr;function ao(e,t,n,r){if(n)return Nn(n,t,r)??null;let i=e.get(we)?.rotation;return i?r.copy(i):null}function oo({world:e,shooter:t,weapon:n,step:r}){J.shooter=t,J.friendlyFire=n.friendlyFire,X=0;let i=O(e,r);for(let t of e.query(Ka)){let n=t.get(we)?.position;if(!n||Ut(t,J))continue;let r=yt(e,t),a=Za[X]??=new Lr;if(!i)a.copy(n);else if(!Pn(i,r,a))continue;let o=i?Fn(i,r):t.get(re);Qa[X]=o,$a[X]=o?.length?ao(t,r,i,eo[X]??=new Fr):null,Xa[X]=r,Ya[X++]=t}}function so(e){let t=Ya[e];q.feet.copy(Za[e]),q.body=t.get(d)??ct,q.zones=Qa[e],q.facing=$a[e]}function co(){let e=0;for(let t=0;t<X;t++){let n=Ya[t];if((n.get(h)?.current??0)<=0)continue;so(t);let r=fn(q);if(r===1/0)continue;let i=q.zone??null,a=e;for(;a>0&&no[a-1]>r;)to[a]=to[a-1],no[a]=no[a-1],ro[a]=ro[a-1],a--;to[a]=n,no[a]=r,ro[a]=i,e++}return e}function lo(e){let t=Wa.subVectors(Ba,q.ray.origin).dot(q.ray.direction),n=e*e-Wa.lengthSq()+t*t;return n<0?0:Math.max(0,t+Math.sqrt(n))}function uo(e,t){let{range:n,pierce:r}=t,i=lo(n);q.range=i;let a=co();Ua.range=i,Ua.target=a>0?to[0]:null,Ha.length=0;for(let e=1;e<Math.min(a,r+1);e++)Ha.push(to[e]);let o=Ue(e,Ua)?.distance??1/0,s=[],c=Math.min(o,i);for(let n=0;n<a&&s.length<=r;n++){let a=no[n];if(a>o||a>i)break;let l=to[n];s.push(fo(e,l,t,ro[n])),s.length>r&&(c=a)}ho(e,{...t,reached:c,hits:s})}function fo(e,t,{shooter:n,weapon:r,data:i,damage:a,zoneDamage:o},s){let c=t.get(h)?.current??0;ot(Gn(e),t,{amount:cr(a,s,o),source:n,weapon:r,zone:s?.name??null,data:i});let l={target:yt(e,t),damage:c-(t.get(h)?.current??0)};return s&&(l.zone=s.name),l}function po(e,t){return so(e),q.range=t,fn(q)===1/0?null:q.zone??null}function mo(e,t,n){let r=lo(t.range),i=[],a=-1;for(let o of n??[]){if(i.length>t.pierce)break;let n=Xa.indexOf(o);if(n===-1||n>=X)continue;let s=Ya[n];if((s.get(h)?.current??0)<=0)continue;let c=po(n,r);i.push(fo(e,s,t,c));let l=Ga.subVectors(Za[n],q.ray.origin).dot(q.ray.direction);a=Math.min(Math.max(l,0),r)}a<0&&(Ua.range=r,Ua.target=null,Ha.length=0,a=Math.min(Ue(e,Ua)?.distance??1/0,r)),ho(e,{...t,reached:a,hits:i})}function ho(e,{shooter:t,weapon:n,shot:r,reached:i,hits:a}){let{origin:o}=q.ray;q.ray.at(i,Va);let s={shooter:yt(e,t),weapon:n,origin:[B(o.x,2),B(o.y,2),B(o.z,2)],end:[B(Va.x,2),B(Va.y,2),B(Va.z,2)],hits:a,...r!==null&&{shot:r}};xt(Gn(e),xe,s)}var Z=/* @__PURE__ */ function(e){return e[e.NotAMessage=4e3]=`NotAMessage`,e[e.ProtocolMismatch=ze]=`ProtocolMismatch`,e[e.NotJoined=4001]=`NotJoined`,e[e.JoinedTwice=4002]=`JoinedTwice`,e[e.Kicked=Qt]=`Kicked`,e[e.JoinRefused=Xt]=`JoinRefused`,e[e.Full=nr]=`Full`,e[e.Replaced=4005]=`Replaced`,e[e.SeatRefused=lr]=`SeatRefused`,e[e.PlayingElsewhere=yn]=`PlayingElsewhere`,e[e.SaveNotLoaded=sr]=`SaveNotLoaded`,e[e.SaveNewer=or]=`SaveNewer`,e[e.Failed=4500]=`Failed`,e[e.RoomFailed=tr]=`RoomFailed`,e[e.Left=P]=`Left`,e[e.SeatNotFreed=1011]=`SeatNotFreed`,e[e.GoingAway=1001]=`GoingAway`,e}({}),go=1006,_o=1e3;function vo(e,t){let n,r=new Promise(e=>{n=setTimeout(e,t)}),i=Promise.all(e.map(e=>e.readyState===V.CLOSED?void 0:new Promise(t=>e.once(`close`,t))));return Promise.race([i,r]).finally(()=>clearTimeout(n))}var yo=`You joined this game somewhere else, so this window left it.`,bo=4,xo=15,So=8,Co=3,wo=1e3,To=30,Eo=1e6,Do=256,Oo=30,ko=5,Ao=15,jo=5,Mo=10,No=2,Po=1e3,Fo=8,Io=4,Lo=2,Ro=Math.round(2/k),zo=15,Bo=60,Vo=64,Ho=4,Uo=vn*1e3/2;function Wo(e){return Number.isSafeInteger(e)&&Number(e)>=0}var Go=Object.values(m);function Ko(e){if(typeof e!=`object`||!e)return!1;switch(e.action){case _.Move:return Wo(e.from)&&Wo(e.to)&&(e.count===void 0||zt(e.count));case _.Use:return Wo(e.slot)&&(e.target===void 0||typeof e.target==`string`&&N(e.target));case _.Equip:return Wo(e.slot);case _.Unequip:return Go.includes(e.slot);case _.Drop:return Wo(e.slot)&&zt(e.count);default:return!1}}function qo(e){switch(e.signal){case p.Open:return!0;case p.Connect:return Object.values(Te).includes(e.direction)&&Vt(e.dtlsParameters);case p.Listen:return Vt(e.rtpCapabilities);case p.Produce:return Vt(e.rtpParameters);default:return!1}}function Jo(e){return typeof e==`string`&&/^[\w.-]{1,64}$/.test(e)}function Yo(e){let t=e.baseline===!0?e.predicted:void 0;return{predicted:[...Me(t??[...pn])],listsPredicted:Array.isArray(t)}}function Xo(e,t){if(e.type===f.Join){let t=e.wire,n=e.avatar,r=e.rejoin,i=e.build,a=e.baseline,o=e.predicted;return typeof e.name==`string`&&N(e.name)&&(t==null||typeof t==`string`)&&(n==null||typeof n==`string`&&N(n))&&(r==null||typeof r==`string`&&N(r))&&(i==null||Jo(i))&&(a==null||typeof a==`boolean`)&&(o==null||Array.isArray(o)&&o.every(e=>typeof e==`string`&&N(e)))}if(e.type===f.Edit){let{entity:t,trait:n,prop:r,value:i}=e;return typeof t==`string`&&typeof n==`string`&&typeof r==`string`&&(typeof i==`boolean`||Number.isFinite(i))}if(e.type===f.Ping)return Number.isFinite(e.sent);if(e.type===f.Shot)return za(e);if(e.type===f.Voice)return qo(e);if(e.type===f.Item)return Ko(e.intent);if(e.type===f.Message)return _a(e);if(e.type===f.Press)return wi(e);if(e.type===f.Leave)return!0;if(e.type===f.Walk){let{tick:t,destination:n}=e;return Q(t)&&(n===null||Array.isArray(n)&&n.length===3&&n.every(Number.isFinite))}let n=e.next,r=e.level;return(r===void 0||Q(r))&&Q(e.tick)&&Zo(e,t)&&Array.isArray(n)&&n.length<ln&&n.every(e=>Zo(e,t))}function Q(e){return Number.isSafeInteger(e)&&e>=0}function Zo(e,t){if(!Vt(e))return!1;let{buttons:n,axes:r,vectors:i,angles:a,changes:o}=e;return(o===void 0||qt(o,t.buttons))&&(i===void 0||Array.isArray(i)&&i.length===t.vectors*2&&i.every((e,t)=>t%2==1||Kt(i[t],i[t+1])))&&(a===void 0||Array.isArray(a)&&a.length===t.angles&&a.every(Lt))&&(n===void 0||Number.isSafeInteger(n)&&n>=0&&n<2**t.buttons)&&(r===void 0||Array.isArray(r)&&r.length===t.axes&&r.every((e,n)=>Qo(e,t.axisSides[n])))}function Qo(e,t){return t!==void 0&&Number.isSafeInteger(e)&&e>=-t.negative&&e<=t.positive}function $o(){return{tick:-1,input:{buttons:0,axes:[],vectors:[],angles:[],changes:[]}}}var es=$o().input;function ts(e,t){e.buttons=t.buttons??0,$e(e.axes??=[],t.axes),$e(e.vectors??=[],t.vectors),$e(e.angles??=[],t.angles),$e(e.changes??=[],t.changes)}function ns(e,t){e.buttons=((e.buttons??0)&~t)>>>0,e.changes&&(e.changes.length=0)}var $=Buffer.from(`{"type":"${f.Input}"`);function rs(e,t){return t?Buffer.isBuffer(e)&&Le(e):Buffer.isBuffer(e)&&e.length>=$.length&&e.compare($,0,$.length,0,$.length)===0}function is(e){return Array.isArray(e)?Buffer.concat(e):e instanceof ArrayBuffer?new Uint8Array(e):e}function as(e){return typeof e==`string`?Buffer.byteLength(e):e.length}function os(e){return typeof e!=`string`}function ss(e,t,n){try{let r=st(t?is(e):String(e));return Xo(r,n)?r:void 0}catch{return}}async function cs({scene:e,plugins:t,physics:n,scatter:r,allowEdits:i}){if(typeof e!=`function`)return i&&e.world.add(te),{world:e.world,unmount:e.unmount,reload:e.reload,objects:e.scene};let{world:a}=await nt({scene:e,plugins:t,physics:n,dev:i}),o=gt(a,r);return{world:a,unmount:async()=>{o()},reload:void 0,objects:void 0}}function ls(e,t){let n=e.headers[`x-forwarded-for`];return((t?String(n??``).split(`,`).at(-1)?.trim():void 0)||e.socket.remoteAddress||``).replace(/^::ffff:(?=\d+\.\d+\.\d+\.\d+$)/i,``)}async function us({port:e,host:t=`127.0.0.1`,roomId:n,scene:r,plugins:i,physics:a,maxPlayers:o=Gr,sendRate:s=Kr,scatter:c=0,movement:l,allowEdits:d=!1,invulnerable:te=!1,log:m=di,onFailure:h,deployed:re=!1,voice:_,record:ie,traitModules:ae=()=>[],heartbeatSeconds:ce=It,hashSends:fe=!1,serve:x=!0,rejoinSeconds:pe,maxQueuedBytes:me=Eo,emptySeconds:_e,onEmpty:ye,director:be=!1,timeline:xe,seed:we,build:Te,seats:C,trustProxy:De=!1,save:Oe,saves:Me,checkpointSeconds:Ne,dueCheckpointSeconds:Le}){let ze=Mn(s);if(!(Number.isFinite(me)&&me>0))throw RangeError(`maxQueuedBytes must be a finite number of bytes over 0, not ${me}: it is the most the room queues for a client before it ends her connection.`);let{world:w,unmount:Ue,reload:We,objects:Je}=await cs({scene:r,plugins:i,physics:a,scatter:c,allowEdits:d}),Ye;try{Ye=Ri(w,Oe)}catch(e){throw await Ue(),w.destroy(),e}w.has(Ce)||w.add(Ce),pr(w,{movement:l,invulnerable:te}),Mt(w,e=>_i(e)),Nt(w,e=>{for(let t of H()){if(t.record!==e)continue;t.id=yt(w,e.entity);let n=Jn(t);n&&(Sr(e.entity,n),Qe(Gn(w),e.entity,n,{simulation:Se.Predicted})),t.welcomed=!1,t.owned={},t.controlled=null,t.rewound={}}});let Xe=w.get(le)?.find(({name:e})=>e===`characters`),Ze=Xe!==void 0,$e=Ze&&!Xe.omitted.includes(`characters.movement`),tt=ka(m,n),nt=Je&&Na(Je);re&&Jt(w,tt.note);let rt=Object.keys(Ye).length>0?Me:void 0,T=rt?new ha({store:rt,log:m,room:n,intervalSeconds:Ne,dueSeconds:Le,onEnd:e=>ei(e),onPastLimit:e=>$r(e),isTakenOver:e=>F.some(({save:t})=>t?.key===e)}):void 0;T&&br(w);let ot=In(w),st=En(w),ct=we??Math.floor(Math.random()*2**32);dr(w,ct),ur(w,we===void 0?Math.floor(Math.random()*2**32):dt(we));let lt=ui();await Promise.all([an(),Rt(w)&&rn()]);let ut=[];for(let e of w.query(ne)){let t=e.get(ne)?.surface;t&&!e.has(oe)&&ut.push(Ie(e,{ground:Re(t)}))}let gt=()=>{for(let e of ut)e()},xt=lt(),E=ie&&Mi({...ie,port:e,invulnerable:te,maxPlayers:o,sendRate:s,allowEdits:d,rejoinSeconds:pe,warn:e=>console.warn(e)}),D=/* @__PURE__ */ new Map,O=!1,At=/* @__PURE__ */ new Map,Lt=0,zt=/* @__PURE__ */ new WeakMap,A,j=!1,Bt=1,M,Vt=[],Ht=/* @__PURE__ */ new Map,Ut=performance.now(),Wt=Ut,Kt=()=>j||M!==void 0?Ut:Ut+(performance.now()-Wt)*Bt,qt=()=>{Ut=Kt(),Wt=performance.now()},N=()=>A??Kt(),Xt={starved:0,late:0},Qt=[],en={buttons:0},P=/* @__PURE__ */ new Map,nn=[],on=[],ln=[],F=[],un=/* @__PURE__ */ new Set,dn=!1,fn=/* @__PURE__ */ new Map,pn=/* @__PURE__ */ new Map,mn=/* @__PURE__ */ new Set,_n=pe===void 0||pe>0,vn=e=>e===`left`||e===`moved`?0:pe??(e===`broken`?30:15),bn=Ur(16).toString(`hex`),xn=0,Tn=()=>Hr(`sha256`,bn).update(String(xn++)).digest(`base64url`),I=performance.now(),L=/* @__PURE__ */ new Set,kn=/* @__PURE__ */ new Map,An=/* @__PURE__ */ new Map,jn=/* @__PURE__ */ new WeakSet,Nn=/* @__PURE__ */ new Map,Pn=/* @__PURE__ */ new Map,Fn=/* @__PURE__ */ new Map,z=0,zn=0;function Vn(e){return e.wire===ke.Binary?ke.Binary:ke.Json}function Un(e){let{record:t,character:n}=e;return{...e,id:yt(w,t.entity),name:t.name,...n&&{characterId:yt(w,n)},rejoinKey:Tn(),welcomed:!1,inputs:Array.from({length:Vo},$o),repeat:$o().input,repeatTick:-1,ownTick:-1,lateEdges:0,heldAcross:0,level:R(w),starvedRun:16,early:null,earlyTick:0,late:0,starved:0,walks:[],walkCredit:at(),pingAnswered:-1/0,refusedShotOrigins:0,refusalsLogged:0,owned:{},controlled:null,rewound:{},itemCredit:xi({burst:8,perSecond:4,counted:N()}),inputCredit:ji(),inputFloodLogged:!1,messageCredits:/* @__PURE__ */ new Map,dropsLogged:0,...Ci(e.presses)}}function qn(e,t){D.set(e,t),I=null}function Jn(e){let{character:t}=e;return t?.isAlive()?t:void 0}function Yn(e){return Jn(e)??e.record.entity}function $n({plugin:e,hook:t,error:n}){tt.note({system:`${e}.${t}`,tick:J,streak:1,error:n})}function tr(e){return F.some(t=>t.socket===e)||ln.some(t=>t.socket===e)}function nr(e,t,n,r={}){E?.active&&E.record({step:J,kind:S.Message,connection:At.get(e)??-1,now:n,message:t,...r})}function rr(e,t){A=e;try{t()}finally{A=void 0}}function or({socket:e,message:t,key:r,now:i=N()}){if(e.readyState!==V.OPEN)return;let a=P.get(r),o=a?.player;for(let[e,t]of D)o||t.rejoinKey!==r||(o=t,D.delete(e),_t(w,Yn(t)),e instanceof V&&Jo?.leave(e),jn.add(e),e.close(4005));if(!o){Ti(e,{...t,rejoin:void 0});return}a&&P.delete(r),E&&!E.active&&E.resume(Oo()),rr(i,()=>{nr(e,t,i);let r=Un({record:o.record,character:Jn(o)??null,wire:Vn(t),baseline:t.baseline===!0,...Yo(t),hostedSeat:o.hostedSeat,save:o.save,presses:t.presses===!0});qn(e,r),a&&Bn(w,r.record,$n),m({kind:U.Rejoined,room:n,player:r.id,name:r.name,players:D.size}),G(t.build)})}function sr(e,t){let n=Nn.get(e);F.push({socket:e,message:t,account:n?.account??t.name,hostedSeat:n?.seat,save:n?.save,...A!==void 0&&{now:A}})}function cr(e){let{socket:t,message:r,account:i,hostedSeat:a,save:o}=e,s=e.now??N();E&&!E.active&&E.resume(Oo()),A=s;try{let c=hr(w,{account:i,name:r.name,seat:Ot(w),role:he.Player,avatar:r.avatar??``});o?.bindPlayer(c);let l,u=Yt(w,c,{returning:un.has(i),restore:()=>{if(!o)return!0;let e=kt(c.entity);return e&&o.attach(e),l=Cr(o),l===void 0},check:()=>!o||(l=kr(o),l===void 0),rethrow:!re});if(!u.ok){u.refusal===v.HookFaulted?lr(e,u.fault):o&&l&&(nr(t,r,s,{save:{record:o.loaded,player:o.player},...i!==r.name&&{account:i}}),Ar(e,o,l));return}let d=Un({record:c,character:kt(c.entity),wire:Vn(r),baseline:r.baseline===!0,...Yo(r),hostedSeat:a,save:o,presses:r.presses===!0});qn(t,d),o&&T?.add(o),nr(t,r,s,{...o&&{save:{record:o.loaded,player:o.player}},...d.characterId!==void 0&&{character:d.characterId},...i!==r.name&&{account:i}}),m({kind:U.Joined,room:n,player:d.id,name:d.name,players:D.size}),G(r.build)}finally{A=void 0}}function lr({socket:e,message:t,hostedSeat:r},{plugin:i,hook:a,error:o}){m({kind:U.JoinRefused,room:n,name:t.name,refusal:v.HookFaulted,plugin:i,hook:a,message:o instanceof Error?o.message:String(o)}),r!==void 0&&!Bi(r)&&ti(r);let s={refusal:v.HookFaulted,plugin:i};e.readyState===V.OPEN&&e.close(Z.JoinRefused,Wi(JSON.stringify(s)))}function Cr(e){let t=e.session.loaded;if(t&&!t.ok)return{owner:t.key,reason:t.message}}function kr(e){let t=e.readRecord();if(t===null)return;if(`problem`in t)return{owner:t.problem.key,reason:t.problem.message};e.committed??=da(t.text);let n=Yi(t.text);return n===void 0?void 0:{owner:``,reason:n,pastLimit:!0}}function Ar({socket:e,hostedSeat:t},n,r){qe(w),ri(n.key,r.reason),e.close(Z.SaveNotLoaded,Gi({key:r.owner,retry:!1})),t!==void 0&&!Bi(t)&&!T?.listLeaving().includes(t)&&ti(t,void 0,r)}function jr({endHolds:e=!0}={}){for(let e of nn.splice(0))li(e);on.length>0&&Fo(on.splice(0));for(let e of ln.splice(0))or(e);P.size>0&&(e||F.length>0)&&gi();for(let e of F.splice(0))cr(e)}function Mr(e,t){if((t.level??0)!==R(w))return;let n=J,r=t.tick,i=r-n;(e.early===null||i<e.early)&&(e.early=i,e.earlyTick=r);for(let i=0;i<=t.next.length;i++){let a=r+i;if(ts(es,i===0?t:t.next[i-1]),a<n){if(e.late++,Xt.late++,a>e.repeatTick){let t=((es.buttons??0)&st.edgeButtons)>>>0;t!==0&&a+$t>=n&&(e.lateEdges=(e.lateEdges|t)>>>0),et(e.repeat,es),ns(e.repeat,st.edgeButtons),e.repeatTick=a}continue}if(a>=n+60)continue;let o=e.inputs[a%Vo];o.tick!==a&&(o.tick=a,et(o.input,es))}}function Nr(e){let t=e.record.entity;if(!t.isAlive())return;let n=J,r=e.inputs[n%Vo];if(r.tick===n?(Or(t,r.input),e.ownTick=n,e.starvedRun=0,et(e.repeat,r.input),ns(e.repeat,st.edgeButtons),e.repeatTick=n):(e.starved++,Xt.starved++,e.starvedRun<15?Or(t,e.repeat):e.starvedRun===15?Qi(e):Zi(t),e.starvedRun++),e.lateEdges!==0&&(en.buttons=((t.get(g)?.buttons??0)|e.lateEdges)>>>0,e.lateEdges=0,t.set(g,en)),e.heldAcross!==0){let i=t.get(g)?.buttons??0;r.tick===n&&(e.heldAcross=(e.heldAcross&i)>>>0),en.buttons=(i&~e.heldAcross)>>>0,t.set(g,en)}ar(w,t)}function Pr(e){for(let t of e.inputs)t.tick=-1;e.heldAcross=e.record.entity.get(g)?.buttons??0,e.repeat.buttons=0,e.repeat.axes?.fill(0),e.repeat.vectors?.fill(0),e.repeat.changes=[],e.lateEdges=0}function Fr(e){ki(w,e,t=>Ir(e,t),{tick:J,level:R(w)},Qt)}function Ir(e,t){let n=e.record.entity;if(t.entity!==void 0){let e=Et(w,t.entity);return e?.isAlive()&&(e===n||Tt(w,e)===n)?e:de.NotYours}let r=wn(w,n);return r.length>1&&w.get(b)?.get(t.name)?.many?de.Ambiguous:r.find(e=>e.has(ue))??r[0]??Jn(e)??n}function Rr(e,{tick:t,destination:n}){e.walks.length===2*wr&&e.walks.shift(),e.walks.push({tick:t,destination:n&&new Lr().fromArray(n)})}function zr(e){let{walks:t}=e,n=Jn(e);if(!$e){t.length>0&&Br(e,Ze?Ee.NoMovement:Ee.NoCharacter),t.length=0;return}for(;t.length>0&&t[0].tick<=J;){let r=t.shift();if(!r||!n)break;let{destination:i}=r;if(!i||!gr(e.walkCredit,r.tick)){n.get(se)?.corners.splice(0);continue}let{refusal:a}=Tr(w,i,n);a&&(n.get(se)?.corners.splice(0),Br(e,a))}}function Br(e,t){e.dropsLogged>=4||(e.dropsLogged++,m({kind:U.WalkRefused,room:n,player:e.id,reason:t}))}function Vr(e,t,n){un.add(e.record.account);let r={reason:t,heldMilliseconds:n};return tn(w,e.record,t,$n,qr(e,r))?Xr(e,r):Promise.resolve(!0)}function qr(e,t){return e.save&&T?()=>Zr(e,t):void 0}function Xr(e,{reason:t,heldMilliseconds:r}){m({kind:t===y.Kicked?U.Kicked:U.Left,room:n,player:e.id,name:e.name,players:D.size}),T&&H().length===0&&T.request();let i=e.hostedSeat;if(i!==void 0)return e.save&&T?Promise.resolve(!0):ti(i,r)}function Zr(e,t){let n=e.save,r=n.readLeave();return r===null?(T?.forget(n),!0):`problem`in r?(ri(n.key,r.problem.message),pn.has(e)?(pn.delete(e),ei({reason:ma.Unreadable,message:`${n.key}'s record could not be read as she left, twice in a row: the room ends, and every player goes back to the last checkpoint. ${r.problem.message}`,unreadable:{key:n.key,owner:r.problem.key,reason:r.problem.message}}),!0):(pn.set(e,t),!1)):(pn.delete(e),T?.leave(n),!0)}function Qr(e){let t=e.readLeave();return t===null?(T?.forget(e),!0):`problem`in t?(ri(e.key,t.problem.message),!1):(T?.leave(e),!0)}function $r(e){e.holdOut=!0;for(let[t,n]of D)n.save===e&&(D.delete(t),jn.add(t),_t(w,Yn(n)),Vr(n,y.Kicked),t.close(Z.SaveNotLoaded,Gi({key:``,retry:!1})));for(let[t,{player:n}]of P)n.save===e&&(P.delete(t),Vr(n,y.Kicked));D.size===0&&I===null&&(I=performance.now()),qe(w)}function ei(e){if($)return;let t=Error(e.message);Ko(U.Failed,t);let n=()=>h?.(t);$=is(!1,!0,e.unreadable&&{seat:e.unreadable.key,owner:e.unreadable.owner,reason:e.unreadable.reason}),$.then(n,n)}function ti(e,t,n){if(!C)return;let r=C,i=(n?ni(r,e,n):r.release(e,t)).then(({freed:e})=>e).catch(e=>(console.error(e),!1));return fn.set(e,i),i.finally(()=>{fn.get(e)===i&&fn.delete(e)}),i}async function ni(e,t,n){for(let r=1;;r++){let i=await e.release(t,void 0,n);if(i.freed||O||r>=3)return i;let a=Math.max(wo*r,(i.retryAfterSeconds??0)*1e3);await new Promise(e=>setTimeout(e,a))}}function ri(e,t){m({kind:U.SaveFailed,room:n,player:e,reason:t})}async function ii(e,t,n,r){let i=(e,n,r)=>(ri(t,r),{code:e,reason:Gi(n),...!n.retry&&{refused:{owner:n.key,reason:r}}});if(!r)return i(Z.SaveNotLoaded,{key:``,retry:!0},`The platform gave her seat no save pass: the room cannot load or store her save now.`);for(let e of F.filter(({save:e})=>e?.key===t))F.splice(F.indexOf(e),1),jn.add(e.socket),e.socket.close(4005);for(let e of H()){if(e.save?.key!==t)continue;let r=Vi(e);if(e.save&&T?.leave(e.save),r===void 0)return i(Z.SaveNotLoaded,{key:``,retry:!0},`Her record could not be read as her reload took over her seat.`);let a=ai(t,n,JSON.parse(r),r);return a.ok?{save:a.save}:i(Z.SaveNotLoaded,{key:a.key,retry:!1},a.message)}await T?.settleLeave(t);let a=T?.saves.get(t);if(a?.state===ua.Leaving&&a.leave){let{text:e}=a.leave,r=ai(t,n,JSON.parse(e),e);return r.ok?{save:r.save}:i(Z.SaveNotLoaded,{key:r.key,retry:!1},r.message)}let o=await e.load(t);if(o.status===Ni.Failed)return i(Z.SaveNotLoaded,{key:``,retry:o.retry},o.reason);let s=null,c=null;if(o.status===Ni.Loaded){c=o.json;let e=zi(Ye,o.versions);if(e!==void 0)return i(Z.SaveNewer,{key:e,retry:!1},`A newer version of the game wrote ${e} at version ${o.versions[e]}, and this room reads it up to version ${Ye[e]}: nothing was loaded or stored, and her seat is freed for a room on the newer version.`);try{s=JSON.parse(o.json)}catch{return i(Z.SaveNotLoaded,{key:``,retry:!1},`The save did not load: it is no JSON. Nothing was restored, and the save must not be written over.`)}}let l=ai(t,n,s,c);return l.ok?{save:l.save}:i(Z.SaveNotLoaded,{key:l.key,retry:!1},l.message)}function ai(e,t,n,r){let i=it(w,{save:Oe,player:()=>({id:t})}),a=i.load(structuredClone(n));return a.ok?{ok:!0,save:new fa({key:e,player:t,loaded:n,loadedText:r,session:i})}:a}function H(){return[...D.values(),...Array.from(P.values(),e=>e.player)]}function oi(){if(T&&!O){br(w)&&T.request(5);for(let[e,t]of pn)jt(w,e.record,qr(e,t))&&Xr(e,t);T.tick()}}function si(e,t){let r=D.get(e);if(!r)return;let i;D.delete(e),_t(w,Yn(r));let a=vn(t),o=Math.round(a/k);return o>0?(Qi(r),P.set(r.rejoinKey,{player:r,since:J,until:J+o}),ht(w,r.record,J+o,$n),m({kind:U.Dropped,room:n,player:r.id,name:r.name,players:D.size,holdSeconds:a})):i=Vr(r,t===`left`||t===`moved`?y.Left:y.HoldEnded),D.size>0?i:(I=performance.now(),E?.pause(Oo()),i)}function li({socket:e,departure:t,now:n=N()}){let r=F.findIndex(t=>t.socket===e);if(r>=0){let[e]=F.splice(r,1);e.hostedSeat!==void 0&&!Bi(e.hostedSeat)&&!T?.listLeaving().includes(e.hostedSeat)&&ti(e.hostedSeat)}let i=ln.findIndex(t=>t.socket===e);i>=0&&ln.splice(i,1);let a=t===`left`,o=At.get(e);if(a||fi(e),!D.has(e)){a&&e.readyState===V.OPEN&&e.close(Z.Left);return}rr(n,()=>{a?nr(e,{type:f.Leave},n):E?.active&&E.record({step:J,kind:S.Close,connection:o??-1,now:n,...t===`broken`&&{broken:!0}});let r=si(e,t);a&&Promise.resolve(r).then(t=>e.close(t===!1?1011:Z.Left))})}function fi(e){e instanceof V&&At.delete(e)}function pi(e,t){nn.push({socket:e,departure:t,...A!==void 0&&{now:A}})}function gi(){for(let[e,{player:t,since:n,until:r}]of P)J<r||(P.delete(e),Vr(t,y.HoldEnded,Math.round((J-n)*k*1e3)))}function _i(e){for(let{player:t,message:n}of e){for(let[e,r]of D){if(r.record.entity!==t)continue;D.delete(e),jn.add(e),_t(w,Yn(r));let i=Vr(r,y.Kicked),a=Wi(n);Promise.resolve(i).then(()=>{e.readyState===V.OPEN&&e.close(Z.Kicked,a)}),D.size===0&&(dn=!0)}for(let[e,{player:n}]of P)n.record.entity===t&&(P.delete(e),Vr(n,y.Kicked));for(let e of yi())e.entity===t&&tn(w,e,y.Kicked,$n)}}function yi(){return Dn(w).filter(e=>e.role===he.Bot)}function wi(e,t,n){if(e.readyState!==V.OPEN||jn.has(e))return;let r=D.get(e),i=r!==void 0&&rs(t,n);if(r&&i&&!W(r))return;let a=ss(t,n,st);if(!a)return e.close(4e3);if(a.type===f.Join&&a.protocolVersion!==Sn){let t={room:ft(Sn),page:ft(a.protocolVersion)};return e.close(Z.ProtocolMismatch,JSON.stringify(t))}if((!r||i||a.type!==f.Input||W(r))&&!L.has(e)){if(a.type===f.Join&&!D.has(e)&&!tr(e)&&!Pi(a.rejoin)){Ti(e,a);return}Ui(e,a)}}function Ti(e,t){if(!(C||rt)||C===void 0&&D.size+P.size+L.size+F.length>=o){Ui(e,t);return}Fi(e,t).catch(t=>{L.delete(e),Nn.delete(e),m({kind:U.JoinFailed,room:n,message:t instanceof Error?t.message:String(t)}),e.readyState===V.OPEN&&e.close(4500)})}function Ei(){return J*k*1e3}function ji(){return xi({burst:Ro,perSecond:2/k,counted:Ei()})}function W(e){return Si(e.inputCredit,Ei())?!0:(e.inputFloodLogged||(e.inputFloodLogged=!0,m({kind:U.InputFlooded,room:n,player:e.id})),!1)}function Pi(e){if(!e)return;let t=P.get(e);if(t)return t.player;for(let t of D.values())if(t.rejoinKey===e)return t}async function Fi(e,t){L.add(e);let n=C?await C.admit(t.seat,zt.get(e)):void 0;if(n&&!n.seated){L.delete(e),e.readyState===V.OPEN&&e.close(Z.SeatRefused,n.reason);return}let r=n?.seat,i=n?.name??t.name;if(!n&&rt&&Hi(e,i)){L.delete(e),e.readyState===V.OPEN&&e.close(Z.SeatRefused,Wi(`${i} is already playing in this room: join under another name, or close the other page first.`));return}let a;kn.set(e,i);let o=Li(r??i);try{await o.ready,a=await Ii(e,t,n)}finally{o.done(),kn.delete(e),L.delete(e)}!O&&!D.has(e)&&!tr(e)&&r!==void 0&&!Bi(r)&&!T?.listLeaving().includes(r)&&ti(r,void 0,a)}async function Ii(e,t,n){let r=n?.seated?n:void 0,i=r?.name??t.name,a=rt&&!O&&e.readyState===V.OPEN?await ii(rt,r?.seat??i,r?.player??i,r?.holdsSave??!0):void 0;if(a&&`code`in a)return e.readyState===V.OPEN&&e.close(a.code,a.reason),a.refused;if(!(O||e.readyState!==V.OPEN)){Nn.set(e,{seat:r?.seat,save:a?.save,account:r?.player??i});try{Ui(e,{...t,name:i})}catch(t){console.error(t),e.close(4500)}finally{Nn.delete(e)}}}function Li(e){let t=An.get(e)??Promise.resolve(),n=()=>void 0,r=new Promise(e=>n=e),i=t.then(()=>r);return An.set(e,i),{ready:t,done:()=>{n(),An.get(e)===i&&An.delete(e)}}}function Bi(e){return H().some(t=>t.hostedSeat===e)}function Vi(e){for(let[t,n]of D)n===e&&(D.delete(t),_t(w,Yn(e)),t.close(4005));for(let[t,n]of P)n.player===e&&P.delete(t);un.add(e.record.account);let t=tn(w,e.record,y.Left,$n,e.save&&(()=>Zr(e,{reason:y.Left})));if(qe(w),t)return m({kind:U.Left,room:n,player:e.id,name:e.name,players:D.size}),e.save?.leave?.text}function Hi(e,t){for(let[n,r]of D)if(n!==e&&r.save?.key===t)return!0;for(let[n,r]of kn)if(n!==e&&r===t)return!0;return!1}function Ui(e,t){if(!M||t.type===f.Ping||t.type===f.Voice||t.type===f.Join||t.type===f.Leave)return Ki(e,t);let n=Ht.get(e)??0;if(n>=256){console.warn(`The room dropped a ${t.type} message a page sent while its scene mounted again: it holds 256 from one page at most.`);return}Ht.set(e,n+1),Vt.push(()=>{Ht.delete(e);try{Ki(e,t),qe(w)}catch(t){console.error(t),e.close(4500)}})}function Ki(e,t){if(!E?.active||t.type===f.Ping||t.type===f.Voice||t.type===f.Join||t.type===f.Leave)return qi(e,t);let n={step:J,kind:S.Message,connection:At.get(e)??-1,now:N(),message:t};rr(n.now,()=>qi(e,t)),E.record(n)}function G(e){Te!==void 0&&typeof e==`string`&&e!==Te&&m({kind:U.BuildMismatch,room:n,build:Te,page:e})}function qi(e,t){if(e.readyState!==V.OPEN)return;let n=D.get(e);if(t.type===f.Join){if(n||tr(e))return e.close(4002);if(t.rejoin&&Pi(t.rejoin)){ln.push({socket:e,message:t,key:t.rejoin,...A!==void 0&&{now:A}});return}let r=D.size+P.size+F.length,i=Nn.get(e)?.seat;if(r>=o&&i===void 0){let t={players:r,maxPlayers:o};return e.close(Z.Full,JSON.stringify(t))}sr(e,t);return}if(!n&&tr(e)){t.type===f.Leave&&(jn.add(e),pi(e,`left`));return}if(!n)return e.close(4001);if(t.type===f.Leave){jn.add(e),pi(e,`left`);return}if(t.type===f.Ping)return Da(e,n,t.sent);if(t.type===f.Shot)return Ji(e,n,t);if(t.type===f.Item){let e=Jn(n);e&&Si(n.itemCredit,N())&&Fe(w,e,t.intent);return}if(t.type===f.Message)return Xi(n,t);if(t.type===f.Press){Di({world:w,player:n,press:t,next:J,level:R(w)});return}if(t.type===f.Voice){Jo?Jo.receive(e,t)||e.close(4e3):!_&&t.signal===p.Open&&_a(e,St({type:ge.Voice,signal:ve.Unavailable},n.wire));return}if(t.type===f.Edit){d&&Pe(w,t);return}if(t.type===f.Walk)return Rr(n,t);Mr(n,t)}function Ji(e,t,r){let i=Jn(t);if(!i)return;let a=qa({world:w,character:i,shot:r,now:N()});if(!a)return;let{rule:o,reason:s}=a;na[o]=(na[o]??0)+1,(o===`origin`||o===`cover`)&&t.refusedShotOrigins++,t.refusalsLogged<4&&(t.refusalsLogged++,m({kind:U.ShotRefused,room:n,player:t.id,weapon:r.weapon,rule:o,reason:s})),r.id!==void 0&&_a(e,St({type:ge.ShotRefused,shot:r.id,rule:o,reason:s},t.wire))}function Xi(e,t){if(w.get(b)?.get(t.name)?.predicted){Oi(w,e,t,J,R(w));return}let r=ba({world:w,character:Yn(e),credits:e.messageCredits,message:t,now:N()});!r||e.dropsLogged>=4||(e.dropsLogged++,m({kind:U.MessageDropped,room:n,player:e.id,message:t.name,reason:r}))}function Zi(e){let t=e.get(g);t&&(ns(t,st.edgeButtons),en.buttons=t.buttons,e.set(g,en))}function Qi(e){let t=e.record.entity,n=t.isAlive()&&t.get(g);if(n){n.axes.fill(0),n.vectors.fill(0),n.changes.length=0,en.buttons=0,t.set(g,en),ar(w,t);for(let e of wn(w,t))e.get(se)?.corners.splice(0)}}let $i=0,ea=/* @__PURE__ */ new Set,ta=e=>{ea.has(e)||(ea.add(e),m({kind:U.Clamped,room:n,entity:e}))},na={};function ra(){let e={entities:vt(w),hash:0};return $i+=ir(e,ta),(fe||E?.active)&&Ft(e),e}let ia=ra(),aa=Ge(w),oa={hash:0,events:0},K={type:ge.Pong,sent:0,step:0,stepMilliseconds:0},sa=0,ca=/* @__PURE__ */ new Map,la=0,pa=0,ga=/* @__PURE__ */ new Map;function _a(e,t){if(e.readyState!==V.OPEN)return;if(e instanceof V){if(e.bufferedAmount>me){let t=performance.now(),n=ga.get(e)??t;if(t-n>=3e4)return va(e);ga.set(e,n)}else ga.delete(e)}e.send(t);let n=as(t);sa+=n,ca.set(e,(ca.get(e)??0)+n)}function va(e){let t=D.get(e);pa++,t&&m({kind:U.FellBehind,room:n,player:t.id,name:t.name,queuedBytes:e.bufferedAmount}),e.terminate()}let ya,Sa;function Ca(e){let t=Rn(w,e),r={},i=!1;for(let e in t.values){let n=t.values[e];ya&&ya[e]===n||(r[e]=n,i=!0)}let a=t.over!==void 0&&(ya===void 0||t.over!==Sa),o=ya!==void 0&&(e.parts??[]).some(e=>e in r);return ya=t.values,Sa=t.over,(i||a)&&m({kind:U.Timeline,room:n,step:J,values:r,...a&&{over:t.over}}),o}function Da(e,t,n){let r=performance.now();t.lastPing={sent:n,landed:r},!(r-t.pingAnswered<Uo)&&(t.pingAnswered=r,ja(e,t,n,r))}function Aa(){let e=performance.now();for(let[t,n]of D)n.lastPing&&ja(t,n,n.lastPing.sent+(e-n.lastPing.landed),e)}function ja(e,t,n,r){let i=j||M!==void 0,a=i?0:Wa+Math.min((r-Ga)/1e3*Bt,cn);K.sent=n,K.step=B(J+a/k,3);let o=i?0:Bt;K.rate=o===1?void 0:o,K.refusedShotOrigins=t.refusedShotOrigins||void 0,K.stepMilliseconds=B(Y>0?Ja/Y:Xa,3),K.early=t.early??void 0,K.earlyTick=t.early===null?void 0:t.earlyTick,K.late=t.late||void 0,K.starved=t.starved||void 0,t.early=null,t.late=0,t.starved=0,_a(e,St(K,t.wire))}function Ma(e){let t=Pa(e,!1);return t&&{type:ge.Owned,traits:t}}function Pa(e,t){let n=e.record.entity;if(!n.isAlive())return;let r={},i;for(let a of[n,...wn(w,n)]){let n=yt(w,a),o=bt(a);if(t&&Fa(a))for(let t of e.predicted)delete o[t];let s=mt(e.owned[n]??{},o);r[n]=o,s&&((i??={})[n]=s)}return e.owned=r,i}function Fa(e){return e.get(ee)?.simulation===Se.Predicted}function Ia(e){return wn(w,e.record.entity).map(e=>{let t=e.get(ee);return{entity:yt(w,e),serial:t?.serial??0,simulation:t?.simulation??Se.Predicted,...t?.context&&{context:t.context}}})}function La(e,t){let n=new Set(e.predicted),r,i={};for(let a of wn(w,e.record.entity)){if(!Fa(a))continue;let e=yt(w,a),o=Ve(a,{},n);i[e]=o,(!t[e]||!wt(t[e],o))&&((r??={})[e]=o)}for(let e of Object.keys(t)){if(i[e])continue;let t=Et(w,e);t?.isAlive()&&((r??={})[e]=Ve(t,{},n))}return e.rewound=i,r}function Ra(e){let t={type:ge.Ack,sequence:J},n=Pa(e,!0);n&&(t.owned=n);let r=Ia(e);wt(e.controlled,r)||(t.control=r,e.controlled=r);let i=La(e,e.rewound);i&&(t.rewound=i);for(let n of wn(w,e.record.entity)){let e=u.takePendingGrants(n);e&&((t.grants??={})[yt(w,n)]=e)}return e.pressAnswers.length>0&&(t.presses=e.pressAnswers.splice(0)),za(e,t),t}function za(e,t){let n=R(w);e.level!==n&&(t.level=n,e.level=n)}function Ba(e){if(e.baseline)return[Ra(e)];let t=Ma(e),n={type:ge.Ack,sequence:J},r=Ia(e);return wt(e.controlled,r)||(n.control=r,e.controlled=r),za(e,n),t?[n,t]:[n]}function Va(e,t,n){let r=Ba(t).map(e=>St(e,t.wire));for(let e of r)la+=as(e);if(typeof n==`string`){_a(e,n);for(let t of r)_a(e,t);return}_a(e,Zt([n,...r.filter(os)]))}function q(e,t){let n=e.filter(e=>e.tick>t);return n.length>0?{events:n}:{}}function Ha(){let e=yr(w);if(D.size===0)return;let t=ra(),n=pt(ia,t);e.length>0&&(n.events=e),oa.hash=t.hash,oa.events=Pt(e),ia=t,Hn(w,{snapshot:t,step:J,sendTicks:ze});let r={type:ge.Delta,delta:n},i={};for(let[n,a]of D){if(n.readyState!==V.OPEN)continue;let{wire:o}=a;if(a.welcomed){Va(n,a,i[o]??=St(r,o));continue}let c=Jn(a);_a(n,St({type:ge.Welcome,protocolVersion:Sn,player:a.id,...c&&{character:a.characterId},snapshot:t,edits:d,..._n&&{rejoin:a.rejoinKey},...Te!==void 0&&{build:Te},composition:ot,sendRate:s,tick:J-1,...q(e,J-1),controlled:a.controlled=Ia(a),predicted:a.predicted,rewound:(La(a,{}),a.rewound),predictionSeed:On(w),...R(w)>0&&{level:a.level=R(w)}},o)),a.welcomed=!0}return E?.active&&E.record({step:J,kind:S.Send,hash:t.hash,events:oa.events}),oa}let Ua={deltaSeconds:k},Wa=0,Ga=performance.now(),Ka=0,J=0;fr(w,J);let Y=0,Ja=0,Ya=0,Xa=0,Za=0,Qa=0,$a=0,eo=0,X=0,to=0,no=(e,t)=>m({kind:e,room:n,...t,slowSystems:Ta(Ln(w))}),ro=wa({onBehind:e=>no(U.Behind,e),onCaughtUp:e=>no(U.CaughtUp,e)},Ga),io=Ea();function ao(){let e=performance.now(),t=(j||M?0:(e-Ga)/1e3)*Bt,n=Ae(Wa,t);ro.note(e,t-n.frameSeconds),Ga=e,Wa=n.leftoverSeconds;for(let e=0;e<n.steps&&!M;e++)ho();e>=z&&oo(e),_e!==void 0&&I!==null&&e-I>=_e*1e3&&so()}function oo(e){z=e+Po;let t=e>=zn;t&&(zn=e+5e3),t&&j&&Aa();for(let[r,i]of Pn)if(e-i>=15e3){Pn.delete(r);let e=D.get(r);e&&m({kind:U.Silent,room:n,player:e.id,name:e.name}),r.terminate()}else t&&r.readyState===V.OPEN&&r.ping();for(let[t,n]of Fn){if(e-n<1e4)break;Fn.delete(t),t.terminate()}}function so(){I=null,m({kind:U.Emptied,room:n,seconds:_e??0}),us().then(ye,ye)}let co=()=>({paused:j,speed:Bt,step:J}),lo=be?{read:co,pause(){return qt(),j=!0,Wa=0,Aa(),co()},resume(e=1){return mi(e),qt(),Bt=e,j&&(Ga=performance.now()),j=!1,Aa(),co()},async set(e,t){M&&await M;let n;try{n=Dt(w,e),ko(n,t)}catch(e){throw new vi(e instanceof Error?e.message:String(e))}return E?.active&&E.record({step:J,kind:S.Set,now:N(),entities:n,traits:t}),{...co(),entities:n}},async step(e){hi(e),qt(),j=!0,Wa=0;for(let t=0;t<e;t++)Ut+=k*1e3,M&&await M,ho();return M&&await M,Aa(),co()}}:void 0;function uo(e){qt(),M=e().finally(()=>{Wt=performance.now(),M=void 0}).then(()=>{po();for(let e of Vt.splice(0))O||e()}),x&&M.catch(fs),po()}function fo(e){M?Vt.push(e):e()}function po(){x&&Aa()}async function mo(){M&&await M;let e=ho();return M&&await M,e}function ho(){let e=performance.now();jr(),Ke(w);let t=0;E?.active&&(J%er===0||E.startsPiece())&&(E.checkpoint(Oo()),t=performance.now()-e,Za=Math.max(Za,t)),D.forEach(Nr),D.forEach(zr),D.forEach(Fr),mr(w,J);let n=R(w);_r(w,Ua),We&&R(w)!==n&&(uo(()=>Wn(w,We)),D.forEach(Pr)),Ai(Qt),oi(),qe(w);let r=performance.now()-e;J++,fr(w,J),Y++,Ja+=r,Ya=Math.max(Ya,r);let i=r-t;Qa+=i,$a=Math.max($a,i);let a=xe&&x?Ca(xe):!1;Ka++;let o;if(Ka===ze){Ka=0;let e=performance.now();if(o=Ha(),o){let t=performance.now()-e;eo++,X+=t,to=Math.max(to,t)}}return a&&Go(),dn&&(dn=!1,D.size===0&&(I=performance.now(),E?.pause(Oo()))),o}let xo=hn(ae()),So=/* @__PURE__ */ new Set([...Ct,...Object.values(u).filter(Gt),...Object.values(Yr).filter(Gt)]);function Co(){return{stepsRun:J,stepsIntoSend:Ka,nextConnection:Lt,lastSent:ia,players:Array.from(D,([e,t])=>({connection:At.get(e)??-1,player:Er({...t,save:null})})),rejoinSecret:bn,keysIssued:xn,dropped:Array.from(P.values(),({player:e,until:t})=>({until:t,player:Er({...e,save:null})})),leftAccounts:[...un],bots:Er(yi()),controls:Er(Be(w))}}function To(e){let t=Cn(e);return t.save=void 0,t}function Do(){let{image:e,blobs:t}=He(w,xo);return{format:9,step:J,now:N(),sceneReloads:R(w),world:e,room:Co(),blobs:t}}function Oo(){let e=Do();return Kn(w,{image:e.world,blobs:e.blobs},xo),e}function ko(e,t){try{Dr(w,e,t,e=>xo.find(e))}finally{qe(w)}}function Ao({record:e,player:t}){if(Object.keys(Ye).length===0)return;let n=ai(t,t,e,null);return n.ok?n.save:void 0}function jo(e,t){A=e.now;try{if(e.kind===S.Message){let n=e.save&&Ao(e.save),{account:r}=e;(n||r!==void 0)&&Nn.set(t,{...n&&{save:n},...r!==void 0&&{account:r}});try{qi(t,e.message)}finally{(n||r!==void 0)&&Nn.delete(t)}}else e.kind===S.Close&&pi(t,e.broken?`broken`:`closed`);jr({endHolds:!1})}finally{A=void 0,qe(w)}}let Mo={readStep:()=>J,write:ko,take(e,t){fo(()=>jo(e,t))},move({accounts:e,now:t}){fo(()=>{A=t;try{Io(e)}finally{A=void 0,qe(w)}})},step:mo,capture:Do,checkpoint:Oo,isEngineTrait(e){let t=xo.find(e);return t!==void 0&&So.has(t)},restore(e,t){if(e.sceneReloads>0)throw Error(`The checkpoint at step ${e.step} was taken after the room's scene started again, so the scene's entities hold numbers a fresh room's mount does not give them: continue from a checkpoint taken before its first restart, and the continue takes the restart again.`);aa();let n=Qn(w,{image:e.world,blobs:e.blobs},xo);aa=Ge(w);let r=e.room;J=r.stepsRun,Ka=r.stepsIntoSend,Lt=r.nextConnection,ia=r.lastSent,D.clear(),At.clear();for(let{connection:e,player:n}of r.players){let r=t(e);D.set(r,To(n)),At.set(r,e)}r.rejoinSecret!==void 0&&(bn=r.rejoinSecret,xn=r.keysIssued),P.clear();for(let{until:e,player:t}of r.dropped??[]){let n=To(t);P.set(n.rejoinKey,{player:n,since:J,until:e})}un.clear();for(let e of r.leftAccounts)un.add(e);return Zn(w,[...H().map(e=>e.record),...Cn(r.bots)??[]]),Xn(w,Cn(r.controls)),nn.length=0,on.length=0,ln.length=0,F.length=0,n},readStream(e){let t=D.get(e);if(t)return{player:t.id,...t.characterId!==void 0&&{character:t.characterId},entities:ia.entities,owned:t.owned,controlled:t.controlled??[],edits:d,composition:ot,tick:J-1,predicted:t.predicted,rewound:t.rewound,predictionSeed:On(w),sendRate:s}}};function No(e){for(let t of mn)e.includes(t)||mn.delete(t);on.push(...e)}function Fo(e){let t=new Set(e);for(let e of t)(Bi(e)||T?.listLeaving().includes(e))&&mn.add(e);t.size>0&&T?.request();let n=H().filter(({hostedSeat:e})=>e!==void 0&&t.has(e)).map(({record:e})=>e.account);if(n.length===0)return;let r=N();E?.record({step:J,kind:S.Moved,now:r,accounts:n}),rr(r,()=>Io(n))}function Io(e){let t=new Set(e);for(let[e,n]of D)t.has(n.record.account)&&(D.delete(e),_t(w,Yn(n)),Vr(n,y.Left),e.close(Z.PlayingElsewhere,yo));for(let[e,{player:n}]of P)t.has(n.record.account)&&(P.delete(e),Vr(n,y.Left));D.size===0&&I===null&&(I=performance.now())}function Lo(){let e=/* @__PURE__ */ new Set([...fn.keys(),...T?.listLeaving()??[]]);for(let t of D.values())t.hostedSeat!==void 0&&e.add(t.hostedSeat);for(let{player:t}of P.values())t.hostedSeat!==void 0&&e.add(t.hostedSeat);return[...e]}function zo(e,t){e.report(t).then(e=>{if(O)return;if(!e.ended)return No(e.moved);let t=/* @__PURE__ */ Error(`The platform ended this room: its row is closed.`);Ko(U.Failed,t);let n=()=>h?.(t);ds().then(n,n)},e=>console.error(e))}let Bo=-1/0,Ho;function Wo(){if(!C||O||Ho!==void 0)return;let e=Bo+5e3-performance.now();Ho=setTimeout(()=>{Ho=void 0,O||ps(Go)()},Math.max(0,e))}function Go(){Bo=performance.now();let e={mean:B(Y>0?Ja/Y:0,3),max:B(Ya,3)},t=B(process.memoryUsage().rss/2**20,1),r=hs.read(),i={mean:B(e.mean/xa,3),max:B(e.max/xa,3)},{skippedSteps:a,behindSeconds:o}=ro.take(),s=io(),c=tt.take(),l=w.entities.length,u=nt?.note(l);u?.growing&&m({kind:U.SceneGrowing,room:n,...u.growing,sceneObjects:u.sceneObjects,entities:l}),C&&zo(C,{players:D.size,seats:Lo(),acknowledged:[...mn],steps:Y,stepMilliseconds:e,memoryMegabytes:t,stepShare:i,skippedSteps:a,behindSeconds:o,cpuShare:s,systemFaults:Object.values(c).reduce((e,t)=>e+t,0)}),m({kind:U.Heartbeat,room:n,players:D.size,steps:Y,step:J,stepMilliseconds:e,memoryMegabytes:t,heapMegabytes:r,stepShare:i,slowSystems:Ta(xr(w)),events:vr(w),skippedSteps:a,behindSeconds:o,cpuShare:s,stepMillisecondsPerPlayer:B(e.mean/Math.max(D.size,1),3),systemFaults:c,entities:l,...u&&{sceneObjects:u.sceneObjects},sentBytes:{total:sa,max:Math.max(0,...ca.values()),words:la},sendMilliseconds:{mean:B(eo>0?X/eo:0,3),max:B(to,3)},clampedCoordinates:$i,refusedShots:na,fellBehind:pa,inputs:{...Xt},...E&&{checkpointMilliseconds:B(Za,3),ownStepMilliseconds:{mean:B(Y>0?Qa/Y:0,3),max:B($a,3)}}}),Za=0,Y>0&&(Xa=Ja/Y),Y=0,sa=0,ca.clear(),la=0,$i=0,na={},pa=0,Xt.starved=0,Xt.late=0,ea.clear(),Ja=0,Ya=0,Qa=0,$a=0,eo=0,X=0,to=0;for(let e of D.values())e.dropsLogged=0,e.refusalsLogged=0,e.inputFloodLogged=!1}function Ko(e,t){m({kind:e,room:n,step:J,...Oa(t)})}function qo(e,t){let n=D.get(e);n&&e.readyState===V.OPEN&&_a(e,St(t,n.wire))}let Jo=await _?.({sendSignal:qo,nameSpeaker:e=>D.get(e)?.characterId}),Xo=lt(),Q=x&&lo?Wr((e,t)=>{bi(lo,e,t)||(t.statusCode=426,t.end(`Upgrade Required`))}):void 0,Zo=x?new Jr(Q?{server:Q,maxPayload:sn}:{port:e,host:t,maxPayload:sn}):void 0;try{Q?await new Promise((n,r)=>{Q.once(`listening`,n),Q.once(`error`,r),Q.listen(e,t)}):Zo&&await new Promise((e,t)=>{Zo.once(`listening`,e),Zo.once(`error`,t)})}catch(e){throw Jo?.close(),gt(),aa(),await Ue(),w.destroy(),e}let Qo=lt();Zo?.on(`connection`,(e,t)=>{if(O){e.terminate();return}At.set(e,Lt++),zt.set(e,ls(t,De));let n=performance.now();if(Pn.set(e,n),Fn.size>=2*o){let[e]=Fn.keys();Fn.delete(e),e.terminate()}Fn.set(e,n),e.on(`pong`,()=>Pn.set(e,performance.now())),e.on(`message`,(t,n)=>{Pn.set(e,performance.now());try{wi(e,t,n),qe(w)}catch(t){console.error(t),e.close(4500)}(D.has(e)||L.has(e)||tr(e))&&Fn.delete(e)}),e.on(`close`,t=>{Pn.delete(e),Fn.delete(e),ga.delete(e),Jo?.leave(e),Ht.delete(e),!O&&!D.has(e)&&!tr(e)?fi(e):O||pi(e,t===go?`broken`:t===yn?`moved`:`closed`)}),e.on(`error`,()=>{})});let $;async function is(e,t=!1,n){O=!0,clearInterval(gs),clearInterval(_s),clearTimeout(Ho),clearInterval(vs),hs.stop();let r=/* @__PURE__ */ new Set,i=[...D.keys()],a=[];if(e)for(let e of H()){let t=e.save;tn(w,e.record,y.RoomClosing,$n,t&&T?()=>Qr(t):void 0)||a.push(e.record)}if(e)for(let e of yi())tn(w,e,y.RoomClosing,$n);if(e&&T){if(await Promise.race([T.close(),new Promise(e=>setTimeout(()=>e(!1),8e3))]))for(let e of i)r.add(e);T.stop()}for(let e of a)jt(w,e);for(let e of bs??[])e();e&&await Promise.race([Promise.all(fn.values()),new Promise(e=>setTimeout(e,8e3))]),T?.stop(),t&&await C?.close(n);let o=[...Zo?.clients??[]];for(let e of o)if(t){let t={saved:r.has(e)};e.close(Z.RoomFailed,JSON.stringify(t))}else e.close(1001),e.terminate();if(t){await vo(o,_o);for(let e of o)e.terminate()}Zo&&await new Promise((e,t)=>Zo.close(n=>n?t(n):e())),Q&&await new Promise(e=>{Q.closeAllConnections(),Q.close(()=>e())}),await E?.close(),D.clear();let s=T?.closeUnreadable;t||await C?.close(s&&{seat:s.key,owner:s.owner,reason:s.reason}),Jo?.close(),gt(),aa(),await Ue(),w.destroy()}function us(){return $??=is(!0),$}function ds(){return $??=is(!1),$}function fs(e){if($)return;Ko(U.Failed,e);let t=()=>h?.(e);$=is(!0,!0),$.then(t,t)}let ps=e=>()=>{try{e()}catch(e){fs(e)}},ms=ps(ao);xe&&x&&Ca(xe);let hs=ci(e=>m({kind:U.HeapNearLimit,room:n,heapMegabytes:e})),gs=x?setInterval(ms,bo):void 0,_s=x?setInterval(ps(Go),ce*1e3):void 0,vs=x&&T?setInterval(ps(oi),1e3):void 0,ys=e=>{for(let t of H()){let n=t.save;if(t.character!==e||!n)continue;let r=n.readDestroyed(e);r!==null&&`problem`in r&&ei({reason:ma.Unreadable,message:`${n.key}'s record could not be read as the game destroyed her character: the room ends, and every player goes back to the last checkpoint. ${r.problem.message}`,unreadable:{key:n.key,owner:r.problem.key,reason:r.problem.message}})}},bs=T?[je(w,(e,t)=>ys(t)),w.onRemove(ue,ys)]:void 0;x&&C&&Go();let xs=(Q??Zo)?.address()??{address:``,family:``,port:0};if(typeof xs!=`object`||!xs)throw Error(`The room's server is listening on no port.`);return E?.listen(xs.port),{port:xs.port,run:E?.run,heartbeatSeconds:ce,maxPlayers:o,sendRate:s,...xe&&{timeline:gn(xe)},...Te!==void 0&&{build:Te},stageMilliseconds:{navMeshBaked:xt,voiceOpened:Xo,listening:Qo},address:xs,driver:Mo,...lo&&{director:lo},world:w,readWaitingTicks:e=>{for(let t of D.values())if(t.character===e)return t.inputs.filter(({tick:e})=>e>=J).length},close:us,fail:fs,reportNow:Wo}}function ds(e){let t={readyState:V.OPEN,send:n=>{t.readyState===V.OPEN&&e(n)},close:()=>{t.readyState=V.CLOSED}};return t}function fs({entity:e,trait:t,path:n},r,i){if(t!==void 0)return r.isEngineTrait(t);if(e!==void 0)return!1;if(n.startsWith(`blobs.`))return E.has(n.slice(6));let a=[...i].sort((e,t)=>t.length-e.length).find(e=>n===`codecs.${e}`||n.startsWith(`codecs.${e}.`));return a!==void 0&&r.isEngineTrait(a)}function ps(e,t){let n=jn(t.folder,t.run);return We(n),{...e,invulnerable:n.invulnerable===!0,maxPlayers:n.maxPlayers,sendRate:n.sendRate,allowEdits:n.allowEdits,rejoinSeconds:n.rejoinSeconds}}async function ms(e,t){let n=await us({...ps(e,t),serve:!1,hashSends:!0}),{blobs:r}=An(t.folder,t.run,t.from);if([...E].some(e=>e in r))try{await rn()}catch(e){throw await n.close(),e}return n}var hs=600;async function gs(e,t){let n=performance.now(),{folder:r,run:i,from:a,to:o,output:c}=t;We(jn(r,i));let l=An(r,i,a),u=kn(r,i,a,o).filter(({index:e})=>e>=l.actions),d=nn(r,i).filter(e=>e>a&&e<=o),f=c&&rt(),p=0,ee=()=>{c&&f&&f.length!==0&&Ar(r,c.id,f.take())},te=e=>c?c.at+(e-c.step)*k*1e3:0,m=0,ne=/* @__PURE__ */ new Map,h=t=>{let n=ne.get(t);return n||(n=ds(n=>{if(!f||t!==c?.connection)return;let r=e.readStep();r<=c.step||(m++,typeof n==`string`?f.writeText(pe.RoomText,te(r),n):n instanceof Uint8Array&&f.writeBytes(pe.RoomBytes,te(r),n),++p%hs===0&&ee())}),ne.set(t,n),n)},re=e.restore(l,h).problems;if(c){let{offset:e=0,clockSlips:t}=L(r,c.rerunOf.id);jr(r,{id:c.id,marks:[],pieces:{},lastAt:0,offset:e,...t&&{clockSlips:t},rerunOf:c.rerunOf})}f&&c&&f.writeJson(pe.Session,0,{...c.session,startedAt:(/* @__PURE__ */ new Date()).toISOString()});let g={run:i,from:a,to:o,restore:re,actions:u.length,sends:{recorded:u.filter(({kind:e,step:t})=>e===S.Send&&t<=o).length,compared:0,matched:0,extra:0},checkpoints:[],frames:0,milliseconds:0},_=l.actions,v=()=>{let t=e.readStep();for(;d.length>0&&d[0]===t;){let n=An(r,i,t);if(n.actions>_)return;d.shift();let a=e.checkpoint();g.checkpoints.push({step:t,differences:[...Ye(n.world,a.world),...Je(n.blobs,a.blobs)].map(t=>fs(t,e,[...Object.keys(n.world.codecs),...Object.keys(a.world.codecs)])?{...t,engine:!0}:t)})}},y=()=>{if(!f||!c)return;let t=e.readStream(h(c.connection));if(!t)return;let{character:n}=t,r={type:ge.Welcome,protocolVersion:Sn,player:t.player,...n!==void 0&&{character:n},snapshot:{entities:{...t.entities,...Object.fromEntries(Object.entries(t.owned).map(([e,n])=>[e,{...t.entities[e]??{},...n,...t.rewound[e]}]))},hash:0},edits:t.edits,composition:t.composition,tick:t.tick,controlled:t.controlled,predicted:t.predicted,rewound:t.rewound,predictionSeed:t.predictionSeed,sendRate:t.sendRate};f.writeJson(pe.Resume,te(c.step)-1,r)},ie=u.at(-1)?.step??a,b=0;for(;;){let n=e.readStep();for(;b<u.length&&u[b].step<=n;){v();let t=u[b++];if(_=t.index+1,t.kind!==S.Send){if(t.kind===S.Set)try{e.write(t.entities,t.traits)}catch(e){re.push(`At step ${t.step}, a tool's write of ${Object.keys(t.traits).join(`, `)} on ${t.entities.join(`, `)} did not take: ${e instanceof Error?e.message:String(e)}`)}else t.kind===S.Moved?e.move(t):e.take(t,h(t.connection))}}if(v(),c&&n===c.step&&y(),t.state&&n===t.state.step&&s(t.state.file,JSON.stringify({step:n,world:e.capture().world})),n>=o)break;let r=await e.step();r!==void 0&&ae(r,e.readStep())}function ae({hash:e,events:t},n){let r=_s(u,b,n);if(!r){n<=ie&&g.sends.extra++;return}b=r.at+1,_=r.action.index+1,g.sends.compared++;let{action:i}=r,a=i.events;i.hash===e&&a===t?g.sends.matched++:g.sends.firstDifferent??={step:n,recorded:i.hash,continued:e,...a!==t&&{events:{recorded:a,continued:t}}}}if(ee(),c){let e=L(r,c.id);jr(r,{...e,rerunOf:c.rerunOf})}return g.frames=m,g.milliseconds=Math.round(performance.now()-n),s(t.report,JSON.stringify(g)),g}function _s(e,t,n){let r=e[t];if(r?.kind===S.Send&&r.step===n)return{action:r,at:t}}var vs={name:`room:no-page-bundle`,enforce:`post`,configEnvironment(e,t){e===`client`&&(t.optimizeDeps={...t.optimizeDeps,noDiscovery:!0,include:[]})}};function ys(e){let t=e.config.plugins.find(({name:e})=>e===`spawnite`)?.api,n=typeof t==`object`&&t?Reflect.get(t,`readBuild`):void 0;if(typeof n!=`function`)return;let r=n();return typeof r==`string`?r:void 0}async function bs(e,t,{plugins:n,physics:r,allowEdits:i}){let a=tt(n,r);a.add(_e),a.add(ae),i&&a.add(te);try{return await e(t,a)}catch(e){throw a.destroy(),e}}var xs=[];function Ss(e){return/^[@\w]/.test(e)&&!e.includes(`:`)}function Cs(e){let n=import.meta.resolve(`@spawnite/engine`);xs.length===0&&t({resolve(e,t,r){let i=t.parentURL;return r(e,i!==void 0&&Ss(e)&&xs.some(e=>i.startsWith(e))?{...t,parentURL:n}:t)}});for(let t of/* @__PURE__ */ new Set([e,i(e)]))xs.push(H(t+c.sep).href)}function ws(e){let t=c.join(e,`build.json`);if(!n(t))return;let{build:i}=JSON.parse(r(t,`utf8`));return typeof i==`string`?i:void 0}function Ts(e){Cs(c.dirname(e));let t=H(e).href,n;return{load:async()=>(n??=await import(t),n),readTraitModules:()=>[{path:`@spawnite/engine/core`,exports:u},...n?[{path:t,exports:n}]:[]],readBuild:()=>ws(c.dirname(e)),onChange:()=>void 0,invalidate:()=>void 0,close:async()=>void 0}}async function Es(e){let t=c.resolve(e),n=Os(t),r=await ks(n),i=await Is(n),a=`/${r.normalizePath(c.relative(n,t))}`;return{load:()=>i.ssrLoadModule(a),readTraitModules:()=>Ls(i),readBuild:()=>ys(i),onChange:e=>i.watcher.on(`change`,t=>{i.moduleGraph.getModulesByFile(r.normalizePath(t))&&e(t)}),invalidate:()=>i.moduleGraph.invalidateAll(),close:()=>i.close()}}async function Ds(e){return c.extname(e)===`.mjs`?Ts(c.resolve(e)):Es(e)}function Os(e){let t=c.dirname(e);for(;!n(c.join(t,`package.json`));){let n=c.dirname(t);if(n===t)throw Error(`${e} sits in no package with a Vite.`);t=n}return t}async function ks(t){let n=e(c.join(t,`package.json`));return await import(H(n.resolve(`vite`)).href)}function As(e){return Array.isArray(e)&&e.every(e=>!e||M(e)||As(e))}function js(e){if(typeof e!=`object`||!e)return`is not { measures, parts, over }`;let{measures:t,parts:n,over:r}=e;if(typeof t!=`object`||!t)return`holds no measures: give each a name, as { measures: { kills: { kind: "counter", read } } }`;for(let[e,n]of Object.entries(t)){let{kind:t,read:r}=n??{};if(!Object.values(ie).includes(t)||typeof r!=`function`)return`whose measure ${e} is not { kind: "counter" or "gauge", read }, a function of the world`}if(n!==void 0){if(!Array.isArray(n))return`whose parts are not a list of its measures' names`;for(let e of n)if(typeof e!=`string`||!(e in t))return`whose parts name ${String(e)}, which none of its measures is`}if(r!==void 0&&typeof r!=`function`)return`whose over is not a function of the world`}function Ms(e,t){let n=c.basename(e,c.extname(e)),{[n]:r,plugins:i,movement:a,timeline:o,save:s,room:l,physics:u}=t,d=l===void 0?[]:qr(l,`${e}'s room, which defineRoom makes,`);if(d.length>0)throw Error(d.join(`
2
+ `));if(s!==void 0&&!Ht(s))throw Error(`${e} exports a save that defineSave did not make: export the declaration src/save.ts makes, as export { save } from "../save", the same object <Game save> takes.`);if(u!==void 0&&!Bt(u))throw Error(`${e} exports physics that definePhysics did not make: export the settings src/game.ts makes, as export { physics } from "../game", the same object <Game physics> takes.`);if(t.systems!==void 0)throw Error(`${e} exports systems, which a room no longer reads: put the game's systems in a plugin made with definePlugin, list it in the plugins src/game.ts exports, and re-export that list here, as export { plugins } from "../game".`);if(i!==void 0&&!As(i))throw Error(`${e} exports plugins that are not a list of plugins: export the list src/game.ts makes with definePlugin and the engine's plugins, as export { plugins } from "../game".`);if(a!==void 0)throw Error(`${e} exports movement, which a room does not read: every player's character moves as the scene's <CharacterSpawn> says. Set it on the <CharacterSpawn>, as <CharacterSpawn preset="realistic" movement={{ speed: 7 }} />.`);if(typeof r!=`function`)throw Error(`${e} exports no ${n} component: a room mounts the component its scene's file exports under the file's name, beside the game's plugins where it exports them.`);let f=o===void 0?void 0:js(o);if(f!==void 0)throw Error(`${e} exports a timeline ${f}. A SceneTimeline is { measures: { wave: { kind: "gauge", read: (world) => number } }, parts: ["wave"], over: (world) => boolean }.`);return{component:r,plugins:i,timeline:o,save:s,room:l,physics:u}}function Ns(e){if(typeof e!=`object`||!e)return!1;let{name:t,settings:n}=e;return typeof t==`string`&&typeof n==`object`&&!!n}function Ps(e){if(typeof e!=`object`||!e)return!1;let{weapons:t,weapon:n,play:r,destination:i}=e;return n!==void 0||r!==void 0&&typeof r!=`function`||i!==void 0&&typeof i!=`function`?!1:t===void 0||Array.isArray(t)&&t.every(Ns)}function Fs(e,t){let{bot:n}=t;if(n!==void 0){if(!Ps(n))throw Error(`${e} exports a bot whose weapons are not a list of { name, settings }, or whose play or destination is not a function: export a SceneBot, such as { weapons: [{ name: "blaster", settings: blasterSettings }], play({ world, character, send }) {} }.`);return n}}async function Is(e){return(await ks(e)).createServer({root:e,configLoader:`runner`,server:{middlewareMode:!0,hmr:!1,ws:!1},appType:`custom`,logLevel:`error`,ssr:{external:[`@spawnite/engine`]},plugins:[vs]})}function Ls(e){let t=[{path:`@spawnite/engine/core`,exports:u}];for(let{url:n,ssrModule:r}of e.moduleGraph.idToModuleMap.values())r&&t.push({path:n,exports:r});return t}async function Rs(e,t,n){let r=ui(),{mountHeadlessScene:i}=await import(import.meta.resolve(`@spawnite/engine`)),{component:a,plugins:o,physics:s,timeline:c,save:l,room:u}=Ms(t,await e.load()),d=r(),f=await bs(i,a,{plugins:o,physics:s,allowEdits:n}),p=r(),ee=Math.round(f.world.get(ae)?.milliseconds??0);return{scene:f,timeline:c,save:l,room:u,stageMilliseconds:{imported:d,mounted:Math.max(0,p-ee),navMeshBaked:ee}}}async function zs(e,t,n){let r=ps(t,n),i=await Ds(e);try{let{scene:t,save:a}=await Rs(i,e,r.allowEdits),o=await ms({...r,scene:t,save:a,traitModules:i.readTraitModules},n);try{return await gs(o.driver,n)}finally{await o.close()}}finally{await i.close()}}async function Bs(e,{watch:t=!0,...n}){let r=ui(),i=await Ds(e),a=r();async function o(t){let r,o;try{r=i.readBuild(),o=await Rs(i,e,n.allowEdits)}catch(e){throw await n.seats?.failLoad(e instanceof Error?e.message:String(e)),e}let{scene:s,timeline:c,save:l,room:u,stageMilliseconds:d}=o,f=await us({...n,maxPlayers:n.maxPlayers??u?.maxPlayers,sendRate:n.sendRate??u?.sendRate,rejoinSeconds:n.rejoinSeconds??u?.rejoinHoldSeconds,port:t,scene:s,timeline:c,save:l,build:r,traitModules:i.readTraitModules}),p=d.imported+a;return a=0,{...f,stageMilliseconds:{...d,...f.stageMilliseconds,imported:p,navMeshBaked:d.navMeshBaked+f.stageMilliseconds.navMeshBaked}}}let s;try{s=await o(n.port)}catch(e){throw await i.close(),e}let l=Promise.resolve(),d=n.log??di;return i.onChange(e=>{t&&(l=l.then(async()=>{d({kind:U.Restarted,room:n.roomId,file:c.resolve(e)}),await s.close(),i.invalidate(),u.beginDefinitionReload(),s=await o(s.port)}).catch(e=>console.error(e)))}),{get port(){return s.port},get run(){return s.run},get heartbeatSeconds(){return s.heartbeatSeconds},get maxPlayers(){return s.maxPlayers},get sendRate(){return s.sendRate},get timeline(){return s.timeline},get build(){return s.build},get stageMilliseconds(){return s.stageMilliseconds},close:async()=>{await l,await s.close(),await i.close()},fail:e=>s.fail(e),reportNow:()=>s.reportNow()}}async function Vs(e){let t=await Ds(e);try{return Fs(e,await t.load())}finally{await t.close()}}export{K as A,U as B,Ao as C,No as D,So as E,W as F,di as H,Ni as I,xi as L,la as M,aa as N,oa as O,na as P,Si as R,wo as S,us as T,ui as U,li as V,ci as W,Mo as _,Bs as a,Oo as b,To as c,Bo as d,Ro as f,Io as g,Fo as h,Is as i,ca as j,sa as k,xo as l,zo as m,Vs as n,Z as o,Lo as p,bs as r,jo as s,zs as t,Eo as u,Ho as v,ko as w,Co as x,Do as y,fi as z};
@@ -0,0 +1,3 @@
1
+ Third-party notices
2
+
3
+ These files include no code from other packages.
package/dist/bots.js ADDED
@@ -0,0 +1 @@
1
+ import{n as e}from"./BJWBohZM.js";import{existsSync as t}from"node:fs";import{CharacterCapsuleTrait as n,ClientMessageType as r,ControlledTrait as i,HealthTrait as a,HitZonesTrait as o,NetworkEntitiesTrait as s,PoseTrait as c,RoomMessageType as l,ShotJudge as u,ShotJudgedEvent as d,TransformTrait as f,WeaponNumber as p,WireFormat as m,applyDelta as h,applySnapshot as g,buildMismatchCloseCode as ee,characterInputs as _,claimShotHits as te,createGameWorld as v,defaultCharacterCapsule as ne,encodeMessage as y,findTrait as b,fixedStepSeconds as x,holdsWeapon as re,layOutComposedInputs as S,leadCeilingTicks as C,leadFloorTicks as w,measureDeltaBytes as T,openDelayedLine as ie,pingIntervalSeconds as ae,protocolVersion as E,random as D,readInputLayout as O,readRoomFrame as k,readSendTicks as A,readWeaponNumber as oe,resolveShotJudge as se,seedRandom as j,setInputLayout as M,startLeadTicks as N,startMarginTicks as P,targetEarlyTicks as F,walksPerSecond as I,writeDumpedTraits as L,writeInputValue as R}from"@spawnite/engine/internal/room";import{Vector3 as z}from"three";import{directLink as ce,parseLinkConditions as le}from"@spawnite/schema";import{WebSocket as B}from"ws";import{setTimeout as V}from"node:timers/promises";function ue({skill:e,world:t,seed:n}){return j(t,n),{skill:e,world:t,target:void 0,chosenAt:0,lastFeet:new z,lastAt:0,velocity:new z,backing:!1,decidedAt:-1/0}}function H(e,t){return e^Math.imul(t+1,2654435769)|0}function U(e,t){if(!e.isAlive()||e.has(i))return;let n=e.get(a),r=e.get(f)?.position;if(!n||!r||n.current<=0)return;let o=new z(r.x,r.y,r.z);return{entity:e,feet:o,distance:o.distanceTo(t)}}function de(e,t){let n=[];for(let r of e.query(a,f)){let e=U(r,t);e&&n.push(e)}return n.sort((e,t)=>e.distance-t.distance)}function fe(e,{from:t,targets:n,reach:r,now:i}){let a=n[0];if(!e.skill)return a;let o=e.target===void 0?void 0:U(e.target,t);return o!==void 0&&o.distance<=r&&(a===void 0||a.entity===o.entity||a.distance>=o.distance*.6)?o:(e.target=a?.entity,e.chosenAt=i,a&&e.lastFeet.copy(a.feet),e.lastAt=i,e.velocity.set(0,0,0),a)}function pe(e,{target:t,now:n}){let r=(n-e.lastAt)/1e3;r>0&&e.velocity.copy(t.feet).sub(e.lastFeet).divideScalar(r),e.lastFeet.copy(t.feet),e.lastAt=n}function me(e,t){return!e.skill||t-e.chosenAt>=e.skill.reactionSeconds*1e3}function he(e,t){return e.skill?t.addScaledVector(e.velocity,-e.skill.trackingSeconds):t}function W(e){return Math.sqrt(-2*Math.log(1-D(e)))*Math.cos(2*Math.PI*D(e))}var G=new z(0,1,0),K=new z(1,0,0),q=new z,J=new z;function ge(e,t){let n=e.skill?.spreadRadians??0;if(n<=0)return t;q.crossVectors(t,G),q.lengthSq()<1e-6&&q.crossVectors(t,K),q.normalize(),J.crossVectors(q,t).normalize();let r=W(e.world)*n,i=W(e.world)*n;return t.addScaledVector(q,r).addScaledVector(J,i).normalize()}function _e(e,{distance:t,now:n}){let r=e.skill?.backAwayMetres??0;return!e.skill||r<=0?!1:n-e.decidedAt<e.skill.reactionSeconds*1e3?e.backing:(e.backing=t<=r,e.decidedAt=n,e.backing)}var ve=12,ye=2,be=2e3/I,xe=.5,Se=1e3,Ce=3e3,we=6e4,Te=.6,Ee=1.5,De=1,Oe=.05,ke=2;function Ae(){return{ticks:N,started:!1}}function je(e,{roundTripMilliseconds:t,early:n}){e.started?n!==void 0&&(n<F?e.ticks+=Math.ceil(F-n):n>F+ke&&--e.ticks):(e.started=!0,e.ticks=w+Math.ceil(t/(x*1e3))+P),e.ticks=Math.min(Math.max(e.ticks,w),C)}function Me(e,t){return t?e instanceof Uint8Array?e:Buffer.concat(e):String(e)}function Ne(e){let t={};for(let n in e)b(n)&&(t[n]=e[n]);return t}function Pe(e){let t={};for(let n in e)t[n]=Ne(e[n]);return t}function Fe(e,[t,r,i]){let a=Ie(e,[t,r,i]);return a?Math.min(De,(a.get(n)?.height??2)/2):De}function Ie(e,[t,n,r]){for(let i of e.query(a,f)){let e=i.get(f)?.position;if(e&&Math.hypot(e.x-t,e.y-n,e.z-r)<=.01)return i}}var Y=new z;function Le(e,t,n,r){let[i,a,s]=t,c=n===void 0?void 0:Ie(e,t),l=c?.get(o)?.find(({name:e})=>e===n);if(!c||!l)return r.set(i,a+Fe(e,t),s);let{at:u,to:d=u}=l;Y.set((u.x+d.x)/2,(u.y+d.y)/2,(u.z+d.z)/2);let p=c.get(f)?.rotation;return p&&Y.applyQuaternion(p),r.set(i,a,s).add(Y)}function Re({x:e,y:t,z:n}){return[e,t,n]}function ze(e,t){let n,r=1/0;for(let i of e){let e=Math.hypot(t.x-i.at[0],t.z-i.at[2]);e<=i.radius&&e<r&&(r=e,n=i)}return n}function Be(e,t,n){let r=e.x-t[0],i=e.z-t[2],a=Math.hypot(r,i);if(a<.001)return[0,-1];let o=r/a,s=i/a;return[o*Math.cos(n)-s*Math.sin(n),-o*Math.sin(n)-s*Math.cos(n)]}function X(e,t){return Math.atan2(-(t[0]-e.x),-(t[2]-e.z))}function Ve(e,t,{intent:n,heading:r,strafe:i}){let a=O(e);a.places.has(_.move.name)&&(R(a,t,_.move,{x:n[0],y:n[1]}),R(a,t,_.heading,r),R(a,t,_.strafe,i))}function He(e){return{buttons:e.buttons||void 0,axes:e.axes,vectors:e.vectors,angles:e.angles}}function Ue({world:e,character:t}){return t===null?void 0:e.get(s)?.get(t)}function We(e,t=[]){for(let{name:n,value:r}of t){if(n!==d.name)continue;let{shooter:t,hits:i}=r;if(t===e.character){e.report.hits+=i.length;for(let{damage:t}of i)e.report.damage+=t}}}function Ge(e){let t=Ue(e)?.get(f)?.position;t&&(e.lastFeet&&(e.report.walkedMetres+=e.lastFeet.distanceTo(t)),e.lastFeet=(e.lastFeet??new z).copy(t))}function Ke(e,t){e.report.receivedBytes+=typeof t==`string`?Buffer.byteLength(t):t.length,e.report.receivedFrames++;for(let{message:n}of k(t))qe(e,n)}function qe(e,t){if(t.type===l.Pong){e.viewStep??=Math.floor((t.step-1)/e.sendTicks)*e.sendTicks;let n=t.rate??1;n>0&&je(e.lead,{roundTripMilliseconds:(performance.now()-t.sent)*n,early:t.early});return}if(t.type===l.Welcome){if(t.protocolVersion!==E){e.refused=!0;return}e.character=t.character??null,e.player=t.player,e.controlled=t.controlled.map(({entity:e})=>e),e.level=t.level??0,e.report.character=t.character??null,e.welcomeTick=t.tick,e.sendTicks=A(t.sendRate),M(e.world,S(t.composition.inputs)),g(e.world,{...t.snapshot,entities:Pe(t.snapshot.entities)});return}if(t.type===l.Ack){t.control&&(e.controlled=t.control.map(({entity:e})=>e)),t.level!==void 0&&(e.level=t.level);return}if(t.type===l.Owned){let n=e.world.get(s);for(let[e,r]of Object.entries(t.traits)){let t=n?.get(e);t?.isAlive()&&L(t,Ne(r))}return}if(t.type!==l.Delta)return;T(t.delta,e.report.deltaBytes);let n={changed:Pe(t.delta.changed),removed:t.delta.removed,hash:t.delta.hash};h(e.world,n),We(e,t.delta.events),Ge(e),e.moves++,e.viewStep!==null&&(e.viewStep+=e.sendTicks)}async function Je({url:e,name:t,seconds:n,bot:i={},signal:a,aimZone:o,seed:l=0,link:d=ce}){let h={world:v(),character:null,player:null,controlled:[],level:0,moves:0,welcomeTick:0,sendTicks:1,lead:Ae(),refused:!1,viewStep:null,lastFeet:null,report:{name:t,character:null,startedAt:Date.now(),seconds:0,moves:0,shots:0,hits:0,damage:0,walkedMetres:0,messages:{},receivedBytes:0,receivedFrames:0,deltaBytes:{}}},{report:g}=h,_=new B(e),b=ie({readConditions:()=>d}),S=ie({readConditions:()=>d}),C=e=>{let t=y(e);b.hold(()=>{_.readyState===B.OPEN&&_.send(t)})},w=(e,t={})=>{C({type:r.Message,name:e,payload:t}),g.messages[e]=(g.messages[e]??0)+1},T=new z,D=ue({skill:i.skill,world:h.world,seed:l}),k=null,A=0,j=0,M=null,N=()=>h.sendTicks*x*1e3,P=()=>h.moves*N(),F=()=>h.welcomeTick+1+h.moves*h.sendTicks+h.lead.ticks,I=!1,L=new z,le=new z,V=-1/0,H=new z,U=0,W=0,G=0,K=(e,t,n)=>{if(I&&n-U>=Se){if(t.distanceTo(H)<xe)return W=n+Math.min(Ce*2**G++,we),!1;G=0,H.copy(t),U=n}if(n-V<be)return I;let[i,a,o]=e;return!I||L.distanceTo(le.set(i,a,o))>ye?(C({type:r.Walk,tick:F(),destination:[i,a,o]}),I||(H.copy(t),U=n),I=!0,L.set(i,a,o),V=n,!0):!0},q=()=>{if(M===null)return;let e=h.world.get(s),t=h.controlled.flatMap(t=>e?.get(t)??[]),n=t.find(e=>e.has(c))??null,a={},o=[],l={world:h.world,player:h.player!==null&&e?.get(h.player)||null,controlled:t,character:n,input:{set:(e,t)=>o.push([e,t])},send:w,moveSeconds:N()/1e3},u=n?.get(f)?.position;n&&u&&J({...l,character:n},u,a),i.play?.(l);for(let[e,t]of o)R(O(h.world),a,e,t);g.moves++;let d=He(a);C({type:r.Input,tick:F(),...d,next:Array.from({length:h.sendTicks-1},()=>d),...h.level>0&&{level:h.level}})},J=(e,t,n)=>{let{character:a}=e,s=P(),c=de(h.world,t),l=c.at(0),d=l&&Re(l.feet),f=i.destination?.(e),m=ze(i.dangers?.(e)??[],t);k=m?k??s:null;let ee=m!==void 0&&s-(k??s)>=(i.skill?.reactionSeconds??0)*1e3,_=[0,1],v=!1,y=!0;if(ee)d&&(A=X(t,d)),_=Be(t,m.at,A),v=!0;else if(f){let e=Math.hypot(f[0]-t.x,f[2]-t.z);d&&(A=X(t,d)),e<=Te?_=[0,0]:s>=W?y=!K(f,t,s):A=X(t,f)}else if(l&&d){if(A=X(t,d),l.distance<=ve){let e=Math.floor(s/1e3/Ee)%2==0,t=_e(D,{distance:l.distance,now:s});_=[e?-1:1,t?-1:0],v=!0}else s>=W&&(y=!K(d,t,s))}else A+=Oe;y?I=!1:_=[0,0],Ve(h.world,n,{intent:_,heading:A,strafe:v});let b=i.weapons?.find(({name:e})=>re(a,e));if(b&&h.viewStep!==null){let e=oe(b.settings,a,p.Range),n=fe(D,{from:t,targets:c,reach:e,now:s});if(n&&pe(D,{target:n,now:s}),n&&n.distance<=e&&s>=j&&me(D,s)){let e=t.clone().setY(t.y+ne.height/2);Le(h.world,Re(n.feet),o,T),he(D,T).sub(e).normalize(),ge(D,T),C({type:r.Shot,weapon:b.name,step:h.viewStep,origin:e.toArray(),direction:T.toArray(),...se(h.world,b.settings)===u.Page&&{hits:te(h.world,{shooter:a,origin:e,direction:T,weapon:b.settings})}}),g.shots++,j=s+1e3/oe(b.settings,a,p.ShotsPerSecond)}}};try{await new Promise((e,t)=>{_.once(`open`,()=>e()),_.once(`error`,t)}),C({type:r.Join,protocolVersion:E,name:t,wire:m.Binary});let e=()=>C({type:r.Ping,sent:performance.now()});await new Promise(t=>{let r=[],i=!1,o=()=>{if(!i){i=!0;for(let e of r)clearTimeout(e);a?.removeEventListener(`abort`,o),M!==null&&(g.seconds=(performance.now()-M)/1e3),t()}};a?.aborted&&o(),a?.addEventListener(`abort`,o);let s=(t,i)=>{let a=h.player!==null,s=h.moves;if(Ke(h,Me(t,i)),h.refused){g.closedWith=ee,_.close(),o();return}a&&h.moves>s&&q(),!(a||h.player===null)&&(M=performance.now(),g.startedAt=Date.now(),q(),e(),r=[setInterval(e,ae*1e3),setTimeout(o,n*1e3)])};_.on(`message`,(e,t)=>S.hold(()=>s(e,t))),_.once(`close`,e=>S.hold(()=>{i||(g.closedWith=e),o()}))})}finally{if(b.close(),S.close(),_.readyState===B.OPEN){let e=new Promise(e=>_.once(`close`,e));_.close(),await e}h.world.destroy()}return g}function Ye(e,t){let n=process.env[e],r=n===void 0?t:Number(n);if(!Number.isInteger(r)||r<1)throw Error(`${e} must be a whole number from 1, not ${n}.`);return r}function Xe(e,t){if(t===`none`)return{...e,skill:void 0};let n=JSON.parse(t),r={...e.skill,...n};for(let e of Ze)if(typeof r[e]!=`number`)throw Error(`BOT_SKILL gives the scene bot's skill no ${e}: give it as a number in the JSON, such as {"${e}":0.3}, or "none".`);return{...e,skill:r}}var Ze=[`reactionSeconds`,`trackingSeconds`,`spreadRadians`],Qe=process.env.ROOM_URL;if(!Qe)throw Error(`ROOM_URL names the room the bots play, such as ws://localhost:8788.`);var Z=Number(process.env.BOT_SECONDS??60);if(!(Number.isFinite(Z)&&Z>0))throw Error(`BOT_SECONDS must be a number of seconds over 0, not ${process.env.BOT_SECONDS}.`);var $e=Ye(`BOT_COUNT`,1),et=process.env.BOT_SEED??`0`,tt=Number(et);if(!Number.isInteger(tt))throw Error(`BOT_SEED must be a whole number, not ${et}.`);var nt=process.env.BOT_AIM_ZONE||void 0,rt=process.env.ROOM_GAME,Q=rt===void 0?void 0:await e(rt),it=process.env.BOT_SKILL,at=Q&&it?Xe(Q,it):Q,ot=process.env.BOT_JOIN_FILE,st=le({latency:process.env.BOT_LATENCY,jitter:process.env.BOT_JITTER,stall:process.env.BOT_STALL});if(ot)for(;!t(ot);)await V(50);var $=new AbortController;process.on(`message`,e=>{e===`stop`&&$.abort()});var ct=await Promise.all(Array.from({length:$e},(e,t)=>Je({url:Qe,name:`Bot ${t+1}`,seconds:Z,bot:at,signal:$.signal,aimZone:nt,seed:H(tt,t),link:st})));for(let e of ct)console.log(JSON.stringify({kind:`bot`,...e}));process.exit(0);export{};
@@ -0,0 +1,2 @@
1
+ import{findRoomSettingsProblems as e}from"@spawnite/schema";function t(t){let n=e=>{let n=t[e];return n===void 0||n===``?void 0:Number(n)},r={maxPlayers:n(`MAX_PLAYERS`),sendRate:n(`SEND_RATE`)},i=e(r,`The room's environment, MAX_PLAYERS and SEND_RATE,`);if(i.length>0)throw Error(i.join(`
2
+ `));return{...r.maxPlayers!==void 0&&{maxPlayers:r.maxPlayers},...r.sendRate!==void 0&&{sendRate:r.sendRate}}}export{t};
@@ -0,0 +1 @@
1
+ import{readFileSync as e}from"node:fs";import{join as t}from"node:path";import{packageRoomHint as n,readPackageRoom as r}from"@spawnite/schema";var i=t(process.cwd(),`package.json`),a=r(JSON.parse(e(i,`utf8`)));if(a===void 0)throw Error(`${i} names no room to run. ${n}`);Object.assign(process.env,a.env,{ROOM_GAME:a.scene}),await import(`./main.js`);export{};
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{A as e,B as t,C as n,D as r,E as i,F as a,H as o,I as s,L as c,M as l,O as u,R as d,S as f,T as p,U as m,V as h,_ as g,a as _,b as v,c as y,d as b,f as x,g as S,h as C,i as w,j as T,k as E,l as D,m as O,n as k,o as A,p as j,r as M,s as N,t as P,u as F,v as I,w as L,x as R,y as z,z as B}from"./BJWBohZM.js";import{t as V}from"./d4V_PUVA.js";export{t as LogKind,h as MessageDropReason,A as RoomCloseCode,a as SaveCommitOutcome,s as SaveLoadStatus,N as askedHeartbeatSeconds,y as brokenHoldSeconds,u as checkpointSeconds,E as checkpointUploads,e as checkpointsPerInterval,D as closedHoldSeconds,P as continueSceneRoom,c as createCredit,F as defaultMaxQueuedBytes,T as firstRetryMilliseconds,b as inputAheadTicks,x as inputMessageBurst,j as inputMessagesPerTick,O as inputRepeatTicks,B as isLoopback,C as itemIntentBurst,S as itemIntentsPerSecond,g as joinSeconds,k as loadSceneBot,o as logLine,I as loggedDropsPerHeartbeat,l as longestRetryMilliseconds,z as maxHeldMessages,M as mountSceneWorld,w as openSceneServer,v as queueOverSeconds,V as readRoomSettingsEnv,R as refusalLeaveTries,f as refusalLeaveWaitMilliseconds,n as silenceSeconds,L as socketPingSeconds,d as spendCredit,p as startRoom,_ as startSceneRoom,m as startStageTimer,i as stopSaveSeconds,r as unjoinedPerSeat};
package/dist/main.js ADDED
@@ -0,0 +1 @@
1
+ import{B as e,H as t,N as n,P as r,T as i,W as a,a as o,t as s}from"./BJWBohZM.js";import{t as c}from"./d4V_PUVA.js";import{mkdirSync as l,readFileSync as u}from"node:fs";import{tmpdir as d}from"node:os";import{basename as f,dirname as p,extname as ee,join as m}from"node:path";import{CharacterCapsuleTrait as te,CharacterTrait as h,ChaseTrait as g,HealthTrait as _,ModelTrait as v,PickupTrait as y,behaviours as b,characters as x,chaserCollisionMask as ne,replaysFolderName as S,requireAuthority as C,spawn as w}from"@spawnite/engine/internal/room";import{Vector3 as T}from"three";import{writeHeapSnapshot as E}from"node:v8";var D=6,O=8,k=8,re=6,ie=[x(),b()],ae=e=>{for(let t=0;t<D;t++){let n=t/D*Math.PI*2;w(C(e),{position:new T(Math.cos(n)*O,0,Math.sin(n)*O),traits:[g({speed:4,reach:1}),te({collisionMask:ne}),h,v({name:`monster`}),_]})}for(let t=0;t<k;t++){let n=t*2.4,r=re*(t+1)/k;w(C(e),{position:new T(Math.cos(n)*r,0,Math.sin(n)*r),traits:[y({reward:1}),v({name:`coin`})]})}};function A(e){if(e===void 0)return;if(e===`exit`)return{exit:!0};let t=Number(e);if(e.trim()===``||!(t>0&&t<1))throw Error(`ROOM_HEAP_SNAPSHOT must be exit, or a share of the heap limit over 0 and under 1, such as 0.6, not ${e}.`);return{share:t}}function j(e,{folder:t,name:n,onWritten:r,write:i=e=>(l(p(e),{recursive:!0}),E(e))}){let o=!1,s=e=>{o||(o=!0,r({file:i(m(t,`${n}-${e}-${Date.now()}.heapsnapshot`)),reason:e,heapMegabytes:c.read()}))},c=a(()=>{`share`in e&&s(`share`)},`share`in e?e.share:1);return{atExit:()=>{`exit`in e&&s(`exit`)},stop:c.stop}}var M=Number(process.env.PORT??8787);if(!Number.isInteger(M)||M<0)throw Error(`PORT must be a port number, not ${process.env.PORT}.`);var{maxPlayers:N,sendRate:P}=c(process.env),F=process.env.HOST,I=process.env.ROOM_EDITS;if(I!==void 0&&I!==`1`&&I!==`0`)throw Error(`ROOM_EDITS must be 1 or 0, not ${I}.`);var L=process.env.ROOM_INVULNERABLE;if(L!==void 0&&L!==`1`&&L!==`0`)throw Error(`ROOM_INVULNERABLE must be 1 or 0, not ${L}.`);var R=process.env.ROOM_DIRECTOR;if(R!==void 0&&R!==`1`&&R!==`0`)throw Error(`ROOM_DIRECTOR must be 1 or 0, not ${R}.`);var z=process.env.ROOM_ID??`local`,B=process.env.ROOM_REPLAY;if(B!==void 0&&B!==`1`&&B!==`0`)throw Error(`ROOM_REPLAY must be 1 or 0, not ${B}.`);var V=process.env.ROOM_HEARTBEAT_SECONDS,H=V===void 0?void 0:Number(V);if(H!==void 0&&!(Number.isFinite(H)&&H>0))throw Error(`ROOM_HEARTBEAT_SECONDS must be a number of seconds over 0, not ${V}.`);var U=process.env.ROOM_SEED,W=U===void 0?void 0:Number(U);if(W!==void 0&&!(Number.isInteger(W)&&W>=0))throw Error(`ROOM_SEED must be a whole number from 0, not ${U}.`);var G=process.env.ROOM_WATCH;if(G!==void 0&&G!==`1`&&G!==`0`)throw Error(`ROOM_WATCH must be 1 or 0, not ${G}.`);var K=process.env.ROOM_CONTINUE,q=process.env.ROOM_SAVES;if(q!==void 0&&q!==`1`&&q!==`0`)throw Error(`ROOM_SAVES must be 1 or 0, not ${q}.`);var J=process.env.ROOM_START_SAVE,Y=process.env.ROOM_GAME,X=A(process.env.ROOM_HEAP_SNAPSHOT),Z={port:M,host:F,roomId:z,allowEdits:I===`1`,invulnerable:L===`1`,director:R===`1`,maxPlayers:N,sendRate:P,heartbeatSeconds:H,seed:W,record:B===`1`&&!K?{folder:m(process.cwd(),S),game:Y??`demo`}:void 0,saves:!Y||K?void 0:J?r(J):q===`0`?void 0:n(m(process.cwd(),`.spawnite`,`saves`)),onFailure:()=>process.exit(1)};if(K){if(!Y)throw Error(`ROOM_CONTINUE continues a game's room: set ROOM_GAME to its scene.`);let e=JSON.parse(u(K,`utf8`));await s(Y,{...Z,log:()=>void 0},e),process.exit(0)}var oe=Math.round(performance.now()),Q=Y?await o(Y,{...Z,watch:G!==`0`}):await i({...Z,scene:ae,plugins:ie});t({kind:e.Started,room:z,port:Q.port,game:Y??`demo`,...Q.run&&{run:Q.run},heartbeatSeconds:Q.heartbeatSeconds,maxPlayers:Q.maxPlayers,sendRate:Q.sendRate,...Q.timeline&&{timeline:Q.timeline},...Q.build!==void 0&&{build:Q.build},stageMilliseconds:{loaded:oe,...Q.stageMilliseconds}});var se=X&&j(X,{folder:process.env.ROOM_HEAP_SNAPSHOT_FOLDER??d(),name:Y?f(Y,ee(Y)):`demo`,onWritten:({file:n,reason:r,heapMegabytes:i})=>t({kind:e.HeapSnapshot,room:z,file:n,reason:r,heapMegabytes:i})});async function $(){se?.atExit(),await Q.close(),process.exit(0)}process.once(`SIGTERM`,()=>void $()),process.once(`SIGINT`,()=>void $()),process.on(`message`,e=>{e===`stop`&&$()});export{};
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@spawnite/room",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "The development room: one game's multiplayer server on your own machine, stepping its world and streaming it to each page over WebSocket, as the platform's rooms play it.",
4
+ "version": "0.1.0-beta.17",
5
+ "license": "SEE LICENSE IN LICENSE.md",
6
+ "homepage": "https://wiki.spawnite.com",
7
+ "publishConfig": {
8
+ "registry": "https://registry.npmjs.org",
9
+ "access": "public"
10
+ },
11
+ "engines": {
12
+ "node": ">=24.2"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "LICENSE.md",
17
+ "!dist/*.tsbuildinfo",
18
+ "!dist/**/*.map",
19
+ "!dist/**/*.d.ts",
20
+ "!dist/bot.d.ts",
21
+ "!dist/bots.d.ts",
22
+ "!dist/budget.d.ts",
23
+ "!dist/checkpoints.d.ts",
24
+ "!dist/continue.d.ts",
25
+ "!dist/credit.d.ts",
26
+ "!dist/demo.d.ts",
27
+ "!dist/director.d.ts",
28
+ "!dist/faults.d.ts",
29
+ "!dist/fromPackage.d.ts",
30
+ "!dist/growth.d.ts",
31
+ "!dist/heap.d.ts",
32
+ "!dist/heapSnapshot.d.ts",
33
+ "!dist/hosting.d.ts",
34
+ "!dist/index.d.ts",
35
+ "!dist/log.d.ts",
36
+ "!dist/main.d.ts",
37
+ "!dist/messages.d.ts",
38
+ "!dist/presses.d.ts",
39
+ "!dist/recording.d.ts",
40
+ "!dist/room.d.ts",
41
+ "!dist/roomSettings.d.ts",
42
+ "!dist/saves.d.ts",
43
+ "!dist/scene.d.ts",
44
+ "!dist/shots.d.ts",
45
+ "!dist/skill.d.ts"
46
+ ],
47
+ "type": "module",
48
+ "exports": {
49
+ "./package.json": "./package.json",
50
+ ".": {
51
+ "import": "./dist/index.js",
52
+ "default": "./dist/index.js"
53
+ }
54
+ },
55
+ "dependencies": {
56
+ "@pixiv/three-vrm": "3.5.5",
57
+ "@react-three/drei": "10.7.8",
58
+ "@react-three/fiber": "9.8.0",
59
+ "koota": "0.6.6",
60
+ "react": "19.3.0",
61
+ "react-dom": "19.3.0",
62
+ "three": "0.186.0",
63
+ "ws": "8.21.3",
64
+ "zustand": "5.0.15",
65
+ "@spawnite/schema": "0.1.0-beta.17",
66
+ "@spawnite/engine": "0.1.0-beta.17",
67
+ "@spawnite/ui": "0.1.0-beta.17"
68
+ }
6
69
  }