spoint 0.1.0 → 0.1.11

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.
Files changed (204) hide show
  1. package/README.md +134 -209
  2. package/SKILL.md +95 -0
  3. package/apps/environment/index.js +200 -1
  4. package/apps/environment/models/decorative/.gitkeep +0 -0
  5. package/apps/environment/models/hazards/.gitkeep +0 -0
  6. package/apps/environment/models/interactive/.gitkeep +0 -0
  7. package/apps/environment/models/structures/.gitkeep +0 -0
  8. package/apps/environment/smartObjects.js +114 -0
  9. package/apps/interactable/index.js +155 -0
  10. package/apps/physics-crate/index.js +15 -9
  11. package/apps/power-crate/index.js +18 -12
  12. package/apps/tps-game/$GDUPI.vrm +0 -0
  13. package/apps/tps-game/Cleetus.vrm +0 -0
  14. package/apps/tps-game/index.js +185 -27
  15. package/apps/world/index.js +68 -22
  16. package/bin/create-app.js +337 -0
  17. package/client/ARControls.js +301 -0
  18. package/client/LoadingManager.js +117 -0
  19. package/client/MobileControls.js +1122 -0
  20. package/client/anim-lib.glb +0 -0
  21. package/client/animation.js +306 -0
  22. package/client/app.js +1341 -65
  23. package/client/camera.js +191 -33
  24. package/client/createLoadingScreen.js +69 -0
  25. package/client/editor/bridge.js +113 -0
  26. package/client/editor/css/main.css +794 -0
  27. package/client/editor/images/rotate.svg +4 -0
  28. package/client/editor/images/scale.svg +4 -0
  29. package/client/editor/images/translate.svg +4 -0
  30. package/client/editor/index.html +103 -0
  31. package/client/editor/js/Command.js +41 -0
  32. package/client/editor/js/Config.js +81 -0
  33. package/client/editor/js/Editor.js +785 -0
  34. package/client/editor/js/EditorControls.js +438 -0
  35. package/client/editor/js/History.js +321 -0
  36. package/client/editor/js/Loader.js +987 -0
  37. package/client/editor/js/LoaderUtils.js +90 -0
  38. package/client/editor/js/Menubar.Add.js +510 -0
  39. package/client/editor/js/Menubar.Edit.js +145 -0
  40. package/client/editor/js/Menubar.File.js +466 -0
  41. package/client/editor/js/Menubar.Help.js +73 -0
  42. package/client/editor/js/Menubar.Status.js +51 -0
  43. package/client/editor/js/Menubar.View.js +183 -0
  44. package/client/editor/js/Menubar.js +27 -0
  45. package/client/editor/js/Player.js +53 -0
  46. package/client/editor/js/Resizer.js +58 -0
  47. package/client/editor/js/Script.js +503 -0
  48. package/client/editor/js/Selector.js +102 -0
  49. package/client/editor/js/Sidebar.Geometry.BoxGeometry.js +121 -0
  50. package/client/editor/js/Sidebar.Geometry.BufferGeometry.js +115 -0
  51. package/client/editor/js/Sidebar.Geometry.CapsuleGeometry.js +97 -0
  52. package/client/editor/js/Sidebar.Geometry.CircleGeometry.js +97 -0
  53. package/client/editor/js/Sidebar.Geometry.CylinderGeometry.js +121 -0
  54. package/client/editor/js/Sidebar.Geometry.DodecahedronGeometry.js +73 -0
  55. package/client/editor/js/Sidebar.Geometry.ExtrudeGeometry.js +196 -0
  56. package/client/editor/js/Sidebar.Geometry.IcosahedronGeometry.js +73 -0
  57. package/client/editor/js/Sidebar.Geometry.LatheGeometry.js +98 -0
  58. package/client/editor/js/Sidebar.Geometry.Modifiers.js +73 -0
  59. package/client/editor/js/Sidebar.Geometry.OctahedronGeometry.js +74 -0
  60. package/client/editor/js/Sidebar.Geometry.PlaneGeometry.js +97 -0
  61. package/client/editor/js/Sidebar.Geometry.RingGeometry.js +121 -0
  62. package/client/editor/js/Sidebar.Geometry.ShapeGeometry.js +76 -0
  63. package/client/editor/js/Sidebar.Geometry.SphereGeometry.js +133 -0
  64. package/client/editor/js/Sidebar.Geometry.TetrahedronGeometry.js +74 -0
  65. package/client/editor/js/Sidebar.Geometry.TorusGeometry.js +109 -0
  66. package/client/editor/js/Sidebar.Geometry.TorusKnotGeometry.js +121 -0
  67. package/client/editor/js/Sidebar.Geometry.TubeGeometry.js +135 -0
  68. package/client/editor/js/Sidebar.Geometry.js +332 -0
  69. package/client/editor/js/Sidebar.Material.BooleanProperty.js +60 -0
  70. package/client/editor/js/Sidebar.Material.ColorProperty.js +87 -0
  71. package/client/editor/js/Sidebar.Material.ConstantProperty.js +62 -0
  72. package/client/editor/js/Sidebar.Material.MapProperty.js +249 -0
  73. package/client/editor/js/Sidebar.Material.NumberProperty.js +60 -0
  74. package/client/editor/js/Sidebar.Material.Program.js +73 -0
  75. package/client/editor/js/Sidebar.Material.RangeValueProperty.js +63 -0
  76. package/client/editor/js/Sidebar.Material.js +751 -0
  77. package/client/editor/js/Sidebar.Object.Animation.js +102 -0
  78. package/client/editor/js/Sidebar.Object.js +898 -0
  79. package/client/editor/js/Sidebar.Project.App.js +165 -0
  80. package/client/editor/js/Sidebar.Project.Image.js +225 -0
  81. package/client/editor/js/Sidebar.Project.Materials.js +82 -0
  82. package/client/editor/js/Sidebar.Project.Renderer.js +144 -0
  83. package/client/editor/js/Sidebar.Project.Video.js +242 -0
  84. package/client/editor/js/Sidebar.Project.js +31 -0
  85. package/client/editor/js/Sidebar.Properties.js +73 -0
  86. package/client/editor/js/Sidebar.Scene.js +585 -0
  87. package/client/editor/js/Sidebar.Script.js +129 -0
  88. package/client/editor/js/Sidebar.Settings.History.js +146 -0
  89. package/client/editor/js/Sidebar.Settings.Shortcuts.js +175 -0
  90. package/client/editor/js/Sidebar.Settings.js +60 -0
  91. package/client/editor/js/Sidebar.js +41 -0
  92. package/client/editor/js/Storage.js +98 -0
  93. package/client/editor/js/Strings.js +2028 -0
  94. package/client/editor/js/Toolbar.js +84 -0
  95. package/client/editor/js/Viewport.Controls.js +92 -0
  96. package/client/editor/js/Viewport.Info.js +136 -0
  97. package/client/editor/js/Viewport.Pathtracer.js +91 -0
  98. package/client/editor/js/Viewport.ViewHelper.js +39 -0
  99. package/client/editor/js/Viewport.XR.js +222 -0
  100. package/client/editor/js/Viewport.js +900 -0
  101. package/client/editor/js/commands/AddObjectCommand.js +68 -0
  102. package/client/editor/js/commands/AddScriptCommand.js +75 -0
  103. package/client/editor/js/commands/Commands.js +23 -0
  104. package/client/editor/js/commands/MoveObjectCommand.js +111 -0
  105. package/client/editor/js/commands/MultiCmdsCommand.js +85 -0
  106. package/client/editor/js/commands/RemoveObjectCommand.js +88 -0
  107. package/client/editor/js/commands/RemoveScriptCommand.js +81 -0
  108. package/client/editor/js/commands/SetColorCommand.js +73 -0
  109. package/client/editor/js/commands/SetGeometryCommand.js +87 -0
  110. package/client/editor/js/commands/SetGeometryValueCommand.js +70 -0
  111. package/client/editor/js/commands/SetMaterialColorCommand.js +86 -0
  112. package/client/editor/js/commands/SetMaterialCommand.js +79 -0
  113. package/client/editor/js/commands/SetMaterialMapCommand.js +143 -0
  114. package/client/editor/js/commands/SetMaterialRangeCommand.js +91 -0
  115. package/client/editor/js/commands/SetMaterialValueCommand.js +90 -0
  116. package/client/editor/js/commands/SetMaterialVectorCommand.js +79 -0
  117. package/client/editor/js/commands/SetPositionCommand.js +84 -0
  118. package/client/editor/js/commands/SetRotationCommand.js +84 -0
  119. package/client/editor/js/commands/SetScaleCommand.js +84 -0
  120. package/client/editor/js/commands/SetSceneCommand.js +103 -0
  121. package/client/editor/js/commands/SetScriptValueCommand.js +80 -0
  122. package/client/editor/js/commands/SetShadowValueCommand.js +73 -0
  123. package/client/editor/js/commands/SetUuidCommand.js +70 -0
  124. package/client/editor/js/commands/SetValueCommand.js +75 -0
  125. package/client/editor/js/libs/acorn/acorn.js +3236 -0
  126. package/client/editor/js/libs/acorn/acorn_loose.js +1299 -0
  127. package/client/editor/js/libs/acorn/walk.js +344 -0
  128. package/client/editor/js/libs/app/index.html +57 -0
  129. package/client/editor/js/libs/app.js +251 -0
  130. package/client/editor/js/libs/codemirror/addon/dialog.css +32 -0
  131. package/client/editor/js/libs/codemirror/addon/dialog.js +163 -0
  132. package/client/editor/js/libs/codemirror/addon/show-hint.css +36 -0
  133. package/client/editor/js/libs/codemirror/addon/show-hint.js +529 -0
  134. package/client/editor/js/libs/codemirror/addon/tern.css +87 -0
  135. package/client/editor/js/libs/codemirror/addon/tern.js +750 -0
  136. package/client/editor/js/libs/codemirror/codemirror.css +344 -0
  137. package/client/editor/js/libs/codemirror/codemirror.js +9849 -0
  138. package/client/editor/js/libs/codemirror/mode/glsl.js +233 -0
  139. package/client/editor/js/libs/codemirror/mode/javascript.js +959 -0
  140. package/client/editor/js/libs/codemirror/theme/monokai.css +41 -0
  141. package/client/editor/js/libs/esprima.js +6401 -0
  142. package/client/editor/js/libs/jsonlint.js +453 -0
  143. package/client/editor/js/libs/signals.min.js +14 -0
  144. package/client/editor/js/libs/tern-threejs/threejs.js +5031 -0
  145. package/client/editor/js/libs/ternjs/comment.js +87 -0
  146. package/client/editor/js/libs/ternjs/def.js +588 -0
  147. package/client/editor/js/libs/ternjs/doc_comment.js +401 -0
  148. package/client/editor/js/libs/ternjs/infer.js +1635 -0
  149. package/client/editor/js/libs/ternjs/polyfill.js +80 -0
  150. package/client/editor/js/libs/ternjs/signal.js +26 -0
  151. package/client/editor/js/libs/ternjs/tern.js +993 -0
  152. package/client/editor/js/libs/ui.js +1346 -0
  153. package/client/editor/js/libs/ui.three.js +855 -0
  154. package/client/facial-animation.js +455 -0
  155. package/client/index.html +7 -4
  156. package/client/loading.css +147 -0
  157. package/client/loading.html +25 -0
  158. package/client/style.css +251 -0
  159. package/package.json +7 -3
  160. package/server.js +9 -1
  161. package/src/apps/AppContext.js +1 -1
  162. package/src/apps/AppLoader.js +50 -37
  163. package/src/apps/AppRuntime.js +32 -8
  164. package/src/client/InputHandler.js +233 -0
  165. package/src/client/JitterBuffer.js +207 -0
  166. package/src/client/KalmanFilter.js +125 -0
  167. package/src/client/MessageHandler.js +101 -0
  168. package/src/client/PhysicsNetworkClient.js +141 -68
  169. package/src/client/ReconnectManager.js +62 -0
  170. package/src/client/SmoothInterpolation.js +127 -0
  171. package/src/client/SnapshotProcessor.js +144 -0
  172. package/src/connection/ConnectionManager.js +21 -3
  173. package/src/connection/SessionStore.js +13 -3
  174. package/src/index.client.js +4 -6
  175. package/src/netcode/EventLog.js +29 -15
  176. package/src/netcode/LagCompensator.js +25 -26
  177. package/src/netcode/NetworkState.js +4 -1
  178. package/src/netcode/PhysicsIntegration.js +20 -6
  179. package/src/netcode/PlayerManager.js +10 -2
  180. package/src/netcode/SnapshotEncoder.js +66 -19
  181. package/src/netcode/TickSystem.js +13 -4
  182. package/src/physics/World.js +66 -13
  183. package/src/protocol/msgpack.js +90 -63
  184. package/src/sdk/ReloadHandlers.js +12 -2
  185. package/src/sdk/ReloadManager.js +5 -0
  186. package/src/sdk/ServerHandlers.js +50 -11
  187. package/src/sdk/StaticHandler.js +22 -6
  188. package/src/sdk/TickHandler.js +101 -34
  189. package/src/sdk/scaffold.js +31 -0
  190. package/src/sdk/server.js +59 -33
  191. package/src/shared/movement.js +2 -1
  192. package/src/spatial/Octree.js +5 -0
  193. package/apps/interactive-door/index.js +0 -33
  194. package/apps/patrol-npc/index.js +0 -37
  195. package/src/connection/QualityMonitor.js +0 -46
  196. package/src/debug/StateInspector.js +0 -42
  197. package/src/index.js +0 -1
  198. package/src/index.server.js +0 -27
  199. package/src/protocol/Codec.js +0 -60
  200. package/src/protocol/SequenceTracker.js +0 -71
  201. package/src/sdk/ClientMessageHandler.js +0 -80
  202. package/src/sdk/client.js +0 -122
  203. package/world/kaira.glb +0 -0
  204. package/world/schwust.glb +0 -0
