Skip to content

Instantly share code, notes, and snippets.

@MCJack123
Last active October 24, 2024 22:20
Show Gist options
  • Save MCJack123/94071c7724045dc048777395afc04eb1 to your computer and use it in GitHub Desktop.
Save MCJack123/94071c7724045dc048777395afc04eb1 to your computer and use it in GitHub Desktop.
DSL for Create train schedules
--[[
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
@tizu69
Copy link

tizu69 commented Oct 24, 2024

can this run doom?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment