Skip to content

Commit 2ba8559

Browse files
committed
feat(bridge): deliver app-built blueprints to the game cursor
New cmd.blueprint message (protocol v7): the app sends a blueprint string, the mod imports it onto the player's cursor as a TEMPORARY stack — clearing it destroys it instead of stuffing the inventory, since these are regenerable artifacts. Refuses politely when the cursor is already holding something.
1 parent ee2af23 commit 2ba8559

4 files changed

Lines changed: 47 additions & 4 deletions

File tree

app/src/server/bridge/fns.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ export const bridgeLocateFn = createServerFn({ method: "POST" })
3535
};
3636
});
3737

38+
/** Put a blueprint string on the connected player's cursor (cmd.blueprint) —
39+
* e.g. the sushi planner's set-point combinator. Fire-and-forget; returns
40+
* whether a peer was reachable. */
41+
export const bridgeBlueprintFn = createServerFn({ method: "POST" })
42+
.validator((d: { bp: string }) => d)
43+
.handler(async ({ data }) => {
44+
b.ensureBridge();
45+
return { sent: b.sendToPeer({ type: "cmd.blueprint", payload: { bp: data.bp } }) };
46+
});
47+
3848
/** Run a user-APPROVED Lua snippet in the game (#15) — the apply half of the
3949
* assistant's gameEval propose-then-approve gate. Only the chat card's Run
4050
* button calls this; the agent tool itself never executes. The mod refuses when

app/src/server/bridge/protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
// The bridge wire contract. Bump on BOTH sides (here and the mod's PROTOCOL_VERSION
1111
// in control.lua) whenever the message shapes change — each side warns when the
1212
// other reports a different version.
13-
export const PROTOCOL_VERSION = 6;
13+
export const PROTOCOL_VERSION = 7;
1414

1515
/** A request from the mod. `type` selects the handler; `payload` is type-specific. */
1616
export type BridgeRequest = {

docs/bridge.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ on any page. The same tab hosts the companion-mod installer (see below).
2626
per ore), and item production stats — pushed on connect and on relevant in-game
2727
events, always as the full authoritative set (no delta merging on the app side).
2828
- **Commands → game:** show a production-block panel in-game (`cmd.show_block`)
29-
and close it again (`cmd.hide_block`), and locate producers/consumers/storage
29+
and close it again (`cmd.hide_block`), locate producers/consumers/storage
3030
(`cmd.locate`, relayed to the
3131
[Factory Search](https://mods.factorio.com/mod/FactorySearch) mod's remote
32-
interface).
32+
interface), and put an app-built blueprint string on the player's cursor
33+
(`cmd.blueprint` — e.g. the sushi planner's set-point combinator; the mod
34+
refuses politely if the cursor is holding something).
3335
- **Task panel:** the in-game panel's **Tasks** tab pulls the project's
3436
tasks with `task.list` (the app replies with the full set — title, status,
3537
priority, body, steps, and links resolved to Factorio sprite paths) and renders

mod/control.lua

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ local BUTTON_NAME = "pyops_button"
1818
local SHORTCUT_NAME = "pyops-toggle-panel"
1919
-- Bridge wire contract. Keep in lockstep with PROTOCOL_VERSION in the app's
2020
-- src/server/bridge/protocol.ts — each side warns if the other reports a different one.
21-
local PROTOCOL_VERSION = 6
21+
local PROTOCOL_VERSION = 7
2222

2323
local function get_player(event)
2424
if not event.player_index then
@@ -404,6 +404,33 @@ local function locate_good(player, payload)
404404
end
405405
end
406406

407+
-- Put an app-sent blueprint string on the player's cursor (cmd.blueprint) —
408+
-- the same import_stack delivery the request-combinator generator uses. Refuses
409+
-- politely when the cursor is holding something so nothing gets clobbered.
410+
local function deliver_blueprint(player, payload)
411+
local bp = payload and payload.bp
412+
if type(bp) ~= "string" or bp == "" then
413+
return
414+
end
415+
local cursor = player.cursor_stack
416+
if not cursor then
417+
return
418+
end
419+
if cursor.valid_for_read then
420+
player.print({ "", "PyOps: clear your cursor first — a blueprint is waiting." })
421+
return
422+
end
423+
cursor.set_stack({ name = "blueprint" })
424+
if cursor.import_stack(bp) < 0 then
425+
cursor.clear()
426+
player.print({ "", "PyOps: failed to import the blueprint from the app." })
427+
return
428+
end
429+
-- temporary: the print vanishes when the cursor is cleared (Q) instead of
430+
-- landing in the inventory — these are regenerable artifacts, not keepsakes
431+
player.cursor_stack_temporary = true
432+
end
433+
407434
-- ── Read-only game-world inspection (app → mod cmd.* → reply) ──────────────────
408435
-- All bounded and structured: the app's assistant tools call these to ground a
409436
-- task in live evidence. No whole-map dumps.
@@ -652,6 +679,10 @@ local function handle_bridge_response(player, response)
652679
elseif response.type == "cmd.locate" then
653680
-- the app asked us to find a good in the world (web "locate in game" button)
654681
locate_good(player, response.payload)
682+
elseif response.type == "cmd.blueprint" then
683+
-- the app sent a blueprint string (e.g. the sushi set-point combinator) —
684+
-- put it on the cursor, same delivery as the request-combinator generator
685+
deliver_blueprint(player, response.payload)
655686
elseif response.type == "cmd.show_block" then
656687
-- the app pushed a solved block to render as an in-game build sheet
657688
Summary.show(player, response.payload)

0 commit comments

Comments
 (0)