651 lines
18 KiB
Lua

do
-- don't load if sim scenario has already loaded this (in another lua state)
local modloader = remote.interfaces["modloader"]
if modloader and modloader[script.mod_name] then
return
end
end
---@class NixieGlobal
---@field alphas {[integer]:LuaEntity?}
---@field next_alpha? integer
---@field controllers {[integer]:LuaEntity?}
---@field next_controller? integer
---@field nextdigit {[integer]:LuaEntity?}
---@field cache {[integer]:NixieCache?}
global = {}
---@class NixieCache
---@field control? LuaLampControlBehavior
---@field lastvalue? integer
---@field lastcolor Color[]
---@field sprites integer[] rendering sprite IDs
---@param unit_number integer
---@return NixieCache
local function getCache(unit_number)
local cache = global.cache[unit_number]
if not cache then
cache = {
lastcolor = {},
sprites = {},
}
global.cache[unit_number] = cache
end
return cache
end
local validEntityName = {
['nixie-tube'] = 1,
['nixie-tube-alpha'] = 1,
['nixie-tube-small'] = 2
}
local signalCharMap = {
["signal-0"] = "0",
["signal-1"] = "1",
["signal-2"] = "2",
["signal-3"] = "3",
["signal-4"] = "4",
["signal-5"] = "5",
["signal-6"] = "6",
["signal-7"] = "7",
["signal-8"] = "8",
["signal-9"] = "9",
["signal-A"] = "A",
["signal-B"] = "B",
["signal-C"] = "C",
["signal-D"] = "D",
["signal-E"] = "E",
["signal-F"] = "F",
["signal-G"] = "G",
["signal-H"] = "H",
["signal-I"] = "I",
["signal-J"] = "J",
["signal-K"] = "K",
["signal-L"] = "L",
["signal-M"] = "M",
["signal-N"] = "N",
["signal-O"] = "O",
["signal-P"] = "P",
["signal-Q"] = "Q",
["signal-R"] = "R",
["signal-S"] = "S",
["signal-T"] = "T",
["signal-U"] = "U",
["signal-V"] = "V",
["signal-W"] = "W",
["signal-X"] = "X",
["signal-Y"] = "Y",
["signal-Z"] = "Z",
["signal-negative"] = "negative",
--extended symbols
["signal-stop"] = "dot",
["signal-qmark"]="?",
["signal-exmark"]="!",
["signal-at"]="@",
["signal-sqopen"]="[",
["signal-sqclose"]="]",
["signal-curopen"]="{",
["signal-curclose"]="}",
["signal-paropen"]="(",
["signal-parclose"]=")",
["signal-slash"]="slash",
["signal-asterisk"]="*",
["signal-minus"]="-",
["signal-plus"]="+",
["signal-percent"]="%",
}
local function RegisterStrings()
if remote.interfaces['signalstrings'] and remote.interfaces['signalstrings']['register_signal'] then
local syms = {
["signal-stop"] = ".",
["signal-qmark"]="?",
["signal-exmark"]="!",
["signal-at"]="@",
["signal-sqopen"]="[",
["signal-sqclose"]="]",
["signal-curopen"]="{",
["signal-curclose"]="}",
["signal-paropen"]="(",
["signal-parclose"]=")",
["signal-slash"]="/",
["signal-asterisk"]="*",
["signal-minus"]="-",
["signal-plus"]="+",
["signal-percent"]="%",
}
for name,char in pairs(syms) do
remote.call('signalstrings','register_signal',name,char)
end
end
end
--sets the state(s) and update the sprite for a nixie
local is_simulation = script.level.is_simulation
---@param nixie LuaEntity
---@param cache NixieCache
---@param newstates string[]
---@param newcolor? Color
local function setStates(nixie,cache,newstates,newcolor)
for key,new_state in pairs(newstates) do
if not new_state then new_state = "off" end
-- printing floats sometimes hands us a literal '.', needs to be renamed
if new_state == '.' then new_state = "dot" end
local obj = cache.sprites[key]
if not (obj and rendering.is_valid(obj)) then
cache.lastcolor[key] = nil
local num = validEntityName[nixie.name]
---@type Vector.0
local position
if num == 1 then -- large tube, one sprite
position = {x=1/32, y=1/32}
else
position = {x=-9/64+((key-1)*20/64), y=3/64} -- sprite offset
end
obj = rendering.draw_sprite{
sprite = "nixie-tube-sprite-" .. new_state,
target = nixie,
target_offset = position,
surface = nixie.surface,
tint = {r=1.0, g=1.0, b=1.0, a=1.0},
x_scale = 1/num,
y_scale = 1/num,
render_layer = "object",
}
cache.sprites[key] = obj
end
if nixie.energy > 70 or is_simulation then
rendering.set_sprite(obj,"nixie-tube-sprite-" .. new_state)
local color = newcolor
if not color then color = {r=1.0, g=0.6, b=0.2, a=1.0} end
if new_state == "off" then color={r=1.0, g=1.0, b=1.0, a=1.0} end
if not (cache.lastcolor[key] and (cache.lastcolor[key].r == color.r) and (cache.lastcolor[key].g == color.g) and (cache.lastcolor[key].b == color.b) and (cache.lastcolor[key].a == color.a)) then
cache.lastcolor[key] = color
rendering.set_color(obj,color)
end
else
if rendering.get_sprite(obj) ~= "nixie-tube-sprite-off" then
rendering.set_sprite(obj,"nixie-tube-sprite-off")
end
rendering.set_color(obj,{r=1.0, g=1.0, b=1.0, a=1.0})
cache.lastcolor[key] = nil
end
end
end
---@param behavior LuaLampControlBehavior
---@return SignalID?
local function get_selected_signal(behavior)
if behavior == nil then
return nil
end
local condition = behavior.circuit_condition
if condition == nil then
return nil
end
local signal = condition.condition.first_signal
if signal and not condition.fulfilled then
-- use >= MININT32 to ensure always-on
condition.condition.comparator=""
condition.condition.constant=-0x80000000
condition.condition.second_signal=nil
behavior.circuit_condition = condition
end
return signal
end
---@param filters {[any]:SignalID?}
---@param entity LuaEntity
---@return {[any]:integer?}
local function get_signals_filtered(filters,entity)
local red = entity.get_circuit_network(defines.wire_type.red)
local green = entity.get_circuit_network(defines.wire_type.green)
---@type {[any]:integer}
local results = {}
if not red and not green then return results end
for i,f in pairs(filters) do
results[i] = 0
if f.name then
if red then
results[i] = results[i] + red.get_signal(f)
end
if green then
results[i] = results[i] + green.get_signal(f)
end
end
end
return results
end
---@param entity LuaEntity
---@param vs? string
---@param color? Color
local function displayValString(entity,vs,color)
local offset = vs and #vs or 0
while entity do
local nextdigit = global.nextdigit[entity.unit_number]
local cache = getCache(entity.unit_number)
local chcount = #cache.sprites
if not vs then
setStates(entity,cache,(chcount==1) and {"off"} or {"off","off"})
elseif offset < chcount then
setStates(entity,cache,{"off",vs:sub(offset,offset)},color)
elseif offset >= chcount then
setStates(entity,cache,
(chcount==1) and
{vs:sub(offset,offset)} or
{vs:sub(offset-1,offset-1),vs:sub(offset,offset)}
,color)
end
if nextdigit then
if nextdigit.valid then
if offset>chcount then
offset = offset-chcount
else
vs = nil
end
else
--when a nixie in the middle is removed, it doesn't have the unit_number to it's right to remove itself
global.nextdigit[entity.unit_number] = nil
nextdigit = nil
end
end
---@diagnostic disable-next-line:cast-local-type
entity = nextdigit
end
end
---@param i integer
---@return float
local function float_from_int(i)
local sign = bit32.btest(i,0x80000000) and -1 or 1
local exponent = bit32.rshift(bit32.band(i,0x7F800000),23)-127
local significand = bit32.band(i,0x007FFFFF)
if exponent == 128 then
if significand == 0 then
return sign/0 --[[infinity]]
else
return 0/0 --[[nan]]
end
end
if exponent == -127 then
if significand == 0 then
return sign * 0 --[[zero]]
else
return sign * math.ldexp(significand,-149) --[[denormal numbers]]
end
end
return sign * math.ldexp(bit32.bor(significand,0x00800000),exponent-23) --[[normal numbers]]
end
---@param entity LuaEntity
---@return string?
local function getAlphaSignals(entity)
local signals = entity.get_merged_signals()
---@type string
local ch
if signals and #signals > 0 then
for _,s in pairs(signals) do
if signalCharMap[s.signal.name] then
if ch then
return "err"
else
ch = signalCharMap[s.signal.name]
end
end
end
end
return ch
end
---@type SignalID
local sigFloat = {name="signal-float",type="virtual"}
---@type SignalID
local sigHex = {name="signal-hex",type="virtual"}
---@param entity LuaEntity
---@param cache NixieCache
local function onTickController(entity,cache)
local control = cache.control
if not (control and control.valid) then
control = entity.get_or_create_control_behavior() --[[@as LuaLampControlBehavior]]
cache.control = control
end
local sigdata = get_signals_filtered( {float = sigFloat, hex = sigHex, v = get_selected_signal(control) }, entity)
local v = sigdata.v or 0
if cache.lastvalue ~= v or cache.control.use_colors then
cache.lastvalue = v
local float = sigdata.float
float = float and float ~= 0 ---@diagnostic disable-line:cast-local-type
local hex = sigdata.hex
hex = hex and hex ~= 0 ---@diagnostic disable-line:cast-local-type
local format = "%i"
if float and hex then
format = "%A"
v = float_from_int(v)
elseif hex then
format = "%X"
if v < 0 then v = v + 0x100000000 end
elseif float then
format = "%G"
v = float_from_int(v)
end
displayValString(entity,format:format(v),control.use_colors and control.color or nil)
end
end
local always_on = {
condition={
first_signal={name="signal-anything",type="virtual"},
comparator="",
constant=0,
second_signal=nil
},
connect_to_logistic_network=false
}
---@param entity LuaEntity
---@param cache NixieCache
local function onTickAlpha(entity,cache)
local charsig = getAlphaSignals(entity) or "off"
---@type Color?
local color
local control = cache.control
if not (control and control.valid) then
control = entity.get_or_create_control_behavior() --[[@as LuaLampControlBehavior]]
cache.control = control
end
if control.use_colors then
control.circuit_condition = always_on
color = control.color
end
setStates(entity,cache,{charsig},color)
end
local function onTick()
for _=1, settings.global["nixie-tube-update-speed-numeric"].value do
---@type LuaEntity?
local nixie
if global.next_controller and not global.controllers[global.next_controller] then
global.next_controller=nil
end
global.next_controller,nixie = next(global.controllers,global.next_controller)
if nixie then
if nixie.valid then
onTickController(nixie,getCache(global.next_controller))
else
log("cleaning up nixie tube " .. global.next_controller .. " destroyed without events")
global.controllers[global.next_controller] = nil
global.cache[global.next_controller] = nil
global.next_controller = nil
end
end
end
for _=1, settings.global["nixie-tube-update-speed-alpha"].value do
---@type LuaEntity?
local nixie
if global.next_alpha and not global.alphas[global.next_alpha] then
global.next_alpha=nil
end
global.next_alpha,nixie = next(global.alphas,global.next_alpha)
if nixie then
if nixie.valid then
onTickAlpha(nixie, getCache(global.next_alpha))
else
log("cleaning up nixie tube " .. global.next_alpha .. " destroyed without events")
global.alphas[global.next_alpha] = nil
global.cache[global.next_alpha] = nil
global.next_alpha = nil
end
end
end
end
---@param entity LuaEntity
local function onPlaceEntity(entity)
local num = validEntityName[entity.name]
if num then
local surf=entity.surface
local cache = getCache(entity.unit_number)
local sprites = cache.sprites
for n=1, num do
--place the /real/ thing(s) at same spot
---@type Vector
local position
if num == 1 then -- large tube, one sprite
position = {x=1/32, y=1/32}
else
position = {x=-9/64+((n-1)*20/64), y=3/64} -- sprite offset
end
local sprite= rendering.draw_sprite{
sprite = "nixie-tube-sprite-off",
target = entity,
target_offset = position,
surface = entity.surface,
tint = {r=1.0, g=1.0, b=1.0, a=1.0},
x_scale = 1/num,
y_scale = 1/num,
render_layer = "object",
}
sprites[n]=sprite
end
cache.control = entity.get_or_create_control_behavior() --[[@as LuaLampControlBehavior]]
if entity.name == "nixie-tube-alpha" then
global.alphas[entity.unit_number] = entity
else
--enslave guy to left, if there is one
local neighbors=surf.find_entities_filtered{
position={x=entity.position.x-1,y=entity.position.y},
name=entity.name}
for _,n in pairs(neighbors) do
if n.valid then
if global.next_controller == n.unit_number then
-- if it's currently the *next* controller, claim that too...
global.next_controller = entity.unit_number
end
global.controllers[n.unit_number] = nil
global.nextdigit[entity.unit_number] = n
end
end
--slave self to right, if any
neighbors=surf.find_entities_filtered{
position={x=entity.position.x+1,y=entity.position.y},
name=entity.name}
local foundright=false
for _,n in pairs(neighbors) do
if n.valid then
foundright=true
global.nextdigit[n.unit_number]=entity
end
end
if not foundright then
global.controllers[entity.unit_number] = entity
end
end
end
end
---@param entity LuaEntity
local function onRemoveEntity(entity)
if entity.valid then
if validEntityName[entity.name] then
--if I was a controller, deregister
if global.next_controller == entity.unit_number then
-- if i was the *next* controller, restart iteration...
global.next_controller=nil
end
global.controllers[entity.unit_number]=nil
--if i was an alpha, deregister
if global.next_alpha == entity.unit_number then
-- if i was the *next* alpha, restart iteration...
global.next_controller=nil
end
global.alphas[entity.unit_number]=nil
global.cache[entity.unit_number]=nil
local nextdigit = global.nextdigit[entity.unit_number]
--if I had a next-digit, register it as a controller
if nextdigit and nextdigit.valid then
global.controllers[nextdigit.unit_number] = nextdigit
displayValString(nextdigit)
global.nextdigit[entity.unit_number] = nil
end
--if i was a next-digit, unlink
for k,v in pairs(global.nextdigit) do
if v == entity then
global.nextdigit[k] = nil
break
end
end
end
end
end
local function RegisterPicker()
if remote.interfaces["picker"] and remote.interfaces["picker"]["dolly_moved_entity_id"] then
script.on_event(remote.call("picker", "dolly_moved_entity_id"), function(event)
onRemoveEntity(event.moved_entity)
onPlaceEntity(event.moved_entity)
end)
end
end
script.on_init(function()
global.alphas = {}
global.controllers = {}
global.cache = {}
global.nextdigit = {}
RegisterStrings()
RegisterPicker()
end)
script.on_load(function()
RegisterStrings()
RegisterPicker()
end)
local function RebuildNixies()
-- clear the tables
global = {
alphas = {},
controllers = {},
cache = {},
nextdigit = {},
}
-- wipe out any lingering sprites i've just deleted the references to...
rendering.clear("nixie-tubes")
-- and re-index the world
for _,surf in pairs(game.surfaces) do
-- re-index all nixies. non-nixie lamps will be ignored by onPlaceEntity
for _,lamp in pairs(surf.find_entities_filtered{type="lamp"}) do
onPlaceEntity(lamp)
end
end
end
remote.add_interface("nixie-tubes",{
RebuildNixies = RebuildNixies
})
commands.add_command("RebuildNixies","Reset all Nixie Tubes to clear display glitches.", RebuildNixies)
script.on_configuration_changed(function(data)
if data.mod_changes and data.mod_changes["nixie-tubes"] then
RebuildNixies()
end
end)
local filters = {
}
local names = {}
for name in pairs(validEntityName) do
filters[#filters+1] = {filter="name",name=name}
filters[#filters+1] = {filter="ghost_name",name=name}
names[#names+1] = name
end
script.on_event(defines.events.on_built_entity, function(event) onPlaceEntity(event.created_entity) end, filters)
script.on_event(defines.events.on_robot_built_entity, function(event) onPlaceEntity(event.created_entity) end, filters)
script.on_event(defines.events.script_raised_built, function(event) onPlaceEntity(event.entity) end)
script.on_event(defines.events.script_raised_revive, function(event) onPlaceEntity(event.entity) end)
script.on_event(defines.events.on_entity_cloned, function(event) onPlaceEntity(event.destination) end)
script.on_event(defines.events.on_pre_player_mined_item, function(event) onRemoveEntity(event.entity) end, filters)
script.on_event(defines.events.on_robot_pre_mined, function(event) onRemoveEntity(event.entity) end, filters)
script.on_event(defines.events.on_entity_died, function(event) onRemoveEntity(event.entity) end, filters)
script.on_event(defines.events.script_raised_destroy, function(event) onRemoveEntity(event.entity) end)
script.on_event(defines.events.on_pre_chunk_deleted, function(event)
for _,chunk in pairs(event.positions) do
local x = chunk.x
local y = chunk.y
local area = {{x*32,y*32},{31+x*32,31+y*32}}
for _,ent in pairs(game.get_surface(event.surface_index).find_entities_filtered{name = names,area = area}) do
onRemoveEntity(ent)
end
end
end)
script.on_event(defines.events.on_pre_surface_cleared,function (event)
for _,ent in pairs(game.get_surface(event.surface_index).find_entities_filtered{name = names}) do
onRemoveEntity(ent)
end
end)
script.on_event(defines.events.on_pre_surface_deleted,function (event)
for _,ent in pairs(game.get_surface(event.surface_index).find_entities_filtered{name = names}) do
onRemoveEntity(ent)
end
end)
script.on_event(defines.events.on_tick, onTick)