commit 20b4e22ea6e84720a383e3ad6adcaa3ddf96c1a4 Author: Quantum Date: Sun Nov 24 23:43:23 2024 -0500 Initial Commit diff --git a/base.lua b/base.lua new file mode 100644 index 0000000..43f2925 --- /dev/null +++ b/base.lua @@ -0,0 +1,665 @@ +base_fth = [[ +: HEX 16 BASE ! ; +: DEC 10 BASE ! ; +: OCT 8 BASE ! ; +: BIN 2 BASE ! ; + +: C! SWAP 255 AND SWAP ! ; +: C@ @ 255 AND ; +: C, + HERE @ C! + 1 HERE +! +; + +: LITERAL IMMEDIATE + COMP' LIT , + , +; +: CHAR BL WORD 1+ C@ ; + +: '"' [ CHAR " ] LITERAL ; +: '[' [ CHAR [ ] LITERAL ; +: ']' [ CHAR ] ] LITERAL ; +: '(' [ CHAR ( ] LITERAL ; +: ')' [ CHAR ) ] LITERAL ; +: ':' [ CHAR : ] LITERAL ; +: ';' [ CHAR ; ] LITERAL ; +: '0' [ CHAR 0 ] LITERAL ; +: 'A' [ CHAR A ] LITERAL ; +: '-' [ CHAR - ] LITERAL ; +: '.' [ CHAR . ] LITERAL ; + +: CR 10 EMIT ; +: SPACE 32 EMIT ; + +: ' BL WORD FIND >CFA ; +: [COMPILE] IMMEDIATE ' , ; +: ['] IMMEDIATE ' [COMPILE] LITERAL ; +: LIT, ['] LIT , , ; + +: / /MOD SWAP DROP ; +: MOD /MOD DROP ; + +: U/ UM/MOD SWAP DROP ; +: UMOD UM/MOD DROP ; + +: NOT 0= ; + +: RECURSE IMMEDIATE + LATEST @ + >CFA , +; + +: IF IMMEDIATE + ['] 0BRANCH , + HERE @ + 0 , +; +: THEN IMMEDIATE + DUP + HERE @ SWAP - + SWAP ! +; +: ELSE IMMEDIATE + ['] BRANCH , + HERE @ + 0 , + SWAP + DUP HERE @ SWAP - + SWAP ! +; + +: ?HIDDEN 1+ C@ F_HIDDEN AND ; +: ?IMMEDIATE 1+ C@ F_IMMED AND ; + +: EXECUTE-COMPILING + STATE @ IF EXECUTE EXIT THEN + ] EXECUTE [ +; + +: POSTPONE IMMEDIATE + BL WORD FIND + DUP ?IMMEDIATE + SWAP >CFA LIT, + IF ['] EXECUTE-COMPILING ELSE ['] , THEN , +; + +: BEGIN IMMEDIATE HERE @ ; +: UNTIL IMMEDIATE + POSTPONE 0BRANCH + HERE @ - , +; +: AGAIN IMMEDIATE + POSTPONE BRANCH + HERE @ - , +; +: WHILE IMMEDIATE + POSTPONE 0BRANCH + HERE @ + 0 , +; +: REPEAT IMMEDIATE + POSTPONE BRANCH + SWAP HERE @ - , + DUP HERE @ SWAP - SWAP ! +; +: UNLESS IMMEDIATE + POSTPONE NOT + POSTPONE IF +; + +: ( IMMEDIATE + 1 + BEGIN + KEY DUP '(' = IF + DROP + 1+ + ELSE + ')' = IF + 1- + THEN + THEN + DUP 0= UNTIL + DROP +; + +: NIP ( x y -- y ) SWAP DROP ; +: TUCK ( x y -- y x y ) SWAP OVER ; +: PICK ( xn ... x1 x0 n -- xn ... x1 x0 xn ) + 1+ 1 CELLS * DSP@ + @ +; + +( Print many of a character ) +: MANY ( n c -- ) + SWAP + BEGIN + DUP 0> + WHILE + OVER EMIT + 1- + REPEAT + 2DROP +; + +: SPACES ( n -- ) + BL MANY +; + +: ZEROS ( n -- ) + '0' MANY +; + +: U. ( u -- ) + BASE @ UM/MOD ( base rem quot ) + ?DUP IF + RECURSE + THEN + + DUP 10 < IF + '0' + ELSE + 10 - + 'A' + THEN + + EMIT +; + +( Get number of chars in an unsigend number ) +: UWIDTH ( u -- width ) + BASE @ U/ ( rem quot) + ?DUP IF ( if quot <> 0 ) + RECURSE 1+ + ELSE + 1 + THEN +; + +( Get number of chars in an unsigend number ) +: SWIDTH ( u -- width ) + DUP 0< IF NEGATE THEN + BASE @ / ( rem quot) + ?DUP IF ( if quot <> 0 ) + RECURSE 1+ + ELSE + 1 + THEN +; + +( Left pad number ) +: U.R ( u width -- ) + SWAP + DUP + UWIDTH + ROT + SWAP - + SPACES + U. +; + +( Zero pad number ) +: U.0R ( u width -- ) + SWAP + DUP + UWIDTH + ROT + SWAP - + ZEROS + U. +; + +( Print left padded signed number ) +: .R + SWAP + DUP 0< IF + NEGATE + 1 + SWAP + ROT + 1- + ELSE + 0 + SWAP + ROT + THEN + SWAP + DUP + SWIDTH + ROT + SWAP - + + SPACES + SWAP + + IF + '-' EMIT + THEN + + U. +; + +( Print zero padded signed number ) +: .0R + SWAP + DUP 0< IF + NEGATE + 1 + SWAP + ROT + 1- + ELSE + 0 + SWAP + ROT + THEN + SWAP + DUP + SWIDTH + ROT + SWAP - + + ZEROS + SWAP + + IF + '-' EMIT + THEN + + U. +; + +: . 0 .R SPACE ; +: U. U. SPACE ; + +: .S ( Print the stack ) + DSP@ S0 @ + BEGIN + 2DUP < + WHILE + 1- + DUP @ . + SPACE + REPEAT + 2DROP + CR +; + +( Print number at address ) +: ? ( addr -- ) @ . ; + +: WITHIN + -ROT OVER + <= IF + > IF + TRUE + ELSE + FALSE + THEN + ELSE + 2DROP + FALSE + THEN +; + +( Returns depth of stack ) +: DEPTH ( -- n ) + S0 @ DSP@ - + 1- +; + +: S" IMMEDIATE + STATE @ IF + POSTPONE LITSTRING + HERE @ + 0 , + BEGIN + KEY + DUP '"' <> + WHILE + C, + REPEAT + DROP + DUP + HERE @ SWAP - + 1- + SWAP ! + ELSE + HERE @ + BEGIN + KEY + DUP '"' <> + WHILE + OVER C! + 1+ + REPEAT + DROP + HERE @ - + HERE @ + SWAP + THEN +; + +: ." IMMEDIATE + STATE @ IF + POSTPONE S" + POSTPONE TELL + ELSE + BEGIN + KEY + DUP '"' = IF + DROP + EXIT + THEN + EMIT + AGAIN + THEN +; + +: DOES> + STATE @ IF + POSTPONE (DOES>) + ELSE + HERE @ ( HERE ) + LATEST @ >CFA ( HERE CFA ) + DUP [ ' DODOES @ ] LITERAL SWAP ! ( Write CW of DODOES to CFA ) + 1+ ! ( Write HERE to >DOES ) + LATEST @ HIDDEN + ] + THEN +; IMMEDIATE + +: VARIABLE CREATE 0 , ; +: CONSTANT CREATE , DOES> @ ; + +: VALUE ( n -- ) + CREATE , + DOES> ( -- n ) + @ +; + +: TO ( n "name" -- ) + ' >BODY ! +; + +: +TO ( n "name" -- ) + ' >BODY +! +; + +0 VALUE I +: SETI [ ' I >BODY ] LITERAL ! ; + +: DO IMMEDIATE ( C: -- HERE ) ( LIMIT I -- ) + SETI + R> ( r: -- LIMIT ) + HERE @ ( C: -- HERE ) +; + +: (LOOP) + I 1+ DUP SETI + R@ > + 0BRANCH +; + +: (UNLOOP) + >R DROP +; + +: LOOP IMMEDIATE + POSTPONE (LOOP) + HERE @ - , + POSTPONE (UNLOOP) +; + +: ID. + 1+ ( Skip link pointer ) + DUP C@ ( entry flags ) + F_LENMASK AND ( entry len) + + BEGIN + DUP 0> ( entry len len>0? ) + WHILE + SWAP 1+ ( len entry+1 ) + DUP C@ EMIT + SWAP 1- ( entry+1 len-1 ) + REPEAT + 2DROP ( entry len -- ) +; + +: WORDS + LATEST @ + BEGIN + ?DUP + WHILE + DUP ?HIDDEN IF + ." (H)" + THEN + DUP ?IMMEDIATE IF + ." (I)" + THEN + DUP ID. SPACE + @ + REPEAT + CR +; + +: HIDE + BL WORD FIND + HIDDEN +; + +: FORGET + BL WORD FIND + DUP @ LATEST ! + HERE ! +; + +: DUMP ( addr len -- ) + ( Save BASE ) + BASE @ -ROT + + BEGIN + ?DUP + WHILE + HEX + OVER 8 U.0R + 4 SPACES + + ( Print hex ) + 2DUP + 1- 15 AND 1+ + BEGIN + ?DUP + WHILE + SWAP + DUP + 5 PICK DEC 10 = IF + @ 0 DEC + ELSE + C@ 2 HEX + THEN + .0R SPACE + 1+ SWAP 1- + REPEAT + DROP + + 3 SPACES + + ( Print ascii ) + 2DUP + 1- 15 AND 1+ + BEGIN + ?DUP + WHILE + SWAP + DUP C@ + DUP 32 128 WITHIN IF + EMIT + ELSE + DROP '.' EMIT + THEN + 1+ SWAP 1- + REPEAT + DROP + CR + + DUP 1- 15 AND 1+ + TUCK + - + >R + R> + REPEAT + + DROP + BASE ! +; + +: CASE IMMEDIATE + 0 +; + +: OF IMMEDIATE + POSTPONE OVER + POSTPONE = + POSTPONE IF + POSTPONE DROP +; + +: ENDOF IMMEDIATE + POSTPONE ELSE +; + +: ENDCASE IMMEDIATE + POSTPONE DROP + + ( Compile THEN's for each ELSE ) + BEGIN + ?DUP + WHILE + POSTPONE THEN + REPEAT +; + +: CFA> + LATEST @ + BEGIN + ?DUP + WHILE + 2DUP SWAP + < IF + NIP + EXIT + THEN + @ + REPEAT + DROP + 0 +; + +: SEE + BL WORD FIND + + HERE @ + LATEST @ + BEGIN + 2 PICK + OVER + <> + WHILE + NIP + DUP @ + REPEAT + + DROP SWAP + + ':' EMIT SPACE DUP ID. CR + DUP ?IMMEDIATE IF ." IMMEDIATE " THEN + + >DFA + + BEGIN + 2DUP > + WHILE + DUP @ + + CASE + ['] LIT OF + CELL+ DUP @ + . CR + ENDOF + ['] LITSTRING OF + [ CHAR S ] LITERAL EMIT '"' EMIT SPACE + CELL+ + COUNT 2DUP TELL + '"' EMIT CR + + + CELL- + ENDOF + ['] 0BRANCH OF + ." 0BRANCH(" + CELL+ DUP @ 0 .R + ." ) " CR + ENDOF + ['] BRANCH OF + ." BRANCH(" + CELL+ DUP @ 0 .R + ." ) " CR + ENDOF + ['] ' OF + [ CHAR ' ] LITERAL EMIT SPACE + CELL+ @ + CFA> ID. CR + ENDOF + ['] EXIT OF + 2DUP CELL+ + <> IF + ." EXIT " CR + THEN + ENDOF + DUP + CFA> + ID. CR + ENDCASE + + CELL+ + REPEAT + + ';' EMIT CR + + 2DROP +; + +: :NONAME + HERE @ + LATEST @ , + LATEST ! + 0 , + HERE @ + [ ' DOCOL @ ] LITERAL , + 0 , + ] +; + +( Colors ) +DEC +: ESC[ 27 EMIT '[' EMIT ; +0 CONSTANT RESET +1 CONSTANT BRIGHT +2 CONSTANT DIM +4 CONSTANT UNSERSCORE +5 CONSTANT BLINK +7 CONSTANT REVERSE +8 CONSTANT HIDDEN + +: FG> 30 + ; +: BG> 40 + ; + +0 CONSTANT ESC[ 0 .R ';' EMIT 0 .R ." m" ; +: ESC[ 0 .R ';' EMIT ." ;m" ; +]] diff --git a/control.lua b/control.lua new file mode 100644 index 0000000..e180cf8 --- /dev/null +++ b/control.lua @@ -0,0 +1,198 @@ +require('forth') + +function init_storage() + -- storage.valid = false + if not storage.valid then + storage.players = {} + storage.computers = {} + storage.guis = {} + storage.valid = true + end +end + +function computer_for_ent(entity) + for _, computer in pairs(storage.computers) do + if computer.entity == entity then + return computer + end + end + return nil +end + +script.on_event(defines.events.on_tick, function(event) + init_storage() + + for _, computer in pairs(storage.computers) do + for _=1,250 do + local ok, result = pcall(forth_tick, computer.ctx) + if ok and result then break end + if not ok then + game.print(result) + computer.ctx = forth_initialize(computer.entity) + end + end + if #computer.ctx.outbuf > 0 then + local out = '' + for i, v in pairs(computer.ctx.outbuf) do + out = out..v + end + computer.screen = computer.screen..out + computer.ctx.outbuf = {} + end + end + + for player_index, player in pairs(storage.players) do + if player.opened_combinator then + local computer = computer_for_ent(player.opened_combinator) + local ui = game.get_player(player_index).gui.screen.forth_combinator_gui + ui.forth_combinator_console.text = computer.screen + end + end +end) + +script.on_event(defines.events.on_gui_text_changed, function(event) + if not event.element or event.element.name ~= 'forth_combinator_editor' then return end + + local player = storage.players[event.player_index] + player.curtext = event.element.text +end) + +function create_ui(player, computer) + local frame = player.gui.screen.add{type = "frame", name = "forth_combinator_gui", caption = "Console"} + frame.force_auto_center() + + local editor = frame.add{type = 'text-box', name = 'forth_combinator_editor'} + editor.style.minimal_height = 600 + editor.style.minimal_width = 800 + editor.style.maximal_height = 600 + editor.style.maximal_width = 800 + editor.text = computer.script + local console = frame.add{type = 'text-box', name = 'forth_combinator_console'} + console.style.minimal_height = 600 + console.style.minimal_width = 400 + console.style.maximal_height = 600 + console.style.maximal_width = 400 + console.text = computer.screen + local run_button = frame.add{type = 'button', name = 'forth_combinator_run', caption = "Do Shit"} + -- console.selectable = false + + + + return frame +end + +script.on_event(defines.events.on_gui_opened, function(event) + if not event.entity or event.entity.name ~= 'forth-combinator' then return end + init_storage() + + local player = game.get_player(event.player_index) + + if not storage.players[event.player_index] then + storage.players[event.player_index] = {} + end + + if player.gui.screen.forth_combinator_gui then + player.gui.screen.forth_combinator_gui.destroy() + end + + if player.opened then + player.opened = nil + end + + player.opened = create_ui(player, computer_for_ent(event.entity)) + storage.players[event.player_index].opened_combinator = event.entity +end) + +script.on_event(defines.events.on_gui_closed, function(event) + local player = game.get_player(event.player_index) + + if player.gui.screen.forth_combinator_gui then + player.gui.screen.forth_combinator_gui.destroy() + if storage.players[event.player_index] then + storage.players[event.player_index].opened_combinator = nil + end + end +end) + +script.on_event(defines.events.on_gui_click, function(event) + local player = game.get_player(event.player_index) + local entity = storage.players[event.player_index].opened_combinator + + if event.element.name == 'forth_combinator_run' then + local computer = computer_for_ent(entity) + if not computer then + game.print('Invalid computer in gui click') + return + end + computer.ctx = forth_initialize(entity) + + -- Hack, idc + local text = storage.players[event.player_index].curtext + for c in text:gmatch('.') do + table.insert(computer.ctx.keybuf, string.byte(c)) + end + table.insert(computer.ctx.keybuf, 10) + + computer.screen = '' + computer.script = text + end +end) + +function OnBuiltEntity(event) + local entity = event.entity + if entity.name ~= 'forth-combinator' then return end + init_storage() + + local computer = { + ctx = forth_initialize(entity), + entity = entity, + screen = '', + script = '', + } + + table.insert(storage.computers, computer) +end + +function OnEntityDied(event) + local entity = event.entity + if entity.name ~= 'forth-combinator' then return end + init_storage() + + local removals = {} + for i, comp in pairs(storage.computers) do + if comp.entity == entity then + table.insert(removals, i) + end + end + for _, i in pairs(removals) do + table.remove(storage.computers, i) + end +end + +script.on_event(defines.events.on_built_entity, OnBuiltEntity) +script.on_event(defines.events.on_robot_built_entity, OnBuiltEntity) +-- script.on_event(defines.events.on_pre_ghost_deconstructed, OnPreGhostDeconstructed) +script.on_event(defines.events.on_entity_died, OnEntityDied) +script.on_event(defines.events.on_pre_player_mined_item, OnEntityDied) +script.on_event(defines.events.on_robot_pre_mined, OnEntityDied) + + + + + + + + + + + + + + + +-- player.surface.create_entity({ +-- name = 'laser-beam', +-- position = entity.position, +-- source_position = entity.position, +-- target = player.position, +-- }) diff --git a/data.lua b/data.lua new file mode 100644 index 0000000..e4f3a5a --- /dev/null +++ b/data.lua @@ -0,0 +1,27 @@ +local forthCombinator = table.deepcopy(data.raw['constant-combinator']['constant-combinator']) + +forthCombinator.name = 'forth-combinator' +forthCombinator.minable.result = 'forth-combinator' +for k, direction in pairs(forthCombinator.sprites) do + for kk, vv in pairs(direction.layers) do + vv.tint = {r = 0, g = 0, b = 1} + end +end + +local forthCombinatorItem = table.deepcopy(data.raw['item']['constant-combinator']) +forthCombinatorItem.name = 'forth-combinator' +forthCombinatorItem.place_result = 'forth-combinator' + +local recipe = { + type = 'recipe', + name = 'forth-combinator', + enabled = true, + energy_required = 1, -- time to craft in seconds (at crafting speed 1) + ingredients = { + {type = 'item', name = 'copper-plate', amount = 1} + }, + results = {{type = 'item', name = 'forth-combinator', amount = 1}} +} + +data:extend{forthCombinator, forthCombinatorItem, recipe} + diff --git a/forth.lua b/forth.lua new file mode 100644 index 0000000..e2c2e3f --- /dev/null +++ b/forth.lua @@ -0,0 +1,1229 @@ +require('base') + +-- Bullshit +sigs = { + virtual_signal = {}, + item = {}, + fluid = {}, +} + +for sig_name, sig_arr in pairs(sigs) do + for name, prototype in pairs(prototypes[sig_name]) do + if not prototype.parameter then + table.insert(sig_arr, name) + end + end +end +-- Bullshit + +local F_TRUE = -1 +local F_FALSE = 0 + +local F_IMMED = 0x80 +local F_HIDDEN = 0x20 +local F_LENMASK = 0x1F + +local cw_index = 0 +local codewords = {} +local codeword_names = {} +local labels = {} +local init_context = { + memspace = {}, + xp = 0, + sp = 0x1000, + rp = 0x1100, + last_xt = 0, + keybuf = {}, + outbuf = {}, + yield = false, + waiting_key = false, + sleep_ticks = 0, + bs = '', + valid = false, +} + +function copy_context(ctx) + local new_ctx = { + xp = ctx.xp, + sp = ctx.sp, + rp = ctx.rp, + last_xt = ctx.last_xt, + yield = ctx.yield, + waiting_key = ctx.waiting_key, + sleep_ticks = ctx.sleep_ticks, + bs = ctx.bs, + keybuf = {}, + outbuf = {}, + memspace = {}, + } + for i, v in pairs(ctx.keybuf) do new_ctx.keybuf[i] = v end + for i, v in pairs(ctx.outbuf) do new_ctx.outbuf[i] = v end + for i, v in pairs(ctx.memspace) do new_ctx.memspace[i] = v end + + return new_ctx +end + +dbg_print = function(...) + --print(...) + -- local str = '' + -- for i, v in pairs({...}) do + -- str = str..tostring(v)..' ' + -- end + -- log(str) + -- game.print(str) +end + +dbg_interp = dbg_print + +local function output_string(context, str) + for c in str:gmatch('.') do + table.insert(context.outbuf, c) + end +end + +local function read(context, addr) + local res = context.memspace[addr] + if res == nil then + error('Read from invalid address') + end + return res +end + +local function write(context, addr, data) + context.memspace[addr] = data +end + +local function push_rsp(context, data) + dbg_print('push rsp ', data) + context.rp = context.rp - 1 + write(context, context.rp, data) +end + +local function pop_rsp(context) + local data = read(context, context.rp) + context.rp = context.rp + 1 + dbg_print('pop rsp ', data) + return data +end + +local function push(context, data) + dbg_print('push ', data) + context.sp = context.sp - 1 + write(context, context.sp, data) +end + +local function pop(context) + local data = read(context, context.sp) + if data == nil then + error('Pop from outside stack') + end + context.sp = context.sp + 1 + dbg_print('pop ', data) + return data +end + +local function peek(context, off) + return read(context, context.sp + off) +end + +local function do_codeword(context, xt) + -- Save xt so we can resume from a yield + context.last_xt = xt + local codeword = read(context, xt) + dbg_print('XT', xt, labels[xt]) + dbg_print('CW', codeword, codeword_names[codeword]) + + -- Reinit codewords when loading a save + if #codewords == 0 then + forth_initialize(context.entity) + end + + codewords[codeword](context) +end + +local function forth_next(context) + local xt = nil + if context.yield then + xt = context.last_xt + context.yield = false + else + local xp = context.xp + xt = read(context, xp) + dbg_print('XP', xp) + context.xp = context.xp + 1 + end + + do_codeword(context, xt) +end + +function forth_tick(context) + if context.sleep_ticks > 0 then + context.sleep_ticks = context.sleep_ticks - 1 + return true + elseif context.yield and context.waiting_key and #context.keybuf == 0 then + return true + else + forth_next(context) + return false + end +end + +function feed_key(context, key) + table.insert(context.keybuf, key) +end + +function feed_file(context, file) + while true do + local c = file:read(1) + if c == nil then break end + feed_key(context, string.byte(c)) + end +end + +function forth_initialize(entity) + -- init_context.valid = false + if init_context.valid then + local new_ctx = copy_context(init_context) + new_ctx.entity = entity + return new_ctx + end + + local link = 0 + local init_here = 0x10 + + local function init_comma(data) + write(init_context, init_here, data) + init_here = init_here + 1 + end + + local function init_comma_string(str) + for i=1, #str do + init_comma(string.byte(str, i, i)) + end + end + + local function defcode(name, flags, callback) + -- log(tostring(init_here)..' '..name) + init_comma(link) + link = init_here - 1 + local lenflag = flags + #name + init_comma(lenflag) + init_comma_string(name) + local xt = init_here + + codewords[cw_index] = callback + codeword_names[cw_index] = name + init_comma(cw_index) -- codeword + init_comma(0) -- doesword + cw_index = cw_index + 1 + + labels[xt] = name + + return xt + end + + local DOCOL = defcode('DOCOL', 0, function(context) + -- XP point to the XT to return to, push that + push_rsp(context, context.xp) + + -- Skip past the codeword and doesword + context.xp = context.last_xt + 2 + -- XP now points to first XT of the called word + end) + local __DOCOL = read(init_context, DOCOL) + + local DOCREATE = defcode('DOCREATE', 0, function(context) + -- Push address of data field (after codeword and doesword) + push(context, context.last_xt + 2) + -- log('docreate '..tonumber(context.last_xt + 2)..' = '..read(context, context.last_xt + 2)) + end) + local __DOCREATE = read(init_context, DOCREATE) + + local DODOES = defcode('DODOES', 0, function(context) + -- XP point to the XT to return to, push that + push_rsp(context, context.xp) + + -- Push address of data field (after codeword and doesword) + push(context, context.last_xt + 2) + -- Skip past the codeword and read doesword + context.xp = read(context, context.last_xt + 1) + -- XP now points to first XT of the DOES> section + end) + local __DODOES = read(init_context, DODOES) + + local function defword(name, flags, xts) + -- log(tostring(init_here)..' '..name) + init_comma(link) + link = init_here - 1 + local lenflag = flags + #name + init_comma(lenflag) + init_comma_string(name) + local xt = init_here + + init_comma(__DOCOL) -- codeword + init_comma(0) -- doesword + for i=1, #xts do + init_comma(xts[i]) + end + + labels[xt] = name + + return xt + end + + local function defvar(name, flags, value) + -- log(tostring(init_here)..' '..name) + init_comma(link) + link = init_here - 1 + local lenflag = flags + #name + init_comma(lenflag) + init_comma_string(name) + local xt = init_here + + init_comma(__DOCREATE) -- codeword + init_comma(0) -- doesword + init_comma(value) -- value on param list + + labels[xt] = name + + return xt + end + + local function readvar(context, xt) + -- Skip over codeword, variable is stored after it + -- log('readvar @ '..tonumber(xt + 2)..': '..read(context, xt + 2)) + return read(context, xt + 2) + end + + local function writevar(context, xt, data) + -- Skip over codeword, variable is stored after it + -- log('writevar @ '..tonumber(xt + 2)..': '..data) + return write(context, xt + 2, data) + end + + local function defconst(name, flags, value) + local entry = defcode(name, flags, function(context) + push(context, value) + end) + + return entry + end + + local DROP = defcode('DROP', 0, function(context) + pop(context) + end) + + local SWAP = defcode('SWAP', 0, function(context) + local a = pop(context) + local b = pop(context) + push(context, a) + push(context, b) + -- print(b, a, '->', a, b) + end) + + local DUP = defcode('DUP', 0, function(context) + local a = peek(context, 0) + push(context, a) + end) + + local OVER = defcode('OVER', 0, function(context) + local a = peek(context, 1) + push(context, a) + end) + + local ROT = defcode('ROT', 0, function(context) + local c = pop(context) + local b = pop(context) + local a = pop(context) + push(context, b) + push(context, c) + push(context, a) + end) + + local NROT = defcode('-ROT', 0, function(context) + local c = pop(context) + local b = pop(context) + local a = pop(context) + push(context, c) + push(context, a) + push(context, b) + end) + + local TWODROP = defcode('2DROP', 0, function(context) + pop(context) + pop(context) + end) + + local TWODUP = defcode('2DUP', 0, function(context) + push(context, peek(context, 1)) + push(context, peek(context, 1)) + end) + + local TWOSWAP = defcode('2SWAP', 0, function(context) + local a1 = pop(context) + local a2 = pop(context) + local b1 = pop(context) + local b2 = pop(context) + push(context, a1) + push(context, a2) + push(context, b1) + push(context, b2) + end) + + local QDUP = defcode('?DUP', 0, function(context) + local a = peek(context, 0) + if a ~= 0 then + push(context, a) + end + end) + + local INCR = defcode('1+', 0, function(context) + local a = pop(context) + push(context, a + 1) + end) + + local DECR = defcode('1-', 0, function(context) + local a = pop(context) + push(context, a - 1) + end) + + local CELLPLUS = defcode('CELL+', 0, function(context) + local a = pop(context) + push(context, a + 1) + end) + + local CELLMINUS = defcode('CELL-', 0, function(context) + local a = pop(context) + push(context, a - 1) + end) + + local ADD = defcode('+', 0, function(context) + local b = pop(context) + local a = pop(context) + push(context, a + b) + end) + + local SUB = defcode('-', 0, function(context) + local b = pop(context) + local a = pop(context) + -- print(a, '-', b, '=', a-b) + push(context, a - b) + end) + + local MUL = defcode('*', 0, function(context) + local b = pop(context) + local a = pop(context) + push(context, a * b) + end) + + local UMDIVMOD = defcode('UM/MOD', 0, function(context) + local b = pop(context) + local a = pop(context) + -- We do some cope + a = bit32.band(a, 0x7FFFFFFF) + b = bit32.band(b, 0x7FFFFFFF) + push(context, a % b) + push(context, math.floor(a / b)) + end) + + local DIVMOD = defcode('/MOD', 0, function(context) + local b = pop(context) + local a = pop(context) + push(context, a % b) + push(context, math.floor(a / b)) + end) + + local EQU = defcode('=', 0, function(context) + local b = pop(context) + local a = pop(context) + if a == b then + push(context, F_TRUE) + else + push(context, F_FALSE) + end + end) + + local NEQU = defcode('<>', 0, function(context) + local b = pop(context) + local a = pop(context) + if a ~= b then + push(context, F_TRUE) + else + push(context, F_FALSE) + end + end) + + local LT = defcode('<', 0, function(context) + local b = pop(context) + local a = pop(context) + if a < b then + push(context, F_TRUE) + else + push(context, F_FALSE) + end + end) + + local GT = defcode('>', 0, function(context) + local b = pop(context) + local a = pop(context) + if a > b then + push(context, F_TRUE) + else + push(context, F_FALSE) + end + end) + + local LE = defcode('<=', 0, function(context) + local b = pop(context) + local a = pop(context) + if a <= b then + push(context, F_TRUE) + else + push(context, F_FALSE) + end + end) + + local GE = defcode('>=', 0, function(context) + local b = pop(context) + local a = pop(context) + if a >= b then + push(context, F_TRUE) + else + push(context, F_FALSE) + end + end) + + local ZEQU = defcode('0=', 0, function(context) + local a = pop(context) + if a == 0 then + push(context, F_TRUE) + else + push(context, F_FALSE) + end + end) + + local ZNEQU = defcode('0<>', 0, function(context) + local a = pop(context) + if a ~= 0 then + push(context, F_TRUE) + else + push(context, F_FALSE) + end + end) + + local ZLT = defcode('0<', 0, function(context) + local a = pop(context) + if a < 0 then + push(context, F_TRUE) + else + push(context, F_FALSE) + end + end) + + local ZGT = defcode('0>', 0, function(context) + local a = pop(context) + if a > 0 then + push(context, F_TRUE) + else + push(context, F_FALSE) + end + end) + + local ZLE = defcode('0<=', 0, function(context) + local a = pop(context) + if a <= 0 then + push(context, F_TRUE) + else + push(context, F_FALSE) + end + end) + + local ZGE = defcode('0>=', 0, function(context) + local a = pop(context) + if a >= 0 then + push(context, F_TRUE) + else + push(context, F_FALSE) + end + end) + + local AND = defcode('AND', 0, function(context) + local b = pop(context) + local a = pop(context) + push(context, bit32.band(a, b)) + end) + + local OR = defcode('OR', 0, function(context) + local b = pop(context) + local a = pop(context) + push(context, bit32.bor(a, b)) + end) + + local XOR = defcode('XOR', 0, function(context) + local b = pop(context) + local a = pop(context) + push(context, bit32.bxor(a, b)) + end) + + local LSL = defcode('LSL', 0, function(context) + local b = pop(context) + local a = pop(context) + push(context, bit32.lshift(a, b)) + end) + + local LSR = defcode('LSR', 0, function(context) + local b = pop(context) + local a = pop(context) + push(context, bit32.rshift(a, b)) + end) + + local INVERT = defcode('INVERT', 0, function(context) + local a = pop(context) + push(context, bit32.bnot(a)) + end) + + local NEGATE = defcode('NEGATE', 0, function(context) + local a = pop(context) + push(context, -a) + end) + + local EXIT = defcode('EXIT', 0, function(context) + context.xp = pop_rsp(context) + end) + + local LIT = defcode('LIT', 0, function(context) + local a = read(context, context.xp) + context.xp = context.xp + 1 + push(context, a) + end) + + local STORE = defcode('!', 0, function(context) + local addr = pop(context) + local data = pop(context) + write(context, addr, data) + -- print(data, addr, '!', read(context, addr)) + end) + + local FETCH = defcode('@', 0, function(context) + local addr = pop(context) + -- print(addr, '@', read(context, addr)) + push(context, read(context, addr)) + end) + + local ADDSTORE = defcode('+!', 0, function(context) + local addr = pop(context) + local amnt = pop(context) + write(context, addr, read(context, addr) + amnt) + end) + + local SUBSTORE = defcode('-!', 0, function(context) + local addr = pop(context) + local amnt = pop(context) + write(context, addr, read(context, addr) - amnt) + end) + + local CMOVE = defcode('CMOVE', 0, function(context) + local len = pop(context) + local dst = pop(context) + local src = pop(context) + + for i=1,len do + write(context, dst + i - 1, read(context, src + i - 1)) + end + end) + + local STATE = defvar('STATE', 0, 0) + local HERE = defvar('HERE', 0, 0) + local LATEST = defvar('LATEST', 0, 0) + local S0 = defvar('S0', 0, init_context.sp) + local BASE = defvar('BASE', 0, 10) + + local R0 = defconst('R0', 0, init_context.rp) + local __F_FALSE = defconst('FALSE', 0, F_FALSE) + local __F_TRUE = defconst('TRUE', 0, F_TRUE) + local __F_IMMED = defconst('F_IMMED', 0, F_IMMED) + local __F_HIDDEN = defconst('F_HIDDEN', 0, F_HIDDEN) + local __F_LENMASK = defconst('F_LENMASK', 0, F_LENMASK) + + local TOR = defcode('>R', 0, function(context) + local a = pop(context) + push_rsp(context, a) + end) + + local FROMR = defcode('R>', 0, function(context) + local a = pop_rsp(context) + push(context, a) + end) + + local RFETCH = defcode('R@', 0, function(context) + local data = read(context, context.rp) + push(context, data) + end) + + local RSTORE = defcode('R!', 0, function(context) + local data = pop(context) + write(context, context.rp, data) + end) + + local RSPFETCH = defcode('RSP@', 0, function(context) + push(context, context.rp) + end) + + local RSPSTORE = defcode('RSP!', 0, function(context) + context.rp = pop(context) + end) + + local RDROP = defcode('RDROP', 0, function(context) + pop_rsp(context) + end) + + local DSPFETCH = defcode('DSP@', 0, function(context) + push(context, context.sp) + end) + + local DSPSTORE = defcode('DSP!', 0, function(context) + context.sp = pop(context) + end) + + local function _key(context) + if #context.keybuf == 0 then + context.waiting_key = true + context.yield = true + return nil + end + context.waiting_key = false + return table.remove(context.keybuf, 1) + end + + local KEY = defcode('KEY', 0, function(context) + local key = _key(context) + if key ~= nil then + context.bs = context.bs..string.char(key) + if key == 10 then + dbg_print('enter is here '..context.bs) + context.bs = '' + end + push(context, key) + end + end) + + local EMIT = defcode('EMIT', 0, function(context) + table.insert(context.outbuf, string.char(pop(context))) + end) + + local BRANCH = defcode('BRANCH', 0, function(context) + context.xp = context.xp + read(context, context.xp) + end) + + local ZBRANCH = defcode('0BRANCH', 0, function(context) + local data = pop(context) + if data == 0 then + context.xp = context.xp + read(context, context.xp) + else + context.xp = context.xp + 1 + end + end) + + local function _comma(context, data) + local here = readvar(context, HERE) + write(context, here, data) + here = here + 1 + writevar(context, HERE, here) + end + + local COMMA = defcode(',', 0, function(context) + local data = pop(context) + _comma(context, data) + end) + + local WORD = defword('WORD', 0, { -- ( delim -- paddr) + HERE, FETCH, TOR, -- ( delim ) ( r: start ) + LIT, 0, COMMA, -- Store temp length + TOR, -- ( ) ( r: start delim ) + -- Skip leading delimiters + KEY, -- ( key ) ( r: start delim ) + DUP, RFETCH, EQU, -- ( key delim ) ( r: start delim ) + ZBRANCH, 4, -- ( key ) ( r: start delim ) + DROP, -- ( ) ( r: start delim ) + BRANCH, -8, -- Branch till no more delim's + -- Check for /n or delim + DUP, RFETCH, EQU, -- ( key not_delim ) Check for delim + OVER, LIT, 10, EQU, -- ( key not_delim not_nl ) Check for \n + OR, ZBRANCH, 15, -- ( key ) ( r: start delim ) If delim or space then + FROMR, TWODROP, -- ( ) ( r: start ) + RFETCH, -- ( start ) ( r: start ) + HERE, FETCH, FROMR, SUB, DECR, -- ( start len ) ( r: ) + OVER, STORE, -- ( start ) ( r: ) - Store length + DUP, HERE, STORE, -- Restore HERE + EXIT, + -- Store key and grab the next one + COMMA, -- Store key ( ) ( r: start delim ) + KEY, -- ( key ) ( r: start delim) + BRANCH, -27, -- Branch till invalid key (space, \n, etc) + }) + + local function _number(context, accum, addr, len) + local str = '' + for i=1, len do + str = str..string.char(read(context, addr + i - 1)) + end + + local start = 1 + if string.sub(str, 1, 1) == '-' then start = 2 end + + local result = accum + local last = 0 + for i=start, len do + local tmp = tonumber(string.sub(str, 1, i), readvar(context, BASE)) + if tmp == nil then break end + result = tmp + last = i + end + + return result, addr + last, len - last + end + + local NUMBER = defcode('>NUMBER', 0, function(context) + local len = pop(context) + local addr = pop(context) + local accum = pop(context) + local num, rem_addr, remaining = _number(context, accum, addr, len) + push(context, num) + push(context, rem_addr) + push(context, remaining) + end) + + local function _find(context, paddr) + local len = read(context, paddr) + local addr = paddr + 1 + local current = readvar(context, LATEST) + + while true do + if current == 0 then + return 0 end + + local cur_len = bit32.band(read(context, current + 1), bit32.bor(F_HIDDEN, F_LENMASK)) + -- game.print('current='..tostring(current)..' cur_len='..tostring(cur_len)..' len='..tostring(len)) + local a = '' + local b = '' + if cur_len == len then + local match = true + for i=1, len do + -- +2 to skip link/flags, -1 because lua | addr is just text so only -1 because lua + a = a..string.char(read(context, current + i + 1)) + b = b..string.char(read(context, addr + i - 1)) + if read(context, current + i + 1) ~= read(context, addr + i - 1) then + match = false + break + end + end + if match then + return current + end + end + current = read(context, current) + end + end + + local FIND = defcode('FIND', 0, function(context) + local paddr = pop(context) + push(context, _find(context, paddr)) + end) + + local function _tcfa(context, addr) + local len = bit32.band(read(context, addr+1), F_LENMASK) + return addr + len + 2 + end + + -- ( entry -- cfa) + local TCFA = defcode('>CFA', 0, function(context) + local addr = pop(context) + push(context, _tcfa(context, addr)) + end) + + -- ( entry -- does) + local TDOES = defcode('>DOES', 0, function(context) + local addr = pop(context) + push(context, _tcfa(context, addr) + 1) + end) + + -- ( entry -- dfa) + local TDFA = defcode('>DFA', 0, function(context) + local addr = pop(context) + push(context, _tcfa(context, addr) + 2) + end) + + -- ( xt -- dfa) + local TBODY = defcode('>BODY', 0, function(context) + local xt = pop(context) + push(context, xt + 2) + end) + + local ALLOT = defword('ALLOT', 0, { -- ( n -- ) + HERE, ADDSTORE, -- ( ) + EXIT + }) + + -- Our cell size is 1 so do nothing /shrug + local CELLS = defword('CELLS', 0, { + EXIT + }) + + local COUNT = defword('COUNT', 0, { + DUP, FETCH, SWAP, INCR, SWAP, + EXIT + }) + + local BL = defword('BL', 0, { + LIT, string.byte(' '), + EXIT + }) + + local LBRAC = defcode('[', F_IMMED, function(context) + -- print('SET STATE EXEC') + writevar(context, STATE, 0) + -- print('STATE ', readvar(context, STATE)) + end) + + local RBRAC = defcode(']', 0, function(context) + -- print('SET STATE COMP') + writevar(context, STATE, 1) + -- print('STATE ', readvar(context, STATE)) + end) + + local IMMEDIATE = defcode('IMMEDIATE', F_IMMED, function(context) + local latest = readvar(context, LATEST) + + local flags = read(context, latest + 1) + flags = bit32.bxor(flags, F_IMMED) + write(context, latest + 1, flags) + end) + + local HIDDEN = defcode('HIDDEN', 0, function(context) + local entry = pop(context) + + local flags = read(context, entry + 1) + flags = bit32.bxor(flags, F_HIDDEN) + write(context, entry + 1, flags) + end) + + local HEADERCOMMA = defword('HEADER,', 0, { + HERE, FETCH, -- ( HERE ) ( c: ) + LATEST, FETCH, COMMA, -- ( HERE ) ( c: LATEST ) + LATEST, STORE, -- ( ) ( c: LATEST ) + BL, WORD, COUNT, INCR, ALLOT, DROP, -- ( ) ( c: LATEST WORD ) Technically should align here + EXIT + }) + + local CREATE = defword('CREATE', 0, { + HEADERCOMMA, + LIT, __DOCREATE, COMMA, + LIT, 0, COMMA, + EXIT + }) + + + local DOES = defword('(DOES>)', 0, { + FROMR, -- ( RP ) Steal the return address (points to target code) + LATEST, FETCH, TCFA, -- ( RP CFA ) Jump to CFA of LATEST word + DUP, LIT, __DODOES, SWAP, STORE, -- ( RP CFA ) Swap docreate for dodoes + INCR, STORE, -- ( ) Store RP in doesword + EXIT, + }) + + local COLON = defword(':', 0, { + HEADERCOMMA, + LIT, __DOCOL, COMMA, + LIT, 0, COMMA, + LATEST, FETCH, HIDDEN, + RBRAC, + EXIT + }) + + local SEMICOLON = defword(';', F_IMMED, { + LIT, EXIT, COMMA, + LATEST, FETCH, HIDDEN, + LBRAC, + EXIT, + }) + + -- Needed to bootstrap LITERAL/['] dependency loop + local COMPTICK = defcode('COMP\'', 0, function(context) + local xt = read(context, context.xp) + context.xp = context.xp + 1 + push(context, xt) + end) + + local LITSTRING = defcode('LITSTRING', 0, function(context) + local len = read(context, context.xp) + context.xp = context.xp + 1 + + local addr = context.xp + + context.xp = context.xp + len + + push(context, addr) + push(context, len) + end) + + local TELL = defcode('TELL', 0, function(context) + local len = pop(context) + local addr = pop(context) + + -- local str = '' + for i=1, len do + -- str = str..string.char(read(context, addr + i - 1)) + table.insert(context.outbuf, string.char(read(context, addr + i - 1))) + end + + -- game.print(str) + end) + + local INTERPRET = defcode('INTERPRET', 0, function(context) + local paddr = pop(context) + + local is_lit = false + local num = nil + + local entry = _find(context, paddr) + + local xt = nil + if entry == 0 then + dbg_print('Not found') + -- Literal? + is_lit = true + local remaining = nil + local len = read(context, paddr) + local addr = paddr + 1 + num, _, remaining = _number(context, 0, addr, len) + if remaining ~= 0 then + -- Not a number + context.keybuf = {} + dbg_interp('ERR!') + output_string(context, 'err!\n') + return + else + dbg_interp('literal') + xt = LIT + end + else + dbg_print('entry=', entry) + xt = _tcfa(context, entry) + dbg_print('xt=', xt) + local flags = read(context, entry + 1) + if bit32.band(flags, F_IMMED) > 0 then + -- Execute XT + dbg_interp('IMMEDIATE EXEC') + local codeword = read(context, xt) + dbg_print('XT', xt, labels[xt]) + dbg_print('CW', codeword, codeword_names[codeword]) + do_codeword(context, xt) + return + end + end + -- Word or literal + -- print('STATE ', readvar(context, STATE)) + if readvar(context, STATE) == 0 then + -- Executing + if is_lit then + dbg_interp('push '..tostring(num)) + push(context, num) + else + -- Execute XT + dbg_interp('INTERPRET EXEC') + local codeword = read(context, xt) + dbg_interp('XT '..tostring(xt)..' '..tostring(labels[xt])) + dbg_interp('CW '..tostring(codeword)..' '..tostring(codeword_names[codeword])) + do_codeword(context, xt) + end + else + -- Compiling + dbg_interp('comma '..tostring(xt)) + _comma(context, xt) + if is_lit then + dbg_interp('comma '..tostring(num)) + _comma(context, num) + end + end + end) + + local OK = defcode('OK', 0, function(context) + -- game.print('ok') + output_string(context, 'ok\n') + end) + + local QUIT = defword('QUIT', 0, { + R0, RSPSTORE, + BL, WORD, + DUP,FETCH,ZGT,ZBRANCH,4, + INTERPRET, + BRANCH,2, + DROP, + OK, + BRANCH,-13, + EXIT, + }) + + local EXECUTE = defcode('EXECUTE', 0, function(context) + local xt = pop(context) + + local cw = read(context, xt) + dbg_print('EXECUTE EXEC') + do_codeword(context, xt) + end) + + local ITEMS = defcode('ITEMS', 0, function(context) + for id, name in pairs(sigs.item) do + output_string(context, id..' '..name..'\n') + end + end) + + local FLUIDS = defcode('FLUIDS', 0, function(context) + for id, name in pairs(sigs.fluid) do + output_string(context, id..' '..name..'\n') + end + end) + + local SIGNALS = defcode('SIGNALS', 0, function(context) + for id, name in pairs(sigs.virtual_signal) do + output_string(context, id..' '..name..'\n') + end + end) + + local NSIGS = defcode('#SLOTS', 0, function(context) + local control = context.entity.get_or_create_control_behavior() + local section = control.get_section(1) + push(context, section.filters_count) + end) + + local LIST_SIGS = defcode('LIST-SLOTS', 0, function(context) + local control = context.entity.get_or_create_control_behavior() + local section = control.get_section(1) + local filters = section.filters + end) + + local function id_for_signal(type, name) + -- Factorio moment + if type == 'virtual' then type = 'virtual_signal' end + for i, sname in pairs(sigs[type]) do + if sname == name then return i end + end + return -1 + end + + local SLOTFETCH = defcode('SLOT@', 0, function(context) + local control = context.entity.get_or_create_control_behavior() + local section = control.get_section(1) + local filters = section.filters + + local index = pop(context) + + if index > section.filters_count then + error('Invalid slot index') + end + + local filter = filters[index] + push(context, id_for_signal(filter.value.type, filter.value.name)) + push(context, filter.min) + end) + + function set_slot(context, type) + local control = context.entity.get_or_create_control_behavior() + control.enabled = true + local section = control.get_section(1) + section.active = true + local filters = section.filters + + local value = pop(context) + local id = pop(context) + local index = pop(context) + + local sig_list = sigs[type] + if id > #sig_list then + error('Invalid signal ID') + end + local name = sig_list[id] + + -- More factorio cope + if type == 'virtual_signal' then type = 'virtual' end + local new_filter = { min = value, ['value'] = { comparator = '=', name = name, quality = 'normal', type = type } } + filters[index] = new_filter + section.filters = filters + end + + local ITEMSTORE = defcode('ITEM!', 0, function(context) + set_slot(context, 'item') + end) + + local FLUIDSTORE = defcode('FLUID!', 0, function(context) + set_slot(context, 'fluid') + end) + + local SIGNALSTORE = defcode('SIGNAL!', 0, function(context) + set_slot(context, 'virtual_signal') + end) + + local SLEEP = defcode('SLEEP', 0, function(context) + local ms = pop(context) + context.sleep_ticks = math.floor(ms * 60.0 / 1000.0) + end) + + local DICTEND = defword('DICTEND', 0, {QUIT}) + + write(init_context, 0, QUIT) + + init_context.xp = 0 + writevar(init_context, HERE, init_here) + writevar(init_context, LATEST, link) + + -- Bake the base forth code + for c in base_fth:gmatch('.') do + table.insert(init_context.keybuf, string.byte(c)) + end + table.insert(init_context.keybuf, 10) + + while true do + local waiting = forth_tick(init_context) + if waiting then break end + end + + -- log('End bake') + -- for i, v in pairs(init_context.memspace) do + -- log(tostring(i)..' '..tostring(v)) + -- end + + init_context.outbuf = {} + + init_context.valid = true + + local new_ctx = copy_context(init_context) + new_ctx.entity = entity + return new_ctx +end + +function pprint(thing) + for i, v in pairs(thing) do + print(i, v) + end +end + +function pc(ctx) + print(ctx.xp, labels[read(ctx, ctx.xp)]) +end + +-- local f = io.open('base.fth', 'r') +-- if f ~= nil then +-- feed_file(ctx, f) +-- f:close() +-- end + +function e() + while true do + local waiting = forth_tick(ctx) + if waiting then + local key = string.byte(io.read(1)) + dbg_print('feed ', key, string.char(key)) + feed_key(ctx, key) + end + end +end diff --git a/info.json b/info.json new file mode 100644 index 0000000..a77baea --- /dev/null +++ b/info.json @@ -0,0 +1,9 @@ +{ + "name": "forthCombinator", + "version": "0.1.0", + "title": "Forth Combinator", + "author": "UrMomLel", + "factorio_version": "2.0", + "dependencies": ["base >= 2.0"], + "description": "This game needs computers and those computers need to be running forth" +} diff --git a/locale/en/locale.cfg b/locale/en/locale.cfg new file mode 100644 index 0000000..67aef3a --- /dev/null +++ b/locale/en/locale.cfg @@ -0,0 +1,9 @@ + +[item-name] +forth-combinator=forth combinator + +[entity-name] +forth-combinator=forth combinator + +[item-description] +forth-combinator=This game needs computers and those computers need to run Forth