@@ -1,20 +1,21 @@
1
- import { PredictionEngine } from './PredictionEngine.js'
2
1
  import { pack, unpack } from '../protocol/msgpack.js'
3
2
  import { MSG } from '../protocol/MessageTypes.js'
3
+ import { ReconnectManager } from './ReconnectManager.js'
4
+ import { SnapshotProcessor } from './SnapshotProcessor.js'
5
+ import { MessageHandler } from './MessageHandler.js'
4
6
 
5
7
  export class PhysicsNetworkClient {
6
8
  constructor(config = {}) {
7
- this.config = { url: config.url || 'ws://localhost:8080/ws', tickRate: config.tickRate || 128, predictionEnabled: config.predictionEnabled !== false, debug: config.debug || false, ...config }
9
+ this.config = { url: config.url || 'ws://localhost:3000/ws', tickRate: config.tickRate || 128, predictionEnabled: config.predictionEnabled !== false, smoothInterpolation: config.smoothInterpolation !== false, debug: config.debug || false, ...config }
8
10
  this.ws = null
9
- this.playerId = null
10
11
  this.connected = false
11
- this._predEngine = null
12
- this._playerStates = new Map()
13
- this._entityStates = new Map()
12
+ this.state = { players: [], entities: [] }
14
13
  this.lastSnapshotTick = 0
15
14
  this.currentTick = 0
16
- this.state = { players: [], entities: [] }
15
+ this._pingSent = 0
17
16
  this.heartbeatTimer = null
17
+ this._destroyed = false
18
+ this._visibilityListener = null
18
19
  this.callbacks = {
19
20
  onConnect: config.onConnect || (() => {}),
20
21
  onDisconnect: config.onDisconnect || (() => {}),
@@ -28,41 +29,63 @@ export class PhysicsNetworkClient {
28
29
  onWorldDef: config.onWorldDef || (() => {}),
29
30
  onAppModule: config.onAppModule || (() => {}),
30
31
  onAssetUpdate: config.onAssetUpdate || (() => {}),
31
- onAppEvent: config.onAppEvent || (() => {})
32
+ onAppEvent: config.onAppEvent || (() => {}),
33
+ onHotReload: config.onHotReload || (() => {})
32
34
  }
35
+ this._reconnect = new ReconnectManager(config)
36
+ this._snapProc = new SnapshotProcessor({ callbacks: this.callbacks })
37
+ this._msgHandler = new MessageHandler({ ...config, callbacks: this.callbacks })
33
38
  }
34
39
 
40
+ get playerId() { return this._msgHandler.getPlayerId() }
41
+
35
42
  async connect() {
36
- return new Promise((resolve, reject) => {
43
+ return new Promise((resolve) => {
44
+ let settled = false
37
45
  try {
38
46
  this.ws = new WebSocket(this.config.url)
39
47
  this.ws.binaryType = 'arraybuffer'
40
- this.ws.onopen = () => this._onOpen(resolve)
48
+ this.ws.onopen = () => { settled = true; this._onOpen(resolve) }
41
49
  this.ws.onmessage = (event) => this.onMessage(event.data)
42
50
  this.ws.onclose = () => this._onClose()
43
- this.ws.onerror = (error) => this._onError(error, reject)
44
- } catch (error) { reject(error) }
51
+ this.ws.onerror = () => { if (!settled) { settled = true; resolve() } }
52
+ } catch (e) { resolve() }
45
53
  })
46
54
  }
47
55
 
48
56
  _onOpen(resolve) {
49
57
  this.connected = true
50
58
  this._startHeartbeat()
59
+ this._reconnect.sendReconnectMessage(this.ws)
60
+ this._reconnect.onConnected()
51
61
  this.callbacks.onConnect()
52
- resolve()
62
+ resolve?.()
53
63
  }
54
64
 
55
65
  _onClose() {
56
66
  this.connected = false
57
67
  this._stopHeartbeat()
58
68
  this.callbacks.onDisconnect()
69
+ this._reconnect.onDisconnected(() => this._doReconnect())
59
70
  }
60
71
 
61
- _onError(error, reject) { reject(error) }
72
+ _doReconnect() {
73
+ try {
74
+ this.ws = new WebSocket(this.config.url)
75
+ this.ws.binaryType = 'arraybuffer'
76
+ this.ws.onopen = () => this._onOpen(null)
77
+ this.ws.onmessage = (event) => this.onMessage(event.data)
78
+ this.ws.onclose = () => this._onClose()
79
+ this.ws.onerror = () => {}
80
+ } catch (e) {
81
+ this._reconnect.onDisconnected(() => this._doReconnect())
82
+ }
83
+ }
62
84
 
63
85
  sendInput(input) {
64
86
  if (!this._isOpen()) return
65
- if (this.config.predictionEnabled && this._predEngine) this._predEngine.addInput(input)
87
+ const predEngine = this._msgHandler.getPredEngine()
88
+ if (this.config.predictionEnabled && predEngine) predEngine.addInput(input)
66
89
  this.ws.send(pack({ type: MSG.INPUT, payload: { input } }))
67
90
  }
68
91
 
@@ -71,89 +94,120 @@ export class PhysicsNetworkClient {
71
94
  this.ws.send(pack({ type: MSG.APP_EVENT, payload: { type: 'fire', shooterId: this.playerId, ...data } }))
72
95
  }
73
96
 
74
- _isOpen() { return this.ws && this.ws.readyState === WebSocket.OPEN }
97
+ sendReload() {
98
+ if (!this._isOpen()) return
99
+ this.ws.send(pack({ type: MSG.APP_EVENT, payload: { type: 'reload', playerId: this.playerId } }))
100
+ }
101
+
102
+ _isOpen() {
103
+ return this.ws && this.ws.readyState === WebSocket.OPEN
104
+ }
75
105
 
76
106
  onMessage(data) {
77
107
  try {
78
108
  const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data
79
109
  const msg = unpack(bytes)
80
110
  this._handleMessage(msg.type, msg.payload || {})
81
- } catch (e) { console.error('[client] parse error:', e) }
111
+ } catch (e) {
112
+ console.error('[client] parse error:', e)
113
+ }
82
114
  }
83
115
 
84
116
  _handleMessage(type, payload) {
85
- if (type === MSG.HANDSHAKE_ACK) {
86
- this.playerId = payload.playerId
87
- this.currentTick = payload.tick
88
- this._predEngine = new PredictionEngine(this.config.tickRate)
89
- this._predEngine.init(this.playerId)
90
- } else if (type === MSG.SNAPSHOT || type === MSG.STATE_CORRECTION) {
91
- this._onSnapshot(payload)
92
- } else if (type === MSG.PLAYER_LEAVE) {
93
- this._playerStates.delete(payload.playerId)
94
- this.callbacks.onPlayerLeft(payload.playerId)
95
- } else if (type === MSG.WORLD_DEF) {
96
- if (payload.movement && this._predEngine) this._predEngine.setMovement(payload.movement)
97
- if (payload.gravity && this._predEngine) this._predEngine.setGravity(payload.gravity)
98
- this.callbacks.onWorldDef?.(payload)
99
- } else if (type === MSG.APP_EVENT) {
100
- this.callbacks.onAppEvent?.(payload)
101
- } else if (type === MSG.HOT_RELOAD || type === MSG.APP_MODULE || type === MSG.ASSET_UPDATE) {
102
- const cb = { [MSG.HOT_RELOAD]: 'onHotReload', [MSG.APP_MODULE]: 'onAppModule', [MSG.ASSET_UPDATE]: 'onAssetUpdate' }[type]
103
- this.callbacks[cb]?.(payload)
117
+ const result = this._msgHandler.handleMessage(type, payload, this._snapProc)
118
+ if (type === MSG.HANDSHAKE_ACK && result?.sessionToken) {
119
+ this._reconnect.setSessionToken(result.sessionToken)
120
+ } else if (type === MSG.RECONNECT_ACK && result?.sessionToken) {
121
+ this._reconnect.setSessionToken(result.sessionToken)
122
+ } else if (result?.invalidate) {
123
+ this._reconnect.invalidateSession()
124
+ } else if (result && (type === MSG.SNAPSHOT || type === MSG.STATE_CORRECTION || type === MSG.STATE_RECOVERY)) {
125
+ this._onSnapshot(result)
104
126
  }
105
127
  }
106
128
 
107
129
  _onSnapshot(data) {
108
130
  this.lastSnapshotTick = this.currentTick = data.tick || 0
109
- for (const p of data.players || []) {
110
- const { playerId, state } = this._parsePlayer(p)
111
- if (!this._playerStates.has(playerId)) this.callbacks.onPlayerJoined(playerId, state)
112
- this._playerStates.set(playerId, state)
113
- if (playerId === this.playerId && this.config.predictionEnabled && this._predEngine) {
114
- this._predEngine.onServerSnapshot({ players: [state] }, this.currentTick)
115
- }
131
+ const snapshotForBuffer = this._snapProc.processSnapshot(data, this.currentTick)
132
+ const smoothInterp = this._msgHandler.getSmoothInterp()
133
+ if (smoothInterp) {
134
+ smoothInterp.addSnapshot(snapshotForBuffer)
116
135
  }
117
- for (const e of data.entities || []) {
118
- const { entityId, state } = this._parseEntity(e)
119
- if (!this._entityStates.has(entityId)) this.callbacks.onEntityAdded(entityId, state)
120
- this._entityStates.set(entityId, state)
136
+ const predEngine = this._msgHandler.getPredEngine()
137
+ if (this.playerId && this.config.predictionEnabled && predEngine) {
138
+ const localState = this._snapProc.getPlayerState(this.playerId)
139
+ if (localState) {
140
+ predEngine.onServerSnapshot({ players: [localState] }, this.currentTick)
141
+ }
121
142
  }
122
- this.state.players = Array.from(this._playerStates.values())
123
- this.state.entities = Array.from(this._entityStates.values())
143
+ this.state.players = Array.from(this._snapProc.getAllPlayerStates().values())
144
+ this.state.entities = Array.from(this._snapProc.getAllEntities().values())
124
145
  this.callbacks.onSnapshot(data)
125
146
  this.callbacks.onStateUpdate(this.state)
126
147
  this._render()
127
148
  }
128
149
 
129
- _parsePlayer(p) {
130
- if (Array.isArray(p)) return { playerId: p[0], state: { id: p[0], position: [p[1], p[2], p[3]], rotation: [p[4], p[5], p[6], p[7]], velocity: [p[8], p[9], p[10]], onGround: p[11] === 1, health: p[12], inputSequence: p[13] } }
131
- return { playerId: p.id || p.i, state: { id: p.id || p.i, position: p.position || [0, 0, 0], rotation: p.rotation || [0, 0, 0, 1], velocity: p.velocity || [0, 0, 0], onGround: p.onGround ?? false, health: p.health ?? 100 } }
132
- }
133
-
134
- _parseEntity(e) {
135
- if (Array.isArray(e)) return { entityId: e[0], state: { id: e[0], model: e[1], position: [e[2], e[3], e[4]], rotation: [e[5], e[6], e[7], e[8]], bodyType: e[9], custom: e[10] } }
136
- return { entityId: e.id, state: { id: e.id, model: e.model, position: e.position || [0, 0, 0], rotation: e.rotation || [0, 0, 0, 1], bodyType: e.bodyType || 'static', custom: e.custom || null } }
137
- }
138
-
139
150
  _render() {
140
151
  const displayStates = new Map()
141
- for (const [playerId, serverState] of this._playerStates) {
142
- displayStates.set(playerId, playerId === this.playerId && this.config.predictionEnabled && this._predEngine ? this._predEngine.getDisplayState(this.currentTick, 0) : serverState)
152
+ const smoothInterp = this._msgHandler.getSmoothInterp()
153
+ if (smoothInterp) {
154
+ const smoothState = smoothInterp.getDisplayState()
155
+ for (const p of smoothState.players) {
156
+ displayStates.set(p.id, p)
157
+ }
158
+ } else {
159
+ const predEngine = this._msgHandler.getPredEngine()
160
+ for (const [playerId, serverState] of this._snapProc.getAllPlayerStates()) {
161
+ if (playerId === this.playerId && this.config.predictionEnabled && predEngine) {
162
+ displayStates.set(playerId, predEngine.getDisplayState(this.currentTick, 0))
163
+ } else {
164
+ displayStates.set(playerId, serverState)
165
+ }
166
+ }
143
167
  }
144
168
  this.callbacks.onRender(displayStates)
145
169
  }
146
170
 
171
+ getSmoothState() {
172
+ const smoothInterp = this._msgHandler.getSmoothInterp()
173
+ if (smoothInterp) {
174
+ return smoothInterp.getDisplayState()
175
+ }
176
+ return { players: this.state.players, entities: this.state.entities }
177
+ }
178
+
179
+ getRTT() {
180
+ return this._msgHandler.getRTT()
181
+ }
182
+
183
+ getBufferHealth() {
184
+ return this._msgHandler.getBufferHealth()
185
+ }
186
+
147
187
  getLocalState() {
148
- return this.config.predictionEnabled && this._predEngine ? this._predEngine.localState : this._playerStates.get(this.playerId)
188
+ const predEngine = this._msgHandler.getPredEngine()
189
+ return this.config.predictionEnabled && predEngine ? predEngine.localState : this._snapProc.getPlayerState(this.playerId)
149
190
  }
150
191
 
151
- getRemoteState(playerId) { return this._playerStates.get(playerId) }
152
- getAllStates() { return new Map(this._playerStates) }
153
- getEntity(entityId) { return this._entityStates.get(entityId) }
154
- getAllEntities() { return new Map(this._entityStates) }
192
+ getRemoteState(playerId) {
193
+ return this._snapProc.getPlayerState(playerId)
194
+ }
195
+
196
+ getAllStates() {
197
+ return this._snapProc.getAllPlayerStates()
198
+ }
199
+
200
+ getEntity(entityId) {
201
+ return this._snapProc.getEntity(entityId)
202
+ }
203
+
204
+ getAllEntities() {
205
+ return this._snapProc.getAllEntities()
206
+ }
155
207
 
156
208
  disconnect() {
209
+ this._destroyed = true
210
+ this._reconnect.clear()
157
211
  this._stopHeartbeat()
158
212
  if (this.ws) this.ws.close()
159
213
  }
@@ -161,11 +215,30 @@ export class PhysicsNetworkClient {
161
215
  _startHeartbeat() {
162
216
  this._stopHeartbeat()
163
217
  this.heartbeatTimer = setInterval(() => {
164
- if (this._isOpen()) this.ws.send(pack({ type: MSG.HEARTBEAT, payload: {} }))
218
+ if (this._isOpen()) {
219
+ this._pingSent = Date.now()
220
+ this.ws.send(pack({ type: MSG.HEARTBEAT, payload: { timestamp: this._pingSent } }))
221
+ }
165
222
  }, 1000)
223
+ if (typeof document !== 'undefined' && !this._visibilityListener) {
224
+ this._visibilityListener = () => {
225
+ if (!document.hidden && this._isOpen()) {
226
+ this._pingSent = Date.now()
227
+ this.ws.send(pack({ type: MSG.HEARTBEAT, payload: { timestamp: this._pingSent } }))
228
+ }
229
+ }
230
+ document.addEventListener('visibilitychange', this._visibilityListener)
231
+ }
166
232
  }
167
233
 
168
234
  _stopHeartbeat() {
169
- if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null }
235
+ if (this.heartbeatTimer) {
236
+ clearInterval(this.heartbeatTimer)
237
+ this.heartbeatTimer = null
238
+ }
239
+ if (this._visibilityListener && typeof document !== 'undefined') {
240
+ document.removeEventListener('visibilitychange', this._visibilityListener)
241
+ this._visibilityListener = null
242
+ }
170
243
  }
171
244
  }
@@ -0,0 +1,62 @@
1
+ import { pack } from '../protocol/msgpack.js'
2
+ import { MSG } from '../protocol/MessageTypes.js'
3
+
4
+ export class ReconnectManager {
5
+ constructor(config = {}) {
6
+ this._sessionToken = null
7
+ this._reconnectAttempts = 0
8
+ this._reconnectTimer = null
9
+ this._reconnecting = false
10
+ this._maxReconnectDelay = config.maxReconnectDelay || 5000
11
+ this._destroyed = false
12
+ }
13
+
14
+ setSessionToken(token) {
15
+ this._sessionToken = token
16
+ }
17
+
18
+ isReconnecting() {
19
+ return this._reconnecting
20
+ }
21
+
22
+ sendReconnectMessage(ws) {
23
+ if (this._sessionToken && this._reconnecting && ws?.readyState === WebSocket.OPEN) {
24
+ ws.send(pack({ type: MSG.RECONNECT, payload: { sessionToken: this._sessionToken } }))
25
+ }
26
+ }
27
+
28
+ onConnected() {
29
+ this._reconnectAttempts = 0
30
+ this._reconnecting = false
31
+ if (this._reconnectTimer) {
32
+ clearTimeout(this._reconnectTimer)
33
+ this._reconnectTimer = null
34
+ }
35
+ }
36
+
37
+ onDisconnected(callback) {
38
+ if (this._destroyed) return
39
+ if (this._reconnectTimer) return
40
+ const delay = Math.min(1000 * Math.pow(1.5, this._reconnectAttempts), this._maxReconnectDelay)
41
+ this._reconnectAttempts++
42
+ this._reconnecting = true
43
+ this._reconnectTimer = setTimeout(() => {
44
+ this._reconnectTimer = null
45
+ if (!this._destroyed) callback()
46
+ }, delay)
47
+ }
48
+
49
+ clear() {
50
+ this._destroyed = true
51
+ if (this._reconnectTimer) {
52
+ clearTimeout(this._reconnectTimer)
53
+ this._reconnectTimer = null
54
+ }
55
+ this._sessionToken = null
56
+ }
57
+
58
+ invalidateSession() {
59
+ this._sessionToken = null
60
+ this._reconnecting = false
61
+ }
62
+ }
@@ -0,0 +1,127 @@
1
+ import { KalmanFilter3D, SmoothStateTracker } from './KalmanFilter.js'
2
+ import { JitterBuffer } from './JitterBuffer.js'
3
+
4
+ export class SmoothInterpolation {
5
+ constructor(config = {}) {
6
+ this.jitterBuffer = new JitterBuffer(config.jitter || {})
7
+ this.playerFilters = new Map()
8
+ this.entityFilters = new Map()
9
+ this.kalmanConfig = config.kalman || {
10
+ processNoise: 0.08,
11
+ measurementNoise: 0.3,
12
+ uncertainty: 0.5
13
+ }
14
+
15
+ this.lastFrameTime = Date.now()
16
+ this.localPlayerId = null
17
+ this.predictionEnabled = config.predictionEnabled !== false
18
+ this.extrapolationLimit = config.extrapolationLimit || 100
19
+ }
20
+
21
+ setLocalPlayer(id) {
22
+ this.localPlayerId = id
23
+ }
24
+
25
+ addSnapshot(snapshot) {
26
+ this.jitterBuffer.addSnapshot(snapshot)
27
+ }
28
+
29
+ getDisplayState(now = Date.now()) {
30
+ const snapshot = this.jitterBuffer.getSnapshotToRender(now)
31
+ if (!snapshot) return { players: [], entities: [] }
32
+
33
+ const dt = Math.min((now - this.lastFrameTime) / 1000, 0.1)
34
+ this.lastFrameTime = now
35
+
36
+ const displayPlayers = []
37
+ for (const player of snapshot.players || []) {
38
+ if (player.id === this.localPlayerId && this.predictionEnabled) {
39
+ displayPlayers.push(player)
40
+ continue
41
+ }
42
+
43
+ const smoothed = this._smoothPlayer(player, dt)
44
+ displayPlayers.push(smoothed)
45
+ }
46
+
47
+ const displayEntities = []
48
+ for (const entity of snapshot.entities || []) {
49
+ const smoothed = this._smoothEntity(entity, dt)
50
+ displayEntities.push(smoothed)
51
+ }
52
+
53
+ return { players: displayPlayers, entities: displayEntities }
54
+ }
55
+
56
+ _smoothPlayer(player, dt) {
57
+ let filter = this.playerFilters.get(player.id)
58
+ if (!filter) {
59
+ filter = new KalmanFilter3D(this.kalmanConfig)
60
+ this.playerFilters.set(player.id, filter)
61
+ }
62
+
63
+ const state = filter.update(player.position, player.velocity)
64
+
65
+ return {
66
+ ...player,
67
+ position: state.position,
68
+ velocity: state.velocity
69
+ }
70
+ }
71
+
72
+ _smoothEntity(entity, dt) {
73
+ let filter = this.entityFilters.get(entity.id)
74
+ if (!filter) {
75
+ filter = new KalmanFilter3D(this.kalmanConfig)
76
+ this.entityFilters.set(entity.id, filter)
77
+ }
78
+
79
+ const state = filter.update(entity.position)
80
+
81
+ return {
82
+ ...entity,
83
+ position: state.position
84
+ }
85
+ }
86
+
87
+ predictStep(dt) {
88
+ for (const [id, filter] of this.playerFilters) {
89
+ filter.predict(dt)
90
+ }
91
+ for (const [id, filter] of this.entityFilters) {
92
+ filter.predict(dt)
93
+ }
94
+ }
95
+
96
+ removePlayer(id) {
97
+ this.playerFilters.delete(id)
98
+ }
99
+
100
+ removeEntity(id) {
101
+ this.entityFilters.delete(id)
102
+ }
103
+
104
+ updateRTT(pingTime, pongTime) {
105
+ this.jitterBuffer.updateRTT(pingTime, pongTime)
106
+ }
107
+
108
+ getRTT() {
109
+ return this.jitterBuffer.getRTT()
110
+ }
111
+
112
+ getBufferHealth() {
113
+ return this.jitterBuffer.getBufferHealth()
114
+ }
115
+
116
+ reset() {
117
+ this.jitterBuffer.clear()
118
+ this.playerFilters.clear()
119
+ this.entityFilters.clear()
120
+ }
121
+
122
+ setConfig(config) {
123
+ if (config.kalman) {
124
+ this.kalmanConfig = { ...this.kalmanConfig, ...config.kalman }
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,144 @@
1
+ export class SnapshotProcessor {
2
+ constructor(config = {}) {
3
+ this._playerStates = new Map()
4
+ this._entityStates = new Map()
5
+ this.lastSnapshotTick = 0
6
+ this._callbacks = config.callbacks || {}
7
+ }
8
+
9
+ processSnapshot(data, tick) {
10
+ this.lastSnapshotTick = tick
11
+
12
+ const snapshotForBuffer = {
13
+ tick: data.tick || 0,
14
+ timestamp: data.timestamp || Date.now(),
15
+ players: [],
16
+ entities: []
17
+ }
18
+
19
+ const seenPlayers = new Set()
20
+ for (const p of data.players || []) {
21
+ const { playerId, state } = this._parsePlayer(p)
22
+ seenPlayers.add(playerId)
23
+ if (!this._playerStates.has(playerId)) {
24
+ this._callbacks.onPlayerJoined?.(playerId, state)
25
+ }
26
+ this._playerStates.set(playerId, state)
27
+ snapshotForBuffer.players.push(state)
28
+ }
29
+
30
+ for (const playerId of this._playerStates.keys()) {
31
+ if (!seenPlayers.has(playerId)) {
32
+ this._playerStates.delete(playerId)
33
+ this._callbacks.onPlayerLeft?.(playerId)
34
+ }
35
+ }
36
+
37
+ this._processEntities(data, snapshotForBuffer)
38
+ return snapshotForBuffer
39
+ }
40
+
41
+ _processEntities(data, snapshotForBuffer) {
42
+ if (data.delta) {
43
+ for (const e of data.entities || []) {
44
+ const { entityId, state } = this._parseEntity(e)
45
+ if (!this._entityStates.has(entityId)) {
46
+ this._callbacks.onEntityAdded?.(entityId, state)
47
+ }
48
+ this._entityStates.set(entityId, state)
49
+ snapshotForBuffer.entities.push(state)
50
+ }
51
+ if (data.removed) {
52
+ for (const eid of data.removed) {
53
+ if (this._entityStates.has(eid)) {
54
+ this._entityStates.delete(eid)
55
+ this._callbacks.onEntityRemoved?.(eid)
56
+ }
57
+ }
58
+ }
59
+ } else {
60
+ const seen = new Set()
61
+ for (const e of data.entities || []) {
62
+ const { entityId, state } = this._parseEntity(e)
63
+ seen.add(entityId)
64
+ if (!this._entityStates.has(entityId)) {
65
+ this._callbacks.onEntityAdded?.(entityId, state)
66
+ }
67
+ this._entityStates.set(entityId, state)
68
+ snapshotForBuffer.entities.push(state)
69
+ }
70
+ for (const eid of this._entityStates.keys()) {
71
+ if (!seen.has(eid)) {
72
+ this._entityStates.delete(eid)
73
+ this._callbacks.onEntityRemoved?.(eid)
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ _parsePlayer(p) {
80
+ if (Array.isArray(p)) {
81
+ return {
82
+ playerId: p[0],
83
+ state: {
84
+ id: p[0], position: [p[1], p[2], p[3]], rotation: [p[4], p[5], p[6], p[7]],
85
+ velocity: [p[8], p[9], p[10]], onGround: p[11] === 1, health: p[12],
86
+ inputSequence: p[13], crouch: p[14] || 0,
87
+ lookPitch: (p[15] || 0) / 255 * 2 * Math.PI - Math.PI,
88
+ lookYaw: (p[16] || 0) / 255 * 2 * Math.PI
89
+ }
90
+ }
91
+ }
92
+ return {
93
+ playerId: p.id || p.i,
94
+ state: {
95
+ id: p.id || p.i, position: p.position || [0, 0, 0], rotation: p.rotation || [0, 0, 0, 1],
96
+ velocity: p.velocity || [0, 0, 0], onGround: p.onGround ?? false, health: p.health ?? 100
97
+ }
98
+ }
99
+ }
100
+
101
+ _parseEntity(e) {
102
+ if (Array.isArray(e)) {
103
+ return {
104
+ entityId: e[0],
105
+ state: {
106
+ id: e[0], model: e[1], position: [e[2], e[3], e[4]], rotation: [e[5], e[6], e[7], e[8]],
107
+ bodyType: e[9], custom: e[10]
108
+ }
109
+ }
110
+ }
111
+ return {
112
+ entityId: e.id,
113
+ state: {
114
+ id: e.id, model: e.model, position: e.position || [0, 0, 0], rotation: e.rotation || [0, 0, 0, 1],
115
+ bodyType: e.bodyType || 'static', custom: e.custom || null
116
+ }
117
+ }
118
+ }
119
+
120
+ getPlayerState(playerId) {
121
+ return this._playerStates.get(playerId)
122
+ }
123
+
124
+ getAllPlayerStates() {
125
+ return new Map(this._playerStates)
126
+ }
127
+
128
+ getEntity(entityId) {
129
+ return this._entityStates.get(entityId)
130
+ }
131
+
132
+ getAllEntities() {
133
+ return new Map(this._entityStates)
134
+ }
135
+
136
+ removePlayer(playerId) {
137
+ this._playerStates.delete(playerId)
138
+ }
139
+
140
+ clear() {
141
+ this._playerStates.clear()
142
+ this._entityStates.clear()
143
+ }
144
+ }
@@ -31,14 +31,14 @@ export class ConnectionManager extends EventEmitter {
31
31
  })
32
32
 
33
33
  transport.on('close', () => {
34
- this.removeClient(clientId)
35
34
  this.emit('disconnect', clientId, 'closed')
35
+ this.removeClient(clientId)
36
36
  })
37
37
 
38
38
  transport.on('error', (err) => {
39
39
  console.error(`[connection] transport error for ${clientId}:`, err.message)
40
- this.removeClient(clientId)
41
40
  this.emit('disconnect', clientId, 'error')
41
+ this.removeClient(clientId)
42
42
  })
43
43
 
44
44
  this.clients.set(clientId, client)
@@ -52,8 +52,8 @@ export class ConnectionManager extends EventEmitter {
52
52
  if (!client) return
53
53
  const age = Date.now() - client.lastHeartbeat
54
54
  if (age > this.heartbeatTimeout) {
55
- this.removeClient(clientId)
56
55
  this.emit('disconnect', clientId, 'timeout')
56
+ this.removeClient(clientId)
57
57
  return
58
58
  }
59
59
  const timer = setTimeout(check, this.heartbeatInterval)
@@ -63,6 +63,11 @@ export class ConnectionManager extends EventEmitter {
63
63
  this.timers.set(`hb-${clientId}`, timer)
64
64
  }
65
65
 
66
+ resetHeartbeat(clientId) {
67
+ const client = this.clients.get(clientId)
68
+ if (client) client.lastHeartbeat = Date.now()
69
+ }
70
+
66
71
  removeClient(clientId) {
67
72
  const client = this.clients.get(clientId)
68
73
  if (!client) return
@@ -75,6 +80,19 @@ export class ConnectionManager extends EventEmitter {
75
80
  this.timers.delete(`hb-${clientId}`)
76
81
  }
77
82
 
83
+ detachClient(clientId) {
84
+ const client = this.clients.get(clientId)
85
+ if (client?.transport) {
86
+ client.transport.removeAllListeners('message')
87
+ client.transport.removeAllListeners('close')
88
+ client.transport.removeAllListeners('error')
89
+ }
90
+ this.clients.delete(clientId)
91
+ const timer = this.timers.get(`hb-${clientId}`)
92
+ if (timer) clearTimeout(timer)
93
+ this.timers.delete(`hb-${clientId}`)
94
+ }
95
+
78
96
  getClient(clientId) {
79
97
  return this.clients.get(clientId)
80
98
  }