Last active
October 24, 2024 22:20
-
-
Save MCJack123/94071c7724045dc048777395afc04eb1 to your computer and use it in GitHub Desktop.
DSL for Create train schedules
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Train schedule DSL by JackMacWindows | |
Example: | |
cyclic | |
to "Station 1" { | |
wait 5m, powered | |
time = 14:00 daily | |
} | |
Can also be represented like this: | |
cyclic to "Station 1" { wait 5m powered; time = 14 24h } | |
CC0 license | |
]] | |
local function lex(str) | |
local state = 0 | |
local partial = "" | |
local tokens = {} | |
for c in str:gmatch "." do | |
if state == 0 then -- whitespace | |
if c:match "[%a_]" then | |
partial = c | |
state = 1 | |
elseif c:match "%d" then | |
partial = c | |
state = 2 | |
elseif c:match "[<=>{},;]" then | |
tokens[#tokens+1] = {type = "symbol", text = c} | |
elseif c == '"' then | |
state = 3 | |
elseif c == '\n' then | |
tokens[#tokens+1] = {type = "newline", text = "\n"} | |
elseif not c:match "%s" then | |
error("unexpected '" .. c .. "'", 0) | |
end | |
elseif state == 1 then -- token | |
if c:match "[%w_]" then | |
partial = partial .. c | |
elseif c:match "%s" then | |
tokens[#tokens+1] = {type = "token", text = partial} | |
if c == "\n" then tokens[#tokens+1] = {type = "newline", text = "\n"} end | |
partial = "" | |
state = 0 | |
elseif c:match "[<=>{},;]" then | |
tokens[#tokens+1] = {type = "token", text = partial} | |
tokens[#tokens+1] = {type = "symbol", text = c} | |
state = 0 | |
else error("unexpected '" .. c .. "'", 0) end | |
elseif state == 2 then -- number | |
if c:match "%d" then | |
partial = partial .. c | |
elseif (c == ":" or c == ".") and not partial:find(c) then | |
partial = partial .. c | |
elseif c:match "%a" then | |
tokens[#tokens+1] = {type = "number", text = partial, unit = c} | |
state = 4 | |
elseif c:match "[<=>{},;]" then | |
tokens[#tokens+1] = {type = "number", text = partial} | |
tokens[#tokens+1] = {type = "symbol", text = c} | |
elseif c:match "%s" then | |
tokens[#tokens+1] = {type = "number", text = partial} | |
if c == "\n" then tokens[#tokens+1] = {type = "newline", text = "\n"} end | |
state = 0 | |
else error("unexpected '" .. c .. "'", 0) end | |
elseif state == 3 then -- quote | |
if c == '\\' then | |
state = 5 | |
elseif c == '"' then | |
tokens[#tokens+1] = {type = "string", text = partial} | |
state = 0 | |
else partial = partial .. c end | |
elseif state == 4 then -- number end (must be whitespace) | |
if not c:match "%s" then error("unexpected '" .. c .. "'", 0) end | |
if c == "\n" then tokens[#tokens+1] = {type = "newline", text = "\n"} | |
elseif c:match "[<=>{},;]" then tokens[#tokens+1] = {type = "symbol", text = c} end | |
state = 0 | |
elseif state == 5 then -- quote escape | |
if c == "n" then partial = partial .. "\n" | |
elseif c == "t" then partial = partial .. "\t" | |
else partial = partial .. c end | |
state = 3 | |
end | |
end | |
if state == 1 then tokens[#tokens+1] = {type = "token", text = partial} | |
elseif state == 2 then tokens[#tokens+1] = {type = "number", text = partial} | |
elseif state == 3 or state == 5 then error("unfinished string at eof", 0) end | |
return tokens | |
end | |
local conditions = {} | |
local delay_units = {["t"] = 0, ["s"] = 1, ["m"] = 2} | |
function conditions.wait(next) | |
local time = next(false, "number") | |
time.unit = time.unit or "s" | |
return { | |
id = "create:delay", | |
data = { | |
value = assert(tonumber(time.text), "invalid number near " .. time.text), | |
time_unit = assert(time.unit, "invalid unit near " .. time.text .. time.unit) | |
} | |
} | |
end | |
local time_rotations = {["24h"] = 0, ["12h"] = 1, ["6h"] = 2, ["4h"] = 3, ["3h"] = 4, ["2h"] = 5, ["1h"] = 6, ["45m"] = 7, ["30m"] = 8, ["15m"] = 9} | |
function conditions.time(next) | |
local eq = next() | |
if eq.type ~= "symbol" or eq.text ~= "=" then error("expected = near " .. eq.text, 0) end | |
local time = next(false, "number") | |
local hour, min | |
if time.text:find ":" then hour, min = time.text:match "([^:]*):(.*)" | |
else hour, min = time.text, 0 end | |
hour, min = assert(tonumber(hour), "invalid hour"), assert(tonumber(min), "invalid minute") | |
if hour < 0 or hour > 23 then error("invalid hour", 0) end | |
if min < 0 or min > 59 then error("invalid minute", 0) end | |
local rot = next() | |
local rotn | |
if rot.type == "token" and rot.text == "daily" then rotn = 0 | |
elseif rot.type == "token" and rot.text == "hourly" then rotn = 6 | |
else | |
assert(rot.type == "number" and rot.unit, "invalid rotation value near " .. rot.text) | |
rotn = assert(time_rotations[rot.text .. rot.unit], "invalid rotation value near " .. rot.text .. rot.unit) | |
end | |
return { | |
id = "create:time_of_day", | |
data = { | |
hour = hour, | |
minute = min, | |
rotation = rotn | |
} | |
} | |
end | |
local compare_ops = {[">"] = 0, ["<"] = 1, ["="] = 2} | |
function conditions.fluid(next) | |
local name = next(false, "string") | |
local op = next(false, "symbol") | |
local threshold = next(false, "number") | |
local thresholdn = assert(tonumber(threshold.text), "invalid number near " .. threshold.text) | |
threshold.unit = threshold.unit or "b" | |
if threshold.unit ~= "b" then error("invalid unit near " .. threshold.text .. threshold.unit) end | |
return { | |
id = "create:fluid_threshold", | |
data = { | |
bucket = { | |
id = name.text, | |
count = 1 | |
}, | |
threshold = thresholdn, | |
operator = assert(compare_ops[op.text], "invalid comparison operator near " .. op.text), | |
measure = 0 | |
} | |
} | |
end | |
local item_measures = {["i"] = 0, ["s"] = 1} | |
function conditions.item(next) | |
local name = next(false, "string") | |
local op = next(false, "symbol") | |
local threshold = next(false, "number") | |
local thresholdn = assert(tonumber(threshold.text), "invalid number near " .. threshold.text) | |
threshold.unit = threshold.unit or "i" | |
return { | |
id = "create:item_threshold", | |
data = { | |
item = { | |
id = name.text, | |
count = 1 | |
}, | |
threshold = thresholdn, | |
operator = assert(compare_ops[op.text], "invalid comparison operator near " .. op.text), | |
measure = assert(item_measures[threshold.unit], "invalid unit near " .. threshold.text .. threshold.unit) | |
} | |
} | |
end | |
function conditions.players(next) | |
local op = next(false, "symbol") | |
if op.text ~= ">" and op.text ~= "=" then error("invalid comparison operator near " .. op.text, 0) end | |
local count = next(false, "number") | |
local n = assert(tonumber(count.text), "invalid number near " .. count.text) | |
if n < 0 then error("invalid player count near " .. n, 0) end | |
return { | |
id = "create:player_count", | |
data = { | |
count = n, | |
exact = op.text == "=" and 0 or 1 | |
} | |
} | |
end | |
function conditions.idle(next) | |
local time = next(false, "number") | |
time.unit = time.unit or "s" | |
return { | |
id = "create:idle", | |
data = { | |
value = assert(tonumber(time.text), "invalid number near " .. time.text), | |
time_unit = assert(time.unit, "invalid unit near " .. time.text .. time.unit) | |
} | |
} | |
end | |
function conditions.unloaded() return {id = "create:unloaded", data = {}} end | |
function conditions.powered() return {id = "create:powered", data = {}} end | |
local function to(next) | |
local dest = next() | |
assert(dest.type == "string", "expected string near " .. dest.text) | |
local bracket = next() | |
assert(bracket.type == "symbol" and bracket.text == "{", "expected '{' near " .. dest.text) | |
local entry = { | |
instruction = { | |
id = "create:destination", | |
data = { | |
text = dest.text | |
} | |
}, | |
conditions = {} | |
} | |
local andl = {} | |
while true do | |
local cond = next(true) | |
if cond.type == "newline" or (cond.type == "symbol" and cond.text == ";") then | |
if _G.next(andl) then entry.conditions[#entry.conditions+1] = andl end | |
andl = {} | |
elseif cond.type == "symbol" and cond.text == "," then | |
elseif cond.type == "symbol" and cond.text == "}" then | |
break | |
elseif cond.type == "token" then | |
if not conditions[cond.text] then error("unexpected " .. cond.text, 0) end | |
andl[#andl+1] = conditions[cond.text](next) | |
else error("unexpected " .. cond.text, 0) end | |
end | |
if next(andl) then entry.conditions[#entry.conditions+1] = andl end | |
return entry | |
end | |
local function parse(tokens) | |
local schedule = {entries = {}} | |
local pos = 1 | |
local function next(nl, typ) | |
local a | |
repeat | |
a = tokens[pos] | |
pos = pos + 1 | |
if not a then error("unexpected eof", 0) end | |
until nl or a.type ~= "newline" | |
if typ and a.type ~= typ then error("expected " .. typ .. " near " .. a.text, 0) end | |
return a | |
end | |
while true do | |
if not tokens[pos] then break end | |
local tok = next(false, "token") | |
if tok.text == "cyclic" then schedule.cyclic = true | |
elseif tok.text == "to" then | |
schedule.entries[#schedule.entries+1] = to(next) | |
elseif tok.text == "rename" then | |
local name = next(false, "string") | |
schedule.entries[#schedule.entries+1] = { | |
instruction = "create:rename", | |
data = { | |
text = name.text | |
} | |
} | |
elseif tok.text == "throttle" then | |
local num = next(false, "number") | |
local n = assert(tonumber(num.text), "invalid number near " .. num.text) | |
if n < 5 or n > 100 then error("invalid throttle value " .. n, 0) end | |
schedule.entries[#schedule.entries+1] = { | |
instruction = "create:throttle", | |
data = { | |
value = n | |
} | |
} | |
else error("unexpected " .. tok.text, 0) end | |
end | |
return schedule | |
end | |
return function(str) return parse(lex(str)) end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
can this run doom?