screeps-connectivity 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - de4fd47: Add memory watch panel with live WebSocket subscriptions, persistent watchlist, temp creep watch, and inline editing.
8
+
9
+ `screeps-connectivity` gains `UserStore.subscribeMemory(path, shard?)` and a new `user:memory` event on `UserStoreEvents`. `screeps-client` adds a full Memory pane to the bottom bar: a persistent watchlist, a temporary per-creep watch triggered from the Eye button on the selection panel, a recursive type-aware `MemoryTree` with expand/collapse, insert-to-console, and inline leaf editing.
10
+
11
+ ## 0.3.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 9c24c2f: Add memory watch panel with live WebSocket subscriptions, persistent watchlist, temp creep watch, and inline editing.
16
+
17
+ `screeps-connectivity` gains `UserStore.subscribeMemory(path, shard?)` and a new `user:memory` event on `UserStoreEvents`. `screeps-client` adds a full Memory pane to the bottom bar: a persistent watchlist, a temporary per-creep watch triggered from the Eye button on the selection panel, a recursive type-aware `MemoryTree` with expand/collapse, insert-to-console, and inline leaf editing.
18
+
19
+ ### Patch Changes
20
+
21
+ - 31e9570: Fix destroying roads and walls in the property viewer when the user owns the room.
22
+
23
+ Roads and walls carry no `user` field, so the destroy button was never shown.
24
+ The fix falls back to `roomOwner().userId` for ownerless structures and
25
+ correctly passes `room`, `roomName`, and an optional `shard` in the
26
+ `destroyStructure` intent — matching the format the official client sends.
27
+ `addObjectIntent` in `screeps-connectivity` now accepts an optional `shard`
28
+ parameter.
29
+
3
30
  ## 0.2.4
4
31
 
5
32
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screeps-connectivity",
3
- "version": "0.2.4",
3
+ "version": "0.4.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "repository": {
@@ -120,7 +120,8 @@ export class HttpClient extends EventTarget {
120
120
  return this.request<T>(method, path, body, true)
121
121
  }
122
122
 
