screeps-connectivity 0.2.4 → 0.3.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 +19 -0
- package/package.json +1 -1
- package/src/http/HttpClient.ts +2 -1
- package/src/http/decompress.ts +6 -24
- package/src/http/endpoints/game.ts +2 -2
- package/src/stores/UserStore.ts +64 -0
- package/src/types/events.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9c24c2f: 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
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 31e9570: Fix destroying roads and walls in the property viewer when the user owns the room.
|
|
14
|
+
|
|
15
|
+
Roads and walls carry no `user` field, so the destroy button was never shown.
|
|
16
|
+
The fix falls back to `roomOwner().userId` for ownerless structures and
|
|
17
|
+
correctly passes `room`, `roomName`, and an optional `shard` in the
|
|
18
|
+
`destroyStructure` intent — matching the format the official client sends.
|
|
19
|
+
`addObjectIntent` in `screeps-connectivity` now accepts an optional `shard`
|
|
20
|
+
parameter.
|
|
21
|
+
|
|
3
22
|
## 0.2.4
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/package.json
CHANGED
package/src/http/HttpClient.ts
CHANGED
|
@@ -120,7 +120,8 @@ export class HttpClient extends EventTarget {
|
|
|
120
120
|
return this.request<T>(method, path, body, true)
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
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}`)
|
package/src/http/decompress.ts
CHANGED
|
@@ -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
|
|
5
|
-
const
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 } : {}) }),
|
package/src/stores/UserStore.ts
CHANGED
|
@@ -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')
|
package/src/types/events.ts
CHANGED
|
@@ -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 {
|