---@class WhereIsMyBody : module local M = {} --#region Global data ---@type table # [render id, corpse entity, tag number?] local players_bodies ---@type table # [render id, corpse entity, tag number?] local inactive_players_bodies ---@type table # {render id, corpse entity, tag number?} local important_players_body ---@type table # {render id, corpse entity, tag number?} local inactive_important_players_body ---@type table local corpses_queue --#endregion --#region Constants local draw_line = rendering.draw_line local set_color = rendering.set_color local rendering_destroy = rendering.destroy local is_render_valid = rendering.is_valid local remove = table.remove --#endregion --#region Settings local update_tick = settings.global["WHMB_update_tick"].value --#endregion --#region Utils local function remove_lines_event(event) local player_index = event.player_index local player_bodies = players_bodies[player_index] if player_bodies ~= nil then for i=1, #player_bodies do local body_data = player_bodies[i] rendering_destroy(body_data[1]) local chart_tag = body_data[3] if chart_tag and chart_tag.valid then chart_tag.destroy() end end end players_bodies[player_index] = nil player_bodies = inactive_players_bodies[player_index] if player_bodies ~= nil then for i=1, #player_bodies do local body_data = player_bodies[i] rendering_destroy(body_data[1]) local chart_tag = body_data[3] if chart_tag and chart_tag.valid then chart_tag.destroy() end end end inactive_players_bodies[player_index] = nil local body_data = important_players_body[player_index] if body_data then rendering_destroy(body_data[1]) important_players_body[player_index] = nil local chart_tag = body_data[3] if chart_tag and chart_tag.valid then chart_tag.destroy() end end body_data = inactive_important_players_body[player_index] if body_data then rendering_destroy(body_data[1]) inactive_important_players_body[player_index] = nil local chart_tag = body_data[3] if chart_tag and chart_tag.valid then chart_tag.destroy() end end end local color_data = {0, 0, 0, 0} -- orange local min_color_data = {0.19, 0.8 * 0.19, 0, 0.19} -- orange local max_color_data = {0.9, 0.8 * 0.9, 0, 0.9} -- orange ---@param character table #LuaEntity ---@param id integer ---@param corpse table #LuaEntity local function update_color(character, id, corpse) local start = character.position local stop = corpse.position local xdiff = start.x - stop.x local ydiff = start.y - stop.y local distance = (xdiff * xdiff + ydiff * ydiff)^0.5 if distance > 450 then set_color(id, min_color_data) elseif distance < 95 then set_color(id, max_color_data) else local r = 1 - distance / 500 color_data[1] = r color_data[2] = 0.8 * r color_data[4] = r set_color(id, color_data) end end -- Perhaps, I should change it local purple_color_data = {171 / 255, 64 / 255, 1, 0.8} local max_purple_color_data = {171 / 255, 64 / 255, 1, 0.8} local min_purple_color_data = {(171 / 255) * 0.15, (64 / 255) * 0.15, 0.15, 0.8} ---@param character table #LuaEntity ---@param id integer ---@param corpse table #LuaEntity local function update_purple_color(character, id, corpse) local start = character.position local stop = corpse.position local xdiff = start.x - stop.x local ydiff = start.y - stop.y local distance = (xdiff * xdiff + ydiff * ydiff)^0.5 if distance > 450 then set_color(id, min_purple_color_data) elseif distance < 166 then set_color(id, max_purple_color_data) else local r = (1 - distance / 500) * 1.5 r = (r > 1 and 1) or r purple_color_data[1] = (171 / 255) * r purple_color_data[2] = (64 / 255) * r purple_color_data[3] = r set_color(id, purple_color_data) end end local orange_color = {1, 0.8, 0, 0.9} local purple_color = {171 / 255, 64 / 255, 1, 0.8} local line_data = { color = orange_color, width = 0.2, from = nil, surface = nil, players = {}, draw_on_ground = true, only_in_alt_mode = true } -- It's a bit messy, so be careful about desyncs ---@param player LuaEntity ---@param player_index number local function draw_all_lines(player, player_index, event) local character = player.character if not (character and character.valid) then remove_lines_event(event) return end local player_bodies local important_body local is_entity_info_visible = player.game_view_settings.show_entity_info if is_entity_info_visible then player_bodies = players_bodies[player_index] important_body = important_players_body[player_index] else player_bodies = inactive_players_bodies[player_index] important_body = inactive_important_players_body[player_index] end line_data.surface = character.surface line_data.from = character line_data.width = player.mod_settings["WHMB_line_width"].value line_data.players = {player_index} if player_bodies then -- There's a chance that some lines still exist due to LuaPlayer.ticks_to_respawn for i=1, #player_bodies do -- is it really safe? rendering_destroy(player_bodies[i][1]) end line_data.color = orange_color for i=#player_bodies, 1, -1 do local body_data = player_bodies[i] local entity = body_data[2] if entity.valid then line_data.to = entity body_data[1] = draw_line(line_data) else remove(player_bodies, i) end end end if important_body then rendering_destroy(important_body[1]) line_data.color = purple_color local entity = important_body[2] if entity.valid then line_data.to = entity important_body[1] = draw_line(line_data) else if is_entity_info_visible then important_players_body[player_index] = nil else inactive_important_players_body[player_index] = nil end end end end ---@param player table #LuaPlayer ---@param corpse table #LuaEntity ---@param player_index number ---@param is_forced? boolean local function draw_new_line_to_body(player, corpse, player_index, is_forced) local character = player.character if not (character and character.valid) then return end if not (corpse and corpse.valid) then return end local surface = character.surface if surface ~= corpse.surface then return end if not is_forced then local items_count = table_size(corpse.get_inventory(defines.inventory.character_corpse).get_contents()) if items_count <= player.mod_settings["WHMB_ignore_if_less_n_items"].value then return end end line_data.surface = surface line_data.from = character line_data.to = corpse line_data.width = player.mod_settings["WHMB_line_width"].value line_data.players = {player_index} local chart_tag if player.mod_settings["WHMB_create_chart_tags_after_death"].value then local icon = {type="virtual", name="signal-info"} chart_tag = player.force.add_chart_tag( surface, {position=corpse.position, text='[entity=character-corpse]' .. player.name, icon=icon} ) end corpses_queue[player_index] = nil -- maybe it can be buggy local player_bodies local is_entity_info_visible = player.game_view_settings.show_entity_info if is_entity_info_visible then player_bodies = players_bodies[player_index] if player_bodies == nil then if important_players_body[player_index] then players_bodies[player_index] = players_bodies[player_index] or {} player_bodies = players_bodies[player_index] else line_data.color = purple_color local id = draw_line(line_data) important_players_body[player_index] = {id, corpse, chart_tag} return end end else player_bodies = inactive_players_bodies[player_index] if player_bodies == nil then if inactive_important_players_body[player_index] then inactive_players_bodies[player_index] = inactive_players_bodies[player_index] or {} player_bodies = inactive_players_bodies[player_index] else line_data.color = purple_color local id = draw_line(line_data) inactive_important_players_body[player_index] = {id, corpse, chart_tag} return end end end line_data.color = orange_color local id = draw_line(line_data) player_bodies[#player_bodies+1] = {id, corpse, chart_tag} end ---@param player table #LuaPlayer ---@param corpse table #LuaEntity ---@param player_index number ---@param is_forced? boolean local function draw_important_line_to_body(player, corpse, player_index, is_forced) local character = player.character if not (character and character.valid) then return end if not (corpse and corpse.valid) then return end local surface = character.surface if surface ~= corpse.surface then return end if not is_forced then local items_count = table_size(corpse.get_inventory(defines.inventory.character_corpse).get_contents()) if items_count <= player.mod_settings["WHMB_ignore_if_less_n_items"].value then return end end line_data.surface = surface line_data.from = character line_data.to = corpse line_data.width = player.mod_settings["WHMB_line_width"].value line_data.players = {player_index} corpses_queue[player_index] = nil -- maybe it can be buggy line_data.color = purple_color local id = draw_line(line_data) local is_entity_info_visible = player.game_view_settings.show_entity_info if is_entity_info_visible then important_players_body[player_index] = {id, corpse} else inactive_important_players_body[player_index] = {id, corpse} end end --#endregion --#region Functions of events local function check_render() local get_player = game.get_player for player_index, all_bodies_data in pairs(players_bodies) do local player = get_player(player_index) if player and player.valid then local character = player.character if character and character.valid then for i=#all_bodies_data, 1, -1 do local body_data = all_bodies_data[i] local corpse = body_data[2] if corpse.valid then local id = body_data[1] if is_render_valid(id) then update_color(character, id, corpse) else local chart_tag = body_data[3] if chart_tag and chart_tag.valid then chart_tag.destroy() end remove(all_bodies_data, i) draw_new_line_to_body(player, corpse, player_index, true) end else local chart_tag = body_data[3] if chart_tag and chart_tag.valid then chart_tag.destroy() end remove(all_bodies_data, i) end end if #all_bodies_data == 0 then players_bodies[player_index] = nil end end end end for player_index, body_data in pairs(important_players_body) do local player = get_player(player_index) if player and player.valid then local character = player.character if character and character.valid then local corpse = body_data[2] if corpse.valid then local id = body_data[1] if is_render_valid(id) then update_purple_color(character, id, corpse) else local chart_tag = body_data[3] if chart_tag and chart_tag.valid then chart_tag.destroy() end important_players_body[player_index] = nil draw_important_line_to_body(player, corpse, player_index, true) end else local chart_tag = body_data[3] if chart_tag and chart_tag.valid then chart_tag.destroy() end important_players_body[player_index] = nil end end end end end local function on_player_toggled_alt_mode(event) local player_index = event.player_index if event.alt_mode then players_bodies[player_index] = inactive_players_bodies[player_index] inactive_players_bodies[player_index] = nil important_players_body[player_index] = inactive_important_players_body[player_index] inactive_important_players_body[player_index] = nil else inactive_players_bodies[player_index] = players_bodies[player_index] players_bodies[player_index] = nil inactive_important_players_body[player_index] = important_players_body[player_index] important_players_body[player_index] = nil end end local function on_pre_player_removed(event) local player_index = event.player_index players_bodies[player_index] = nil inactive_players_bodies[player_index] = nil important_players_body[player_index] = nil inactive_important_players_body[player_index] = nil corpses_queue[player_index] = nil end local function on_console_command(event) if event.command ~= "editor" then return end remove_lines_event(event) end local function on_player_clicked_gps_tag(event) local player_index = event.player_index local player = game.get_player(player_index) if not (player and player.valid) then return end local character = player.character if not (character and character.valid) then return end local is_entity_info_visible = player.game_view_settings.show_entity_info if is_entity_info_visible == false then return end local surface = game.get_surface(event.surface) if not (surface and surface.valid) then return end local player_bodies = players_bodies[player_index] local important_player_body = important_players_body[player_index] local pos = event.position if important_player_body then local x = pos.x local y = pos.y local corpse = important_player_body[2] if corpse.valid then if corpse.surface == surface then local pos2 = corpse.position local xdiff = x - pos2.x local ydiff = y - pos2.y local distance = (xdiff * xdiff + ydiff * ydiff)^0.5 if distance <= 2 then return end end else important_players_body[player_index] = nil end end if player_bodies then local x = pos.x local y = pos.y for i=#player_bodies, 1, -1 do local body_data = player_bodies[i] local corpse = body_data[2] if corpse.valid then if corpse.surface == surface then local pos2 = corpse.position local xdiff = x - pos2.x local ydiff = y - pos2.y local distance = (xdiff * xdiff + ydiff * ydiff)^0.5 if distance <= 2 then rendering_destroy(body_data[1]) remove(player_bodies, i) local important_body_data = important_player_body draw_important_line_to_body(player, corpse, player_index, true) if important_body_data then rendering_destroy(important_body_data[1]) local entity = important_body_data[2] if entity.valid then draw_new_line_to_body(player, entity, player_index, true) end end return end end else remove(player_bodies, i) end end end if player.cheat_mode then return end local filter = {type="character-corpse", position=pos, radius=2} local corpses = surface.find_entities_filtered(filter) for i=1, #corpses do local corpse = corpses[i] if corpse.valid then local items_count = table_size(corpse.get_inventory(defines.inventory.character_corpse).get_contents()) if items_count > 0 then draw_new_line_to_body(player, corpse, player_index, true) return end end end end local function on_player_respawned(event) local player_index = event.player_index local player = game.get_player(player_index) if not (player and player.valid) then return end if player.cheat_mode then return end draw_all_lines(player, player_index, event) local corpse = corpses_queue[player_index] corpses_queue[player_index] = nil if not (corpse and corpse.valid) then return end if settings.global["WHMB_delete_empty_bodies"].value then local items_count = table_size(corpse.get_inventory(defines.inventory.character_corpse).get_contents()) if items_count == 0 then corpse.destroy({raise_destroy=true}) return end end draw_new_line_to_body(player, corpse, player_index) end local function on_player_died(event) local player_index = event.player_index local player = game.get_player(player_index) if not (player and player.valid) then return end if player.cheat_mode then return end if player.mod_settings["WHMB_create_lines"].value == false then return end local surface = player.surface local position = player.position local corpse = surface.find_entity("character-corpse", position) if not (corpse and corpse.valid) then return end corpses_queue[player_index] = corpse end --TODO: check tag content, prohibit deletion of chart tags if the ones belongs to another player local function on_chart_tag_removed(event) local player_index = event.player_index if player_index == nil then return end local player = game.get_player(player_index) if not (player and player.valid) then return end local tag = event.tag if tag.valid == false then return end local player_bodies local important_body local is_entity_info_visible = player.game_view_settings.show_entity_info if is_entity_info_visible then player_bodies = players_bodies[player_index] important_body = important_players_body[player_index] else player_bodies = inactive_players_bodies[player_index] important_body = inactive_important_players_body[player_index] end local tag_number = tag.tag_number local filter = {type="character-corpse", position=tag.position, radius=2} local corpses = player.surface.find_entities_filtered(filter) for i=1, #corpses do local corpse = corpses[i] if corpse.valid then if player_bodies ~= nil then for j=1, #player_bodies do local body_data = player_bodies[j] local chart_tag = body_data[3] if chart_tag and chart_tag.valid and chart_tag.tag_number == tag_number then rendering_destroy(body_data[1]) remove(player_bodies, j) if #player_bodies == 0 then if is_entity_info_visible then players_bodies[player_index] = nil else inactive_players_bodies[player_index] = nil end end return end end end if important_body then local chart_tag = important_body[3] if chart_tag and chart_tag.valid and chart_tag.tag_number == tag_number then rendering_destroy(important_body[1]) if is_entity_info_visible then important_players_body[player_index] = nil else inactive_important_players_body[player_index] = nil end return end end end end end local function on_runtime_mod_setting_changed(event) if event.setting == "WHMB_update_tick" then local value = settings.global[event.setting].value script.on_nth_tick(update_tick, nil) update_tick = value script.on_nth_tick(value, check_render) end end --#endregion --#region Pre-game stage local function link_data() players_bodies = global.players_bodies inactive_players_bodies = global.inactive_players_bodies important_players_body = global.important_players_body inactive_important_players_body = global.inactive_important_players_body corpses_queue = global.corpses_queue end local function update_global_data() global.players_bodies = {} global.inactive_players_bodies = {} global.important_players_body = {} global.inactive_important_players_body = {} global.corpses_queue = global.corpses_queue or {} link_data() for player_index, corpse in pairs(corpses_queue) do if corpse.valid == false then corpses_queue[player_index] = nil end end end M.on_init = update_global_data M.on_configuration_changed = function(event) local mod_changes = event.mod_changes["m_WhereIsMyBody"] if not (mod_changes and mod_changes.old_version) then return end update_global_data() global.inactive_players_data = nil -- old data global.players = nil -- old data end M.on_load = link_data --#endregion M.events = { -- [defines.events.on_game_created_from_scenario] = on_game_created_from_scenario, -- [defines.events.on_pre_player_mined_item] = on_pre_player_mined_item, -- [defines.events.on_character_corpse_expired] = on_character_corpse_expired, -- [defines.events.on_pre_surface_cleared] = on_pre_surface_cleared, -- [defines.events.on_pre_surface_deleted] = on_pre_surface_deleted, [defines.events.on_runtime_mod_setting_changed] = on_runtime_mod_setting_changed, [defines.events.on_player_respawned] = on_player_respawned, [defines.events.on_pre_player_removed] = on_pre_player_removed, [defines.events.on_player_died] = on_player_died, [defines.events.on_player_left_game] = remove_lines_event, [defines.events.on_player_changed_surface] = remove_lines_event, [defines.events.on_player_toggled_alt_mode] = on_player_toggled_alt_mode, [defines.events.on_console_command] = on_console_command, -- on_player_toggled_map_editor event seems doesn't work [defines.events.on_player_clicked_gps_tag] = on_player_clicked_gps_tag, -- [defines.events.on_chart_tag_modified] = on_chart_tag_modified, [defines.events.on_chart_tag_removed] = on_chart_tag_removed } M.on_nth_tick = { [update_tick] = check_render, } return M