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)