123
- if (!res.ok) {
123
+ // 304: some servers (e.g. private Screeps) send a body with 304 — treat it as success
124
+ if (!res.ok && res.status !== 304) {
124
125
  let body = ''
125
126
  try { body = await res.text() } catch { /* ignore */ }
126
127
  const error = new Error(`HTTP ${res.status}: ${body}`)
@@ -1,31 +1,13 @@
1
1
  async function decompress(data: string, format: 'gzip' | 'deflate'): Promise<unknown> {
2
- const b64 = data.slice(3) // strip 'gz:' prefix
2
+ const b64 = data.slice(3) // strip 'gz:' or 'zlib:' prefix
3
3
  const binary = Uint8Array.from(atob(b64), c => c.charCodeAt(0))
4
- const ds = new DecompressionStream(format)
5
- const writer = ds.writable.getWriter()
6
- await writer.write(binary)
7
- await writer.close()
8
- const reader = ds.readable.getReader()
9
- const chunks: Uint8Array[] = []
4
+ const decompressed = new Blob([binary]).stream().pipeThrough(new DecompressionStream(format))
5
+ const text = await new Response(decompressed).text()
10
6
  try {
11
- while (true) {
12
- const { done, value } = await reader.read()
13
- if (done) break
14
- chunks.push(value)
15
- }
16
- } catch (e) {
17
- reader.cancel()
18
- throw e
7
+ return JSON.parse(text)
8
+ } catch {
9
+ return text
19
10
  }
20
- let totalLength = 0
21
- for (const chunk of chunks) totalLength += chunk.length
22
- const result = new Uint8Array(totalLength)
23
- let offset = 0
24
- for (const chunk of chunks) {
25
- result.set(chunk, offset)
26
- offset += chunk.length
27
- }
28
- return JSON.parse(new TextDecoder().decode(result))
29
11
  }
30
12
 
31
13
  export function decompressGzip(data: string): Promise<unknown> {
@@ -37,7 +37,7 @@ export interface GameEndpoints {
37
37
  placeSpawn(room: string, x: number, y: number, name?: string, shard?: string | null): Promise<{ ok: number }>
38
38
  createConstruction(room: string, x: number, y: number, structureType: string, name?: string, shard?: string | null): Promise<{ ok: number }>
39
39
  removeConstructionSite(room: string, ids: string[], shard?: string | null): Promise<{ ok: number }>
40
- addObjectIntent(id: string, room: string, name: string, intent: unknown): Promise<{ ok: number }>
40
+ addObjectIntent(id: string, room: string, name: string, intent: unknown, shard?: string | null): Promise<{ ok: number }>
41
41
  addGlobalIntent(name: string, intent: unknown): Promise<{ ok: number }>
42
42
  setNotifyWhenAttacked(id: string, enabled: boolean): Promise<{ ok: number }>
43
43
  createInvader(room: string, x: number, y: number, size: number, type: string, boosted?: boolean): Promise<{ ok: number }>
@@ -83,7 +83,7 @@ export function createGameEndpoints(http: HttpClient): GameEndpoints {
83
83
  placeSpawn: (room, x, y, name, shard) => http.request('POST', '/api/game/place-spawn', withShard({ room, x, y, ...(name ? { name } : {}) }, shard)),
84
84
  createConstruction: (room, x, y, structureType, name, shard) => http.request('POST', '/api/game/create-construction', withShard({ room, x, y, structureType, ...(name ? { name } : {}) }, shard)),
85
85
  removeConstructionSite: (room, ids, shard) => http.request('POST', '/api/game/add-object-intent', withShard({ _id: 'room', room, name: 'removeConstructionSite', intent: ids.map(id => ({ id, roomName: room })) }, shard)),
86
- addObjectIntent: (id, room, name, intent) => http.request('POST', '/api/game/add-object-intent', { _id: id, room, name, intent }),
86
+ addObjectIntent: (id, room, name, intent, shard) => http.request('POST', '/api/game/add-object-intent', withShard({ _id: id, room, name, intent }, shard)),
87
87
  addGlobalIntent: (name, intent) => http.request('POST', '/api/game/add-global-intent', { name, intent }),
88
88
  setNotifyWhenAttacked: (id, enabled) => http.request('POST', '/api/game/set-notify-when-attacked', { _id: id, enabled }),
89
89
  createInvader: (room, x, y, size, type, boosted) => http.request('POST', '/api/game/create-invader', { room, x, y, size, type, ...(boosted != null ? { boosted } : {}) }),
@@ -136,6 +136,70 @@ export class UserStore extends TypedStore<UserStoreEvents> {
136
136
  }
137
137
  }
138
138
 
139
+ subscribeMemory(path: string, shard?: string | null): Subscription {
140
+ this.logger.log('subscribe memory', path)
141
+ let socketSub: Subscription | null = null
142
+ let listenerSub: Subscription | null = null
143
+ let disposed = false
144
+
145
+ const setup = async () => {
146
+ try {
147
+ const uid = this._userId ?? (await this.me())._id
148
+ if (disposed) return
149
+ const shardSegment = shard ? `${shard}/` : ''
150
+ const fullChannel = `user:${uid}/memory/${shardSegment}${path}`
151
+ socketSub = this.socket.subscribe(fullChannel)
152
+ listenerSub = this.socket.on(fullChannel, (raw) => {
153
+ if (typeof raw === 'string' && raw.startsWith('gz:')) {
154
+ void (async () => {
155
+ try {
156
+ const { decompressZlib } = await import('../http/decompress.js')
157
+ const value = await decompressZlib(raw)
158
+ this.emit('user:memory', { path, shard: shard ?? null, value })
159
+ } catch (err) {
160
+ this.logger.log('memory decompress failed', err)
161
+ }
162
+ })()
163
+ return
164
+ }
165
+ // Memory values arrive as JSON-encoded strings inside the WS frame,
166
+ // e.g. the frame ["user:x/memory/foo","1"] delivers raw="1" here.
167
+ // Objects can't be serialized over WS; the server sends "[object Object]".
168
+ // Emit a sentinel so the UI can show a collapsed placeholder and fetch
169
+ // the real value via HTTP only when the user expands it.
170
+ if (raw === '[object Object]') {
171
+ this.logger.log('memory object placeholder', path)
172
+ this.emit('user:memory', { path, shard: shard ?? null, value: { __screeps_object__: true } })
173
+ return
174
+ }
175
+ let value: unknown = raw
176
+ if (raw === 'undefined') {
177
+ value = undefined
178
+ } else if (typeof raw === 'string') {
179
+ try { value = JSON.parse(raw) } catch { /* leave as-is */ }
180
+ }
181
+ this.logger.log('memory value received', path, value)
182
+ this.emit('user:memory', { path, shard: shard ?? null, value })
183
+ })
184
+ } catch (err) {
185
+ if (!disposed) {
186
+ this.dispatchEvent(new ErrorEvent('error', { error: err instanceof Error ? err : new Error(String(err)) }))
187
+ }
188
+ }
189
+ }
190
+
191
+ void setup()
192
+
193
+ return {
194
+ dispose: () => {
195
+ this.logger.log('unsubscribe memory', path)
196
+ disposed = true
197
+ socketSub?.dispose()
198
+ listenerSub?.dispose()
199
+ },
200
+ }
201
+ }
202
+
139
203
  /** Subscribe to the general user stream to receive global data like flags. */
140
204
  subscribeUserStream(): Subscription {
141
205
  this.logger.log('subscribe user stream')
@@ -41,6 +41,7 @@ export interface UserStoreEvents {
41
41
  'user:console': { messages: ConsoleMessage }
42
42
  'user:code': { branch: string; modules: Record<string, string> }
43
43
  'user:stream': Record<string, unknown>
44
+ 'user:memory': { path: string; shard: string | null; value: unknown }
44
45
  }
45
46
 
46
47
  export interface ServerStoreEvents {