Refactor legacy addons into Clusterio format (#413)

* Refactor custom start

* Refactor afk kick

* Fix use of assert get player

* Refactor chat popup

* Refactor chat auto reply

* Refactor help bubbles

* Refactor damage popups

* Refactor death markers

* Refactor deconstruction log

* Remove FAGC logging

* Refactor discord alerts

* Refactor insert pickup

* Refactor inventory clear

* Refactor extra logging

* Refactor nuke protection

* Refactor pollution grading

* Refactor protection jail

* Refactor report jail

* Refactor mine depletion

* Refactor degrading tiles

* Refactor station auto name

* Refactor spawn area

* Refactor fast deconstruction

* Bug Fixes
This commit is contained in:
Cooldude2606
2025-12-02 18:34:24 +00:00
committed by GitHub
parent a45f53bc48
commit 9bd699ebf1
69 changed files with 2614 additions and 2260 deletions

View File

@@ -31,7 +31,8 @@ types.lower_role =
--- @cast result any TODO role is not a defined type
local player_highest = highest_role(player)
if player_highest.index >= result.index then
local is_root = Roles.config.internal.root == player_highest.name
if not is_root and player_highest.index >= result.index then
return invalid{ "exp-commands-parse_role.lower-role" }
else
return valid(result)
@@ -47,7 +48,8 @@ types.lower_role_player =
local other_highest = highest_role(result)
local player_highest = highest_role(player)
if player_highest.index >= other_highest.index then
local is_root = Roles.config.internal.root == player_highest.name
if not is_root and player_highest.index >= other_highest.index then
return invalid{ "exp-commands-parse_role.lower-role-player" }
else
return valid(result)
@@ -63,7 +65,8 @@ types.lower_role_player_online =
local other_highest = highest_role(result)
local player_highest = highest_role(player)
if player_highest.index >= other_highest.index then
local is_root = Roles.config.internal.root == player_highest.name
if not is_root and player_highest.index >= other_highest.index then
return invalid{ "exp-commands-parse_role.lower-role-player" }
else
return valid(result)
@@ -79,7 +82,8 @@ types.lower_role_player_alive =
local other_highest = highest_role(result)
local player_highest = highest_role(player)
if player_highest.index >= other_highest.index then
local is_root = Roles.config.internal.root == player_highest.name
if not is_root and player_highest.index >= other_highest.index then
return invalid{ "exp-commands-parse_role.lower-role-player" }
else
return valid(result)

View File

@@ -43,8 +43,29 @@ require("modules/exp_scenario/commands/warnings")
require("modules/exp_scenario/commands/waterfill")
--- Control
add(require("modules/exp_scenario/control/afk_kick"))
add(require("modules/exp_scenario/control/bonus"))
add(require("modules/exp_scenario/control/chat_auto_reply"))
add(require("modules/exp_scenario/control/chat_popup"))
add(require("modules/exp_scenario/control/custom_start"))
add(require("modules/exp_scenario/control/damage_popups"))
add(require("modules/exp_scenario/control/death_markers"))
add(require("modules/exp_scenario/control/deconstruction_log"))
add(require("modules/exp_scenario/control/degrading_tiles"))
add(require("modules/exp_scenario/control/discord_alerts"))
add(require("modules/exp_scenario/control/extra_logging"))
add(require("modules/exp_scenario/control/fast_deconstruction"))
add(require("modules/exp_scenario/control/help_bubbles"))
add(require("modules/exp_scenario/control/inserter_pickup"))
add(require("modules/exp_scenario/control/inventory_clear"))
add(require("modules/exp_scenario/control/mine_depletion"))
add(require("modules/exp_scenario/control/nuke_protection"))
add(require("modules/exp_scenario/control/pollution_grading"))
add(require("modules/exp_scenario/control/protection_jail"))
add(require("modules/exp_scenario/control/report_jail"))
add(require("modules/exp_scenario/control/research"))
add(require("modules/exp_scenario/control/spawn_area"))
add(require("modules/exp_scenario/control/station_auto_name"))
--- Guis
add(require("modules/exp_scenario/gui/autofill"))

View File

@@ -0,0 +1,84 @@
--[[-- Control -- AFK Kick
Kicks players when all players on the server are afk
]]
local Async = require("modules/exp_util/async")
local Storage = require("modules/exp_util/storage")
local config = require("modules.exp_legacy.config.afk_kick")
--- @type { last_active: number }
local script_data = { last_active = 0 }
Storage.register(script_data, function(tbl)
script_data = tbl
end)
--- Kicks an afk player, used to add a delay so the gui has time to appear
local afk_kick_player_async =
Async.register(function(player)
if game.tick - script_data.last_active < config.kick_time then return end
game.kick_player(player, "AFK while no active players on the server")
end)
--- Check if there is an active player
local function has_active_player()
for _, player in ipairs(game.connected_players) do
if player.afk_time < config.afk_time
or config.admin_as_active and player.admin
or config.trust_as_active and player.online_time > config.trust_time
or config.custom_active_check and config.custom_active_check(player) then
script_data.last_active = game.tick
return true
end
end
return false
end
--- Check for an active player every update_time number of ticks
local function check_afk_players()
-- Check for active players
if has_active_player() then return end
-- Check if players should be kicked
if game.tick - script_data.last_active < config.kick_time then return end
-- Kick time exceeded, kick all players
for _, player in ipairs(game.connected_players) do
-- Add a frame to say why the player was kicked
local frame = player.gui.screen.add{
type = "frame",
name = "afk-kick",
caption = { "exp_afk-kick.kick-message" },
}
local uis = player.display_scale
local res = player.display_resolution
frame.location = {
x = res.width * (0.5 - 0.11 * uis),
y = res.height * (0.5 - 0.14 * uis),
}
-- Kick the player, some delay needed allow the gui to show
afk_kick_player_async:start_after(60, player)
end
end
--- Remove the screen gui if it is present
--- @param event EventData.on_player_joined_game
local function on_player_joined_game(event)
local player = assert(game.get_player(event.player_index))
local frame = player.gui.screen["afk-kick"]
if frame and frame.valid then frame.destroy() end
end
local e = defines.events
return {
events = {
[e.on_player_joined_game] = on_player_joined_game,
},
on_nth_tick = {
[config.update_time] = check_afk_players,
},
has_active_player = has_active_player,
}

View File

@@ -0,0 +1,63 @@
--[[-- Control - Chat Auto Reply
Adds auto replies to chat messages, as well as chat commands
]]
local Roles = require("modules.exp_legacy.expcore.roles")
local config = require("modules.exp_legacy.config.chat_reply")
local prefix = config.command_prefix
local prefix_len = string.len(prefix)
local find = string.find
local sub = string.sub
--- Check if a message has any trigger words
--- @param event EventData.on_console_chat
local function on_console_chat(event)
if not event.player_index then return end
local player = assert(game.get_player(event.player_index))
local message = event.message:lower():gsub("%s+", "")
-- Check if the player can chat commands
local commands_allowed = true
if config.command_admin_only and not player.admin then commands_allowed = false end
if config.command_permission and not Roles.player_allowed(player, config.command_permission) then commands_allowed = false end
-- Check if a key word appears in the message
for key_word, reply in pairs(config.messages) do
local start_pos = find(message, key_word)
if start_pos then
local is_command = sub(message, start_pos - prefix_len - 1, start_pos - 1) == prefix
if type(reply) == "function" then
reply = reply(player, is_command)
end
if is_command and commands_allowed then
game.print{ "exp_chat-auto-reply.chat-reply", reply }
elseif is_command then
player.print{ "exp_chat-auto-reply.chat-disallowed" }
elseif not commands_allowed then
player.print{ "exp_chat-auto-reply.chat-reply", reply }
end
end
end
if not commands_allowed then return end
-- Check if a command appears in the message
for key_word, reply in pairs(config.commands) do
if find(message, prefix .. key_word) then
if type(reply) == "function" then
reply = reply(player, true)
end
game.print{ "exp_chat-auto-reply.chat-reply", reply }
end
end
end
local e = defines.events
return {
events = {
[e.on_console_chat] = on_console_chat,
}
}

View File

@@ -0,0 +1,48 @@
--[[-- Control - Chat Popup
Creates flying text entities when a player sends a message in chat
]]
local FlyingText = require("modules/exp_util/flying_text")
local config = require("modules.exp_legacy.config.popup_messages")
local lower = string.lower
local find = string.find
--- Create a chat bubble when a player types a message
--- @param event EventData.on_console_chat
local function on_console_chat(event)
if not event.player_index then return end
local player = assert(game.get_player(event.player_index))
local name = player.name
-- Sends the message as text above them
if config.show_player_messages then
FlyingText.create_as_player{
target_player = player,
text = { "exp_chat-popup.flying-text-message", name, event.message },
}
end
if not config.show_player_mentions then return end
-- Loops over online players to see if they name is included
local search_string = lower(event.message)
for _, mentioned_player in ipairs(game.connected_players) do
if mentioned_player.index ~= player.index then
if find(search_string, lower(mentioned_player.name), 1, true) then
FlyingText.create_as_player{
target_player = mentioned_player,
text = { "exp_chat-popup.flying-text-ping", name },
}
end
end
end
end
local e = defines.events
return {
events = {
[e.on_console_chat] = on_console_chat,
}
}

View File

@@ -0,0 +1,64 @@
--[[-- Control - Custom Start
Changes the starting script and the items given on first join depending on factory production levels
]]
local config = require("modules.exp_legacy.config.advanced_start")
local floor = math.floor
--- Give a player their starting items
--- @param player LuaPlayer
local function give_starting_items(player)
local get_prod_stats = player.force.get_item_production_statistics(player.physical_surface)
local get_input_count = get_prod_stats.get_input_count
local insert_param = { name = "", count = 0 }
local insert = player.insert
for item_name, insert_amount in pairs(config.items) do
insert_param.name = item_name
if type(insert_amount) == "function" then
local count = insert_amount(get_input_count(item_name), get_input_count, player)
if count >= 1 then
insert_param.count = floor(count)
insert(insert_param)
end
elseif insert_amount >= 1 then
insert_param.count = floor(insert_amount)
insert(insert_param)
end
end
end
--- Calls remote interfaces to configure the base game scenarios
local function on_init()
game.forces.player.friendly_fire = config.friendly_fire
game.map_settings.enemy_expansion.enabled = config.enemy_expansion
if remote.interfaces["freeplay"] then
remote.call("freeplay", "set_created_items", {})
remote.call("freeplay", "set_disable_crashsite", config.disable_crashsite)
remote.call("freeplay", "set_skip_intro", config.skip_intro)
remote.call("freeplay", "set_chart_distance", config.chart_radius)
end
if remote.interfaces["silo_script"] then
remote.call("silo_script", "set_no_victory", config.skip_victory)
end
if remote.interfaces["space_finish_script"] then
remote.call("space_finish_script", "set_no_victory", config.skip_victory)
end
end
--- Give a player starting items when they first join
--- @param event EventData.on_player_created
local function on_player_created(event)
-- We can't trust on_init to work for clusterio
if event.player_index == 1 then on_init() end
give_starting_items(assert(game.get_player(event.player_index)))
end
local e = defines.events
return {
on_init = on_init,
events = {
[e.on_player_created] = on_player_created,
},
give_starting_items = give_starting_items,
}

View File

@@ -0,0 +1,50 @@
--[[-- Control - Damage PopUps
Displays the amount of dmg that is done by players to entities;
also shows player health when a player is attacked
]]
local FlyingText = require("modules/exp_util/flying_text")
local config = require("modules.exp_legacy.config.popup_messages")
local random = math.random
local floor = math.floor
local max = math.max
--- Called when entity entity is damaged including the player character
--- @param event EventData.on_entity_damaged
local function on_entity_damaged(event)
local message
local cause = event.cause
local entity = event.entity
-- Check which message to display
if config.show_player_health and entity.name == "character" then
message = { "exp_damage-popup.flying-text-health", floor(entity.health) }
elseif config.show_player_damage and entity.name ~= "character" and cause and cause.name == "character" then
message = { "exp_damage-popup.flying-text-damage", floor(event.original_damage_amount) }
end
-- Outputs the message as floating text
if message then
local entity_radius = max(1, entity.get_radius())
local offset = (random() - 0.5) * entity_radius * config.damage_location_variance
local position = { x = entity.position.x + offset, y = entity.position.y - entity_radius }
local health_percentage = entity.get_health_ratio()
local color = { r = 1 - health_percentage, g = health_percentage, b = 0 }
FlyingText.create{
text = message,
position = position,
color = color,
}
end
end
local e = defines.events
return {
events = {
[e.on_entity_damaged] = on_entity_damaged,
},
}

View File

@@ -0,0 +1,157 @@
--[[-- Control - Death Markers
Makes markers on the map where places have died and reclaims items if not recovered
]]
local ExpUtil = require("modules/exp_util")
local Storage = require("modules/exp_util/storage")
local config = require("modules.exp_legacy.config.death_logger")
local map_tag_time_format = ExpUtil.format_time_factory{ format = "short", hours = true, minutes = true }
--- @class CorpseData
--- @field player LuaPlayer
--- @field corpse LuaEntity
--- @field tag LuaCustomChartTag?
--- @field created_at number
--- @type table<number, CorpseData>
local character_corpses = {}
Storage.register(character_corpses, function(tbl)
character_corpses = tbl
end)
--- Creates a new death marker and saves it to the given death
--- @param corpse_data CorpseData
local function create_map_tag(corpse_data)
local player = corpse_data.player
local message = player.name .. " died"
if config.include_time_of_death then
local time = map_tag_time_format(corpse_data.created_at)
message = message .. " at " .. time
end
corpse_data.tag = player.force.add_chart_tag(corpse_data.corpse.surface, {
position = corpse_data.corpse.position,
icon = config.map_icon,
text = message,
})
end
--- Checks that all map tags are present and valid, creating any that are missing
local function check_map_tags()
for _, corpse_data in pairs(character_corpses) do
if not corpse_data.tag or not corpse_data.tag.valid then
create_map_tag(corpse_data)
end
end
end
-- when a player dies a new death is added to the records and a map marker is made
--- @param event EventData.on_player_died
local function on_player_died(event)
local player = assert(game.get_player(event.player_index))
local corpse = player.surface.find_entity("character-corpse", player.physical_position)
if not corpse or not corpse.valid then return end
local corpse_data = {
player = player,
corpse = corpse,
created_at = event.tick,
}
local registration_number = script.register_on_object_destroyed(corpse)
character_corpses[registration_number] = corpse_data
-- Create a map marker
if config.show_map_markers then
create_map_tag(corpse_data)
end
-- Draw a light attached to the corpse with the player color
if config.show_light_at_corpse then
rendering.draw_light{
sprite = "utility/light_medium",
surface = player.surface,
color = player.color,
force = player.force,
target = corpse,
}
end
end
--- Called to remove stale corpse data
--- @param event EventData.on_object_destroyed
local function on_object_destroyed(event)
local corpse_data = character_corpses[event.registration_number]
character_corpses[event.registration_number] = nil
if not corpse_data then
return
end
local tag = corpse_data.tag
if tag and config.clean_map_markers then
tag.destroy()
end
end
--- Draw lines to the player corpse
--- @param event EventData.on_player_respawned
local function on_player_respawned(event)
local index = event.player_index
local player = assert(game.get_player(index))
for _, corpse_data in pairs(character_corpses) do
if corpse_data.player.index == index then
local line_color = player.color
line_color.a = .3
rendering.draw_line{
color = line_color,
from = player.character,
to = corpse_data.corpse,
surface = player.surface,
players = { index },
draw_on_ground = true,
dash_length = 1,
gap_length = 1,
width = 2,
}
end
end
end
--- Collect all items from expired character corpses
--- @param event EventData.on_character_corpse_expired
local function on_character_corpse_expired(event)
local corpse = event.corpse
local inventory = assert(corpse.get_inventory(defines.inventory.character_corpse))
ExpUtil.transfer_inventory_to_surface{
inventory = inventory,
surface = corpse.surface,
name = "iron-chest",
allow_creation = true,
}
end
local on_nth_tick = {}
if config.show_map_markers then
on_nth_tick[config.period_check_map_tags] = check_map_tags
end
local e = defines.events
local events = {
[e.on_player_died] = on_player_died,
[e.on_object_destroyed] = on_object_destroyed,
}
if config.show_line_to_corpse then
events[e.on_player_respawned] = on_player_respawned
end
if config.collect_corpses then
events[e.on_character_corpse_expired] = on_character_corpse_expired
end
return {
on_nth_tick = on_nth_tick,
events = events,
}

View File

@@ -0,0 +1,181 @@
--[[-- Control - Deconstruction Log
Log certain actions into a file when events are triggered
]]
local ExpUtil = require("modules/exp_util")
local Roles = require("modules.exp_legacy.expcore.roles")
local config = require("modules.exp_legacy.config.deconlog")
local seconds_time_format = ExpUtil.format_time_factory{ format = "short", hours = true, minutes = true, seconds = true }
local format_number = require("util").format_number
local write_file = helpers.write_file
local format_string = string.format
local concat = table.concat
local filepath = "log/deconstruction.log"
--- Clear the log file
local function clear_log()
helpers.remove_path(filepath)
end
--- Add a new line to the log
--- @param player LuaPlayer
--- @param action string
--- @param ... string
local function add_log_line(player, action, ...)
local text = concat({
seconds_time_format(game.tick),
player.name,
action,
...
}, ",")
write_file(filepath, text .. "\n", true, 0)
end
--- Convert a position to a string
--- @param pos MapPosition
--- @return string
local function format_position(pos)
return format_string("%.1f,%.1f", pos.x, pos.y)
end
--- Convert an area to a string
--- @param area BoundingBox
--- @return string
local function format_area(area)
return format_string("%.1f,%.1f,%.1f,%.1f", area.left_top.x, area.left_top.y, area.right_bottom.x, area.right_bottom.y)
end
--- Convert an entity to a string
--- @param entity LuaEntity
--- @return string
local function format_entity(entity)
return format_string("%s,%.1f,%.1f,%s,%s", entity.name, entity.position.x, entity.position.y, entity.direction, entity.orientation)
end
--- Concert a position into a gps tag
--- @param pos MapPosition
--- @param surface_name string
--- @return string
local function format_position_gps(pos, surface_name)
return format_string("[gps=%.1f,%.1f,%s]", pos.x, pos.y, surface_name)
end
--- Print a message to all players who match the value of admin
--- @param message LocalisedString
local function admin_print(message)
for _, player in ipairs(game.connected_players) do
if player.admin then
player.print(message)
end
end
end
--- Check if a log should be created for a player
--- @param event { player_index: number }
--- @return LuaPlayer?
local function get_log_player(event)
local player = assert(game.get_player(event.player_index))
if Roles.player_has_flag(player, "deconlog-bypass") then
return nil
end
return player
end
--- Log when an area is deconstructed
--- @param event EventData.on_player_deconstructed_area
local function on_player_deconstructed_area(event)
local player = get_log_player(event)
if not player then return end
--- Don't log when a player clears a deconstruction
if event.alt then
return
end
local area = event.area
local surface_name = event.surface.name
local items = event.surface.find_entities_filtered{ area = area, force = player.force }
if #items > 250 then
admin_print{
"exp_deconstruction-log.chat-admin",
player.name,
format_position_gps(area.left_top, surface_name),
format_position_gps(area.right_bottom, surface_name),
format_number(#items, false),
}
end
add_log_line(player, "deconstructed_area", surface_name, format_area(area))
end
--- Log when an entity is built
--- @param event EventData.on_built_entity
local function on_built_entity(event)
local player = get_log_player(event)
if not player then return end
add_log_line(player, "built_entity", format_entity(event.entity))
end
--- Log when an entity is mined
--- @param event EventData.on_player_mined_entity
local function on_player_mined_entity(event)
local player = get_log_player(event)
if not player then return end
add_log_line(player, "mined_entity", format_entity(event.entity))
end
--- Log when rocket is fired
--- @param event EventData.on_player_ammo_inventory_changed
local function on_player_ammo_inventory_changed(event)
local player = get_log_player(event)
if not player or not player.character then return end
local character_ammo = assert(player.get_inventory(defines.inventory.character_ammo))
local item = character_ammo[player.character.selected_gun_index]
if not item or not item.valid or not item.valid_for_read then
return
end
local action_name = "shot-" .. item.name
if not config.fired_rocket and action_name == "shot-rocket" then
return
elseif not config.fired_explosive_rocket and action_name == "shot-explosive-rocket" then
return
elseif not config.fired_nuke and action_name == "shot-atomic-bomb" then
return
end
add_log_line(player, action_name, format_position(player.physical_position), format_position(player.shooting_state.position))
end
local e = defines.events
local events = {
[e.on_multiplayer_init] = clear_log,
}
if config.decon_area then
events[e.on_player_deconstructed_area] = on_player_deconstructed_area
end
if config.built_entity then
events[e.on_built_entity] = on_built_entity
end
if config.mined_entity then
events[e.on_player_mined_entity] = on_player_mined_entity
end
if config.fired_rocket or config.fired_explosive_rocket or config.fired_nuke then
events[e.on_player_ammo_inventory_changed] = on_player_ammo_inventory_changed
end
return {
events = events,
}

View File

@@ -0,0 +1,118 @@
--[[-- Control - Degrading Tiles
When a player walks around the tiles under them will degrade over time, the same is true when entites are built
]]
local config = require("modules.exp_legacy.config.scorched_earth")
local random = math.random
--- Get the max tile strength
local max_strength = 0
for _, strength in pairs(config.strengths) do
if strength > max_strength then
max_strength = strength
end
end
--- Replace a tile with the next tile in the degrade chain
--- @param surface LuaSurface
--- @param position MapPosition
local function degrade_tile(surface, position)
--- @diagnostic disable-next-line Incorrect Api Type: https://forums.factorio.com/viewtopic.php?f=233&t=109145&p=593761&hilit=get_tile#p593761
local tile = surface.get_tile(position)
local tile_name = tile.name
local degrade_tile_name = config.degrade_order[tile_name]
if not degrade_tile_name then return end
surface.set_tiles{ { name = degrade_tile_name, position = position } }
end
--- Replace all titles under an entity with the next tile in the degrade chain
--- @param entity LuaEntity
local function degrade_entity(entity)
if not config.entities[entity.name] then return end
local tiles = {}
local surface = entity.surface
local left_top = entity.bounding_box.left_top
local right_bottom = entity.bounding_box.right_bottom
for x = left_top.x, right_bottom.x do
for y = left_top.y, right_bottom.y do
local tile = surface.get_tile(x, y)
local tile_name = tile.name
local degrade_tile_name = config.degrade_order[tile_name]
if degrade_tile_name then
tiles[#tiles + 1] = { name = degrade_tile_name, position = { x, y } }
end
end
end
surface.set_tiles(tiles)
end
--- Covert strength of a tile into a probability to degrade (0 = impossible, 1 = certain)
--- @param strength number
--- @return number
local function get_probability(strength)
return 1.5 * (1 - (strength / max_strength)) / config.weakness_value
end
--- Gets the average tile strengths around position
--- @param surface LuaSurface
--- @param position MapPosition
--- @return number?
local function get_tile_strength(surface, position)
--- @diagnostic disable-next-line Incorrect Api Type: https://forums.factorio.com/viewtopic.php?f=233&t=109145&p=593761&hilit=get_tile#p593761
local tile = surface.get_tile(position)
local tile_name = tile.name
local strength = config.strengths[tile_name]
if not strength then return end
for x = position.x - 1, position.x + 1 do
for y = position.y - 1, position.y + 1 do
local check_tile = surface.get_tile(x, y)
local check_tile_name = check_tile.name
local check_strength = config.strengths[check_tile_name] or 0
strength = strength + check_strength
end
end
return strength / 9
end
--- When the player changes position the tile will have a chance to downgrade
--- @param event EventData.on_player_changed_position
local function on_player_changed_position(event)
local player = game.players[event.player_index]
if player.controller_type ~= defines.controllers.character then return end
local surface = player.physical_surface
local position = player.physical_position
local strength = get_tile_strength(surface, position)
if not strength then return end
if get_probability(strength) > random() then
degrade_tile(surface, position)
end
end
--- When an entity is build there is a much higher chance that the tiles will degrade
--- @param event EventData.on_built_entity | EventData.on_robot_built_entity
local function on_built_entity(event)
local entity = event.entity
local strength = get_tile_strength(entity.surface, entity.position)
if not strength then return end
if get_probability(strength) * config.weakness_value > random() then
degrade_entity(entity)
end
end
local e = defines.events
return {
events = {
[e.on_player_changed_position] = on_player_changed_position,
[e.on_robot_built_entity] = on_built_entity,
[e.on_built_entity] = on_built_entity,
},
}

View File

@@ -0,0 +1,332 @@
--[[-- Control - Discord Alerts
Sends alert messages to our discord server when certain events are triggered
]]
local ExpUtil = require("modules/exp_util")
local Colors = require("modules/exp_util/include/color")
local config = require("modules.exp_legacy.config.discord_alerts")
local format_string = string.format
local write_json = ExpUtil.write_json
local playtime_format = ExpUtil.format_time_factory{ format = "short", hours = true, minutes = true, seconds = true }
local emit_event_time_format = ExpUtil.format_time_factory{ format = "short", hours = true, minutes = true }
local e = defines.events
local events = {}
--- Append the play time to a players name
--- @param player_name string
--- @return string
local function append_playtime(player_name)
if not config.show_playtime then
return player_name
end
local player = game.get_player(player_name)
if not player then
return player_name
end
return format_string("%s (%s)", player.name, playtime_format(player.online_time))
end
--- Get the player name from an event
--- @param event { player_index: number, by_player_name: string? }
--- @return string, string
local function get_player_name(event)
local player = game.players[event.player_index]
return player.name, event.by_player_name
end
--- Convert a colour value into hex
--- @param color Color.0
--- @return string
local function to_hex(color)
local hex_digits = "0123456789ABCDEF"
local function hex(bit)
local major, minor = math.modf(bit / 16)
major, minor = major + 1, minor * 16 + 1
return hex_digits:sub(major, major) .. hex_digits:sub(minor, minor)
end
return format_string("0x%s%s%s", hex(color.r), hex(color.g), hex(color.b))
end
--- Emit the requires json to file for the given event arguments
--- @param opts { title: string?, color: (Color.0 | string)?, description: string?, tick: number?, fields: { name: string, value: string, inline: boolean? }[] }
local function emit_event(opts)
local admins_online = 0
local players_online = 0
for _, player in pairs(game.connected_players) do
players_online = players_online + 1
if player.admin then
admins_online = admins_online + 1
end
end
local tick_formatted = emit_event_time_format(opts.tick or game.tick)
table.insert(opts.fields, 1, {
name = "Server Details",
value = format_string("Server: ${serverName} Time: %s\nTotal: %d Online: %d Admins: %d", tick_formatted, #game.players, players_online, admins_online),
})
local color = opts.color
write_json("ext/discord.out", {
title = opts.title or "",
description = opts.description or "",
color = type(color) == "table" and to_hex(color) or color or "0x0",
fields = opts.fields,
})
end
--- Repeated protected entity mining
if config.entity_protection then
local EntityProtection = require("modules.exp_legacy.modules.control.protection")
events[EntityProtection.events.on_repeat_violation] = function(event)
local player_name = get_player_name(event)
emit_event{
title = "Entity Protection",
description = "A player removed protected entities",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "Entity", inline = true, value = event.entity.name },
{ name = "Location", value = format_string("X %.1f Y %.1f", event.entity.position.x, event.entity.position.y) },
},
}
end
end
--- Reports added and removed
if config.player_reports then
local Reports = require("modules.exp_legacy.modules.control.reports")
events[Reports.events.on_player_reported] = function(event)
local player_name, by_player_name = get_player_name(event)
local player = assert(game.get_player(player_name))
emit_event{
title = "Report",
description = "A player was reported",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player_name) },
{ name = "Report Count", inline = true, value = Reports.count_reports(player) },
{ name = "Reason", value = event.reason },
},
}
end
events[Reports.events.on_report_removed] = function(event)
if event.batch ~= 1 then return end
local player_name = get_player_name(event)
emit_event{
title = "Reports Removed",
description = "A player has a report removed",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(event.removed_by_name) },
{ name = "Report Count", inline = true, value = tostring(event.batch_count) },
},
}
end
end
--- Warnings added and removed
if config.player_warnings then
local Warnings = require("modules.exp_legacy.modules.control.warnings")
events[Warnings.events.on_warning_added] = function(event)
local player_name, by_player_name = get_player_name(event)
local player = assert(game.get_player(player_name))
emit_event{
title = "Warning",
description = "A player has been given a warning",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player_name) },
{ name = "Report Count", inline = true, value = Warnings.count_warnings(player) },
{ name = "Reason", value = event.reason },
},
}
end
events[Warnings.events.on_warning_removed] = function(event)
if event.batch ~= 1 then return end
local player_name = get_player_name(event)
emit_event{
title = "Warnings Removed",
description = "A player has a warning removed",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(event.removed_by_name) },
{ name = "Report Count", inline = true, value = tostring(event.batch_count) },
},
}
end
end
--- When a player is jailed or unjailed
if config.player_jail then
local Jail = require("modules.exp_legacy.modules.control.jail")
events[Jail.events.on_player_jailed] = function(event)
local player_name, by_player_name = get_player_name(event)
emit_event{
title = "Jail",
description = "A player has been jailed",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player_name) },
{ name = "Reason", value = event.reason },
},
}
end
events[Jail.events.on_player_unjailed] = function(event)
local player_name, by_player_name = get_player_name(event)
emit_event{
title = "Unjail",
description = "A player has been unjailed",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player_name) },
},
}
end
end
--- Ban and unban
if config.player_bans then
--- @param event EventData.on_player_banned
events[e.on_player_banned] = function(event)
if event.by_player then
local by_player = game.players[event.by_player]
emit_event{
title = "Banned",
description = "A player has been banned",
color = Colors.red,
fields = {
{ name = "Player", inline = true, value = append_playtime(event.player_name) },
{ name = "By", inline = true, value = append_playtime(by_player.name) },
{ name = "Reason", value = event.reason },
},
}
end
end
--- @param event EventData.on_player_unbanned
events[e.on_player_unbanned] = function(event)
if event.by_player then
local by_player = game.players[event.by_player]
emit_event{
title = "Un-Banned",
description = "A player has been un-banned",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(event.player_name) },
{ name = "By", inline = true, value = append_playtime(by_player.name) },
{ name = "Reason", value = event.reason },
},
}
end
end
end
--- Mute and unmute
if config.player_mutes then
--- @param event EventData.on_player_muted
events[e.on_player_muted] = function(event)
local player_name = get_player_name(event)
emit_event{
title = "Muted",
description = "A player has been muted",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
},
}
end
--- @param event EventData.on_player_unmuted
events[e.on_player_unmuted] = function(event)
local player_name = get_player_name(event)
emit_event{
title = "Un-Muted",
description = "A player has been un-muted",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
},
}
end
end
--- Kick
if config.player_kicks then
--- @param event EventData.on_player_kicked
events[e.on_player_kicked] = function(event)
if event.by_player then
local player_name = get_player_name(event)
local by_player = game.players[event.by_player]
emit_event{
title = "Kick",
description = "A player has been kicked",
color = Colors.orange,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player.name) },
{ name = "Reason", value = event.reason },
},
}
end
end
end
--- Promote and demote
if config.player_promotes then
--- @param event EventData.on_player_promoted
events[e.on_player_promoted] = function(event)
local player_name = get_player_name(event)
emit_event{
title = "Promote",
description = "A player has been promoted",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
},
}
end
--- @param event EventData.on_player_demoted
events[e.on_player_demoted] = function(event)
local player_name = get_player_name(event)
emit_event{
title = "Demote",
description = "A player has been demoted",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
},
}
end
end
--- @param event EventData.on_console_command
events[e.on_console_command] = function(event)
if event.player_index then
local player_name = get_player_name(event)
if config[event.command] then
emit_event{
title = event.command:gsub("^%l", string.upper),
description = "/" .. event.command .. " was used",
color = Colors.grey,
fields = {
{ name = "By", inline = true, value = append_playtime(player_name) },
{ name = "Details", value = event.parameters ~= "" and event.parameters or "<no details>" },
},
}
end
end
end
return {
events = events,
}

View File

@@ -0,0 +1,83 @@
--[[-- Addon Logging
Log some extra events to a separate file
]]
local config = require("modules.exp_legacy.config.logging")
local config_res = require("modules.exp_legacy.config.research")
local concat = table.concat
local write_file = helpers.write_file
--- Add a line to the log file
--- @param ... string
local function add_log_line(...)
write_file(config.file_name, concat({ ... }, " ") .. "\n", true, 0)
end
--- Add a line to the log file
--- @param line LocalisedString
local function add_log_line_locale(line)
write_file(config.file_name, line, true, 0)
end
--- @param event EventData.on_cargo_pod_finished_ascending
local function on_cargo_pod_finished_ascending(event)
if event.launched_by_rocket then
local force = event.cargo_pod.force
if force.rockets_launched >= config.rocket_launch_display_rate and force.rockets_launched % config.rocket_launch_display_rate == 0 then
add_log_line("[ROCKET]", force.rockets_launched, "rockets launched")
elseif config.rocket_launch_display[force.rockets_launched] then
add_log_line("[ROCKET]", force.rockets_launched, "rockets launched")
end
end
end
--- @param event EventData.on_pre_player_died
local function on_pre_player_died(event)
local player = assert(game.get_player(event.player_index))
local cause = event.cause
if cause then
local by_player = event.cause.player
add_log_line("[DEATH]", player.name, "died because of", by_player and by_player.name or event.cause.name)
else
add_log_line("[DEATH]", player.name, "died because of unknown reason")
end
end
--- @param event EventData.on_research_finished
local function on_research_finished(event)
if event.by_script then
return
end
local inf_research_level = config_res.inf_res[config_res.mod_set][event.research.name]
if inf_research_level and event.research.level >= inf_research_level then
add_log_line_locale{ "", "[RES] ", event.research.prototype.localised_name, " at level ", event.research.level - 1, "has been researched\n" }
else
add_log_line_locale{ "", "[RES] ", event.research.prototype.localised_name, "has been researched\n" }
end
end
--- @param event EventData.on_player_joined_game
local function on_player_joined_game(event)
local player = assert(game.get_player(event.player_index))
add_log_line("[JOIN]", player.name, "joined the game")
end
--- @param event EventData.on_player_left_game
local function on_player_left_game(event)
local player = assert(game.get_player(event.player_index))
add_log_line("[LEAVE]", game.players[event.player_index].name, config.disconnect_reason[event.reason])
end
local e = defines.events
return {
events = {
[e.on_cargo_pod_finished_ascending] = on_cargo_pod_finished_ascending,
[e.on_pre_player_died] = on_pre_player_died,
[e.on_research_finished] = on_research_finished,
[e.on_player_joined_game] = on_player_joined_game,
[e.on_player_left_game] = on_player_left_game,
}
}

View File

@@ -0,0 +1,181 @@
--[[-- Control - Fast Deconstruction
Makes trees which are marked for decon "decay" quickly to allow faster building
]]
local Gui = require("modules/exp_gui")
local Async = require("modules/exp_util/async")
local Roles = require("modules.exp_legacy.expcore.roles")
local PlayerData = require("modules.exp_legacy.expcore.player_data")
local HasEnabledDecon = PlayerData.Settings:combine("HasEnabledDecon")
HasEnabledDecon:set_default(false)
local random = math.random
local floor = math.floor
local min = math.min
--- @class TreeDeconCache
--- @field tick number The tick this cache is valid
--- @field player_index number
--- @field player LuaPlayer
--- @field force LuaForce
--- @field trees LuaEntity[]
--- @field tree_count number
--- @field permission "fast" | "allow" | "disallow"
--- @field task Async.AsyncReturn
local cache --- @type TreeDeconCache?
local remove_trees_async =
Async.register(function(task_data)
--- @cast task_data TreeDeconCache
if task_data.tree_count == 0 then
return Async.status.complete()
end
local head = task_data.tree_count
local trees = task_data.trees
local max_remove = floor(head / 100) + 1
local remove_count = min(random(0, max_remove), head)
for i = 1, remove_count do
local index = random(1, head)
local entity = trees[index]
trees[index] = trees[head]
head = head - 1
if entity and entity.valid then
entity.destroy()
end
end
task_data.tree_count = head
return Async.status.continue(task_data)
end)
--- Check the permission the player has
--- @param player LuaPlayer
--- @return "fast" | "allow" | "disallow"
local function get_permission(player)
if Roles.player_allowed(player, "fast-tree-decon") then
return HasEnabledDecon:get(player) and "fast" or "allow"
elseif Roles.player_allowed(player, "standard-decon") then
return "allow"
else
return "disallow"
end
end
--- Return or build the cache for a player
--- @param player_index number
--- @return TreeDeconCache
local function get_player_cache(player_index)
-- Return the current cache if it is valid
if cache and cache.tick == game.tick and cache.player_index == player_index then
return cache
end
-- Create a new cache if the previous on is in use
if not cache or cache.task and not cache.task.completed then
cache = {} --[[@as any]]
end
local player = assert(game.get_player(player_index))
cache.tick = game.tick
cache.player_index = player_index
cache.player = player
cache.force = player.force --[[ @as LuaForce ]]
cache.tree_count = 0
cache.trees = {}
cache.permission = get_permission(player)
cache.task = remove_trees_async:start_soon(cache)
return cache
end
-- Left menu button to toggle between fast decon and normal decon marking
Gui.toolbar.create_button{
name = "toggle-tree-decon",
sprite = "entity/tree-01",
tooltip = { "exp_fast-decon.tooltip-main" },
auto_toggle = true,
visible = function(player, _)
return Roles.player_allowed(player, "fast-tree-decon")
end
}:on_click(function(def, player, element)
local state = Gui.toolbar.get_button_toggled_state(def, player)
HasEnabledDecon:set(player, state)
player.print{ "exp_fast-decon.chat-toggle", state and { "exp_fast-decon.chat-enabled" } or { "exp_fast-decon.chat-disabled" } }
end)
-- Add trees to queue when marked, only allows simple entities and for players with role permission
--- @param event EventData.on_marked_for_deconstruction
local function on_marked_for_deconstruction(event)
-- Check player and entity are valid
local entity = event.entity
local player_index = event.player_index
if not player_index or not entity.valid then
return
end
-- If it has a last user then either do nothing or cancel decon
local last_user = entity.last_user
local player_cache = get_player_cache(player_index)
if last_user then
if player_cache.permission == "disallow" then
entity.cancel_deconstruction(player_cache.force)
end
return
end
-- Allow fast decon on no last user and not cliff
if player_cache.permission == "fast" and entity.type ~= "cliff" then
local head = player_cache.tree_count + 1
player_cache.tree_count = head
player_cache.trees[head] = entity
end
end
--- Clear trees when hit with a car
--- @param event EventData.on_entity_damaged
local function on_entity_damaged(event)
-- Check it was an impact from a force
if not (event.damage_type.name == "impact" and event.force) then
return
end
-- Check the entity hit was a tree or rock
if not (event.entity.type == "tree" or event.entity.type == "simple-entity") then
return
end
-- Check the case was a car
if (not event.cause) or (event.cause.type ~= "car") then
return
end
-- Get a valid player as the driver
local driver = event.cause.get_driver()
if not driver then return end
if driver.object_name ~= "LuaPlayer" then
driver = driver.player
if not driver then return end
end
-- Mark the entity to be removed
local allow = get_player_cache(driver.index)
if allow == "fast" and HasEnabledDecon:get(driver) then
event.entity.destroy()
else
event.entity.order_deconstruction(event.force, driver)
end
end
local e = defines.events
return {
events = {
[e.on_entity_damaged] = on_entity_damaged,
[e.on_marked_for_deconstruction] = on_marked_for_deconstruction,
}
}

View File

@@ -0,0 +1,100 @@
--[[-- Control - Help Bubbles
Adds friendly biters that walk around and give helpful messages
]]
local Async = require("modules/exp_util/async")
local Storage = require("modules/exp_util/storage")
local config = require("modules.exp_legacy.config.compilatron")
--- @type table<string, Async.AsyncReturn>
local persistent_locations = {}
Storage.register(persistent_locations, function(tbl)
persistent_locations = tbl
end)
--- @class speech_bubble_task_param
--- @field entity LuaEntity
--- @field messages LocalisedString[]
--- @field previous_message LuaEntity?
--- @field current_message_index number
--- Used by create_entity within speech_bubble_async
local speech_bubble_param = {
name = "compi-speech-bubble",
position = { 0, 0 },
}
--- Cycle between a set category of messages above an entity
local speech_bubble_task =
Async.register(function(task)
--- @cast task speech_bubble_task_param
local entity = task.entity
if not entity.valid then
return Async.status.complete()
end
local index = task.current_message_index
if index > #task.messages then
index = 1
end
local previous_message = task.previous_message
if previous_message and previous_message.valid then
previous_message.destroy()
end
speech_bubble_param.source = entity
speech_bubble_param.text = task.messages[index]
task.previous_message = entity.surface.create_entity(speech_bubble_param)
task.current_message_index = index + 1
return Async.status.delay(config.message_cycle, task)
end)
--- Register an entity to start spawning speech bubbles
--- @param entity LuaEntity the entity which will have messages spawn from it
--- @param messages LocalisedString[] the messages which should be shown
--- @param starting_index number? the message index to start at, default 1
--- @return Async.AsyncReturn
local function register_entity(entity, messages, starting_index)
return speech_bubble_task{
entity = entity,
messages = messages,
current_message_index = starting_index or 1,
}
end
--- Check all persistent locations from the config are active
local function check_persistent_locations()
for name, location in pairs(config.locations) do
local task = persistent_locations[name]
if task and not task.completed then
goto continue
end
local surface = game.get_surface(location.spawn_surface)
if not surface then
goto continue
end
local position = surface.find_non_colliding_position(location.entity_name, location.spawn_position, 1.5, 0.5)
if not position then
goto continue
end
local entity = surface.create_entity{ name = location.entity_name, position = position, force = game.forces.neutral }
if not entity then
goto continue
end
persistent_locations[name] = register_entity(entity, location.messages)
::continue::
end
end
return {
on_nth_tick = {
[config.message_cycle] = check_persistent_locations,
},
register_entity = register_entity,
}

View File

@@ -0,0 +1,34 @@
--[[-- Control Insert Pickup
Automatically pick up the items in the inserts hand when you mine it
]]
local controllers_with_inventory = {
[defines.controllers.character] = true,
[defines.controllers.god] = true,
[defines.controllers.editor] = true,
}
--- @param event EventData.on_player_mined_entity
local function on_player_mined_entity(event)
local entity = event.entity
if not entity.valid or entity.type ~= "inserter" or entity.drop_target then
return
end
local item_entity = entity.surface.find_entity("item-on-ground", entity.drop_position)
if item_entity then
local player = assert(game.get_player(event.player_index))
if controllers_with_inventory[player.controller_type] then
player.mine_entity(item_entity)
end
end
end
local e = defines.events
return {
events = {
[e.on_player_mined_entity] = on_player_mined_entity
}
}

View File

@@ -0,0 +1,27 @@
--[[-- Control - Inventory Clear
Will move players items to spawn when they are banned or kicked, option to clear on leave
]]
local ExpUtil = require("modules/exp_util")
local events = require("modules.exp_legacy.config.inventory_clear")
--- @param event { player_index: number }
local function clear_items(event)
local player = assert(game.get_player(event.player_index))
local inventory = assert(player.get_main_inventory())
ExpUtil.transfer_inventory_to_surface{
inventory = inventory,
surface = game.planets.nauvis.surface,
name = "iron-chest",
allow_creation = true,
}
end
local event_handlers = {}
for _, event_name in ipairs(events) do
event_handlers[event_name] = clear_items
end
return {
events = event_handlers,
}

View File

@@ -0,0 +1,231 @@
--[[-- Control - Mine Depletion
Marks mining drills for deconstruction when resources deplete
]]
local Async = require("modules/exp_util/async")
local config = require("modules.exp_legacy.config.miner")
local floor = math.floor
--- Orders the deconstruction of an entity by its own force
local order_deconstruction_async =
Async.register(function(entity)
--- @cast entity LuaEntity
entity.order_deconstruction(entity.force)
end)
--- Reliability get the drop target of an entity
--- @param entity LuaEntity
--- @return LuaEntity?
local function get_drop_chest(entity)
-- First check the direct drop target
local target = entity.drop_target
if target and (target.type == "container" or target.type == "logistic-container" or target.type == "infinity-container") then
return target
end
-- Then check all entities at the drop position
local entities = entity.surface.find_entities_filtered{
position = entity.drop_position,
type = { "container", "logistic-container", "infinity-container" },
}
return #entities > 0 and entities[1] or nil
end
--- Check if an entity should has checked performed
--- @param entity LuaEntity
--- @return boolean
local function prevent_deconstruction(entity)
-- Already waiting to be deconstructed
if not entity.valid or entity.to_be_deconstructed() then
return true
end
-- Not minable, selectable, or deconstructive
if not entity.minable or not entity.prototype.selectable_in_game or entity.has_flag("not-deconstructable") then
return true
end
-- Is connected to the circuit network
local red_write_connection = entity.get_wire_connector(defines.wire_connector_id.circuit_red, false)
local green_write_connection = entity.get_wire_connector(defines.wire_connector_id.circuit_green, false)
if red_write_connection and red_write_connection.connection_count > 0
or green_write_connection and green_write_connection.connection_count > 0 then
return true
end
return false
end
--- Check if an output chest should be deconstructed
--- @param entity LuaEntity
local function try_deconstruct_output_chest(entity)
-- Get a valid chest as the target
local target = get_drop_chest(entity)
if not target or prevent_deconstruction(target) then
return
end
-- Get all adjacent mining drills and inserters
local entities = target.surface.find_entities_filtered{
type = { "mining-drill", "inserter" },
to_be_deconstructed = false,
area = {
{ target.position.x - 1, target.position.y - 1 },
{ target.position.x + 1, target.position.y + 1 }
},
}
-- Check if any other entity is using this chest
for _, other in ipairs(entities) do
if other ~= entity and get_drop_chest(other) == target then
return
end
end
-- Deconstruct the chest
order_deconstruction_async:start_after(10, target)
end
--- Check if a miner should be deconstructed
--- @param entity LuaEntity
local function try_deconstruct_miner(entity)
-- Check if the miner should be deconstructed
if prevent_deconstruction(entity) then
return
end
-- Check if there are any resources remaining for the miner
local surface = entity.surface
local resources = surface.find_entities_filtered{
type = "resource",
area = entity.mining_area,
}
for _, resource in ipairs(resources) do
if resource.amount > 0 then
return
end
end
-- Deconstruct the miner
order_deconstruction_async:start_after(10, entity)
-- Try deconstruct the output chest
if config.chest then
try_deconstruct_output_chest(entity)
end
-- Skip pipe build if not required
if not config.fluid or #entity.fluidbox == 0 then
return
end
-- Build pipes if the miner used fluid
local position = entity.position
local create_entity_position = { x = position.x, y = position.y }
local create_entity_param = { name = "entity-ghost", inner_name = "pipe", force = entity.force, position = create_entity_position }
local create_entity = surface.create_entity
create_entity(create_entity_param)
-- Find all the entities to connect to
local bounding_box = entity.bounding_box
local search_area = {
{ bounding_box.left_top.x - 1, bounding_box.left_top.y - 1 },
{ bounding_box.right_bottom.x + 1, bounding_box.right_bottom.y + 1 },
}
local entities = surface.find_entities_filtered{ area = search_area, type = { "mining-drill", "pipe", "pipe-to-ground", "infinity-pipe" } }
local ghosts = surface.find_entities_filtered{ area = search_area, ghost_type = { "pipe", "pipe-to-ground", "infinity-pipe" } }
table.insert_array(entities, ghosts)
-- Check which directions to add pipes in
local pos_x, pos_y, neg_x, neg_y = false, false, false, false
for _, other in ipairs(entities) do
if (other.position.x > position.x) and (other.position.y == position.y) then
pos_x = true
elseif (other.position.x < position.x) and (other.position.y == position.y) then
neg_x = true
elseif (other.position.x == position.x) and (other.position.y > position.y) then
pos_y = true
elseif (other.position.x == position.x) and (other.position.y < position.y) then
neg_y = true
end
end
-- Build the pipes
if pos_x then
create_entity_position.y = floor(position.y)
for x = position.x + 1, bounding_box.right_bottom.x do
create_entity_position.x = x
create_entity(create_entity_param)
end
end
if neg_x then
create_entity_position.y = floor(position.y)
for x = floor(bounding_box.left_top.x), floor(position.x - 1) do
create_entity_position.x = x
create_entity(create_entity_param)
end
end
if pos_y then
create_entity_position.x = floor(position.x)
for y = floor(position.y + 1), floor(bounding_box.right_bottom.y) do
create_entity_position.y = y
create_entity(create_entity_param)
end
end
if neg_y then
create_entity_position.x = floor(position.x)
for y = floor(bounding_box.left_top.y), floor(position.y - 1) do
create_entity_position.y = y
create_entity(create_entity_param)
end
end
end
--- Get the max mining radius
local max_mining_radius = 0
for _, proto in pairs(prototypes.get_entity_filtered{ { filter = "type", type = "mining-drill" } }) do
if proto.mining_drill_radius > max_mining_radius then
max_mining_radius = proto.mining_drill_radius
end
end
--- Try deconstruct a miner when its resources deplete
--- @param event EventData.on_resource_depleted
local function on_resource_depleted(event)
local resource = event.entity
if resource.prototype.infinite_resource then
return
end
-- Find all mining drills within the area
local position = resource.position
local drills = resource.surface.find_entities_filtered{
type = "mining-drill",
area = {
{ position.x - max_mining_radius, position.y - max_mining_radius },
{ position.x + max_mining_radius, position.y + max_mining_radius },
},
}
-- Check which could have reached this resource
for _, drill in pairs(drills) do
local radius = drill.prototype.mining_drill_radius
local dx = math.abs(drill.position.x - resource.position.x)
local dy = math.abs(drill.position.y - resource.position.y)
if dx <= radius and dy <= radius then
try_deconstruct_miner(drill)
end
end
end
local e = defines.events
return {
events = {
[e.on_resource_depleted] = on_resource_depleted,
},
}

View File

@@ -0,0 +1,56 @@
--[[-- Control - Nuke Protection
Disable new players from having certain items in their inventory, most commonly nukes
]]
local ExpUtil = require("modules/exp_util")
local Roles = require("modules.exp_legacy.expcore.roles")
local config = require("modules.exp_legacy.config.nukeprotect")
--- Check all items in the given inventory
--- @param player LuaPlayer
--- @param type defines.inventory
--- @param banned_items string[]
local function check_items(player, type, banned_items)
-- If the player has perms to be ignored, then they should be
if config.ignore_permission and Roles.player_allowed(player, config.ignore_permission) then return end
if config.ignore_admins and player.admin then return end
local items = {} --- @type LuaItemStack[]
local inventory = assert(player.get_inventory(type))
-- Check what items the player has
for i = 1, #inventory do
local item = inventory[i]
if item.valid_for_read and banned_items[item.name] then
player.print{ "exp_nuke-protection.chat-found", item.prototype.localised_name }
items[#items + 1] = item
end
end
-- Move any items they aren't allowed
ExpUtil.move_items_to_surface{
items = items,
surface = game.planets.nauvis.surface,
allow_creation = true,
name = "iron-chest",
}
end
--- Add event handlers for the different inventories
local events = {}
for index, inventory in ipairs(config.inventories) do
if next(inventory.items) then
local assert_msg = "invalid event, no player index, index: " .. index
--- @param event { player_index: number }
events[inventory.event] = function(event)
local player_index = assert(event.player_index, assert_msg)
local player = assert(game.get_player(player_index))
if player and player.valid then
check_items(player, inventory.inventory, inventory.items)
end
end
end
end
return {
events = events,
}

View File

@@ -0,0 +1,27 @@
--[[-- Control - Pollution Grading
Makes pollution look much nice of the map, ie not one big red mess
]]
local config = require("modules.exp_legacy.config.pollution_grading")
local function check_surfaces()
local max_reference = 0
for _, surface in pairs(game.surfaces) do
local reference = surface.get_pollution(config.reference_point)
if reference > max_reference then
max_reference = reference
end
end
local max = max_reference * config.max_scalar
local min = max * config.min_scalar
local settings = game.map_settings.pollution
settings.expected_max_per_chunk = max
settings.min_to_show_per_chunk = min
end
return {
on_nth_tick = {
[config.update_delay * 3600] = check_surfaces,
}
}

View File

@@ -0,0 +1,47 @@
--[[-- Control - Projection Jail
When a player triggers protection multiple times they are automatically jailed
]]
local ExpUtil = require("modules/exp_util")
local Storage = require("modules/exp_util/storage")
local Jail = require("modules.exp_legacy.modules.control.jail")
local Protection = require("modules.exp_legacy.modules.control.protection")
local format_player_name = ExpUtil.format_player_name_locale
--- Stores how many times the repeat violation was triggered
--- @type table<number, number>
local repeat_count = {}
Storage.register(repeat_count, function(tbl)
repeat_count = tbl
end)
--- When a protection is triggered increment their counter and jail if needed
local function on_repeat_violation(event)
local player = assert(game.get_player(event.player_index))
-- Increment the counter
local count = (repeat_count[player.index] or 0) + 1
repeat_count[player.index] = count
-- Jail if needed
if count >= 3 then
Jail.jail_player(player, "<protection>", "Removed too many protected entities, please wait for a moderator.")
game.print{ "exp_protection-jail.chat-jailed", format_player_name(player) }
end
end
--- Clear the counter when they leave the game (stops a build up of data)
--- @param event EventData.on_player_left_game
local function on_player_left_game(event)
repeat_count[event.player_index] = nil
end
local e = defines.events
return {
events = {
[Protection.events.on_repeat_violation] = on_repeat_violation,
[e.on_player_left_game] = on_player_left_game,
}
}

View File

@@ -0,0 +1,38 @@
--[[-- Control - Report Jail
When a player is reported, the player is automatically jailed if the combined playtime of the reporters exceeds the reported player
]]
local ExpUtil = require("modules/exp_util")
local Jail = require("modules.exp_legacy.modules.control.jail")
local Reports = require("modules.exp_legacy.modules.control.reports")
local max = math.max
local format_player_name = ExpUtil.format_player_name_locale
--- Returns the playtime of the reporter. Used when calculating the total playtime of all reporters
--- @param player LuaPlayer
--- @param by_player_name string
--- @param reason string
--- @return number
local function reporter_playtime(player, by_player_name, reason)
local by_player = game.get_player(by_player_name)
return by_player and by_player.online_time or 0
end
--- Check if the player has too many reports against them (based on playtime)
local function on_player_reported(event)
local player = assert(game.get_player(event.player_index))
local total_playtime = Reports.count_reports(player, reporter_playtime)
-- Total time greater than the players own time, or 30 minutes, which ever is greater
if Reports.count_reports(player) > 1 and total_playtime > max(player.online_time * 2, 108000) then
Jail.jail_player(player, "<reports>", "Reported by too many players, please wait for a moderator.")
game.print{ "exp_report-jail.chat-jailed", format_player_name(player) }
end
end
return {
events = {
[Reports.events.on_player_reported] = on_player_reported,
}
}

View File

@@ -0,0 +1,292 @@
--[[-- Control - Spawn Area
Adds a custom spawn area with chests and afk turrets
]]
local config = require("modules.exp_legacy.config.spawn_area")
--- Apply an offset to a LuaPosition
--- @param position MapPosition
--- @param offset MapPosition
--- @return MapPosition.0
local function apply_offset(position, offset)
return {
x = (position.x or position[1]) + (offset.x or offset[1]),
y = (position.y or position[2]) + (offset.y or offset[2])
}
end
--- Apply offset to an array of positions
--- @param positions table
--- @param offset MapPosition
--- @param x_index number
--- @param y_index number
local function apply_offset_to_array(positions, offset, x_index, y_index)
local x = (offset.x or offset[1])
local y = (offset.y or offset[2])
for _, position in ipairs(positions) do
position[x_index] = position[x_index] + x
position[y_index] = position[y_index] + y
end
end
-- Apply the offsets to all config values
apply_offset_to_array(config.turrets.locations, config.turrets.offset, 1, 2)
apply_offset_to_array(config.afk_belts.locations, config.afk_belts.offset, 1, 2)
apply_offset_to_array(config.water.locations, config.water.offset, 1, 2)
apply_offset_to_array(config.water.locations, config.water.offset, 1, 2)
apply_offset_to_array(config.entities.locations, config.entities.offset, 2, 3)
--- Get or create the force used for entities in spawn
--- @return LuaForce
local function get_spawn_force()
local force = game.forces["spawn"]
if force and force.valid then
return force
end
force = game.create_force("spawn")
force.set_cease_fire("player", true)
game.forces["player"].set_cease_fire("spawn", true)
return force
end
--- Protects an entity from player interaction
--- @param entity LuaEntity
local function protect_entity(entity)
if entity and entity.valid then
entity.destructible = false
entity.minable = false
entity.rotatable = false
entity.operable = false
end
end
--- Will spawn all infinite ammo turrets and keep them refilled
local function update_turrets()
local force = get_spawn_force()
for _, position in pairs(config.turrets.locations) do
-- Get or create a valid turret
local surface = assert(game.get_surface("nauvis"))
local turret = surface.find_entity("gun-turret", position)
if not turret or not turret.valid then
turret = surface.create_entity{ name = "gun-turret", position = position, force = force }
if not turret then
goto continue
end
protect_entity(turret)
end
-- Adds ammo to the turret
local inv = turret.get_inventory(defines.inventory.turret_ammo)
if inv and inv.can_insert{ name = config.turrets.ammo_type, count = 10 } then
inv.insert{ name = config.turrets.ammo_type, count = 10 }
end
::continue::
end
end
--- Details required to create a 2x2 belt circle
local belt_details = {
{ -0.5, -0.5, defines.direction.east },
{ 0.5, -0.5, defines.direction.south },
{ -0.5, 0.5, defines.direction.north },
{ 0.5, 0.5, defines.direction.west },
}
--- Makes a 2x2 afk belt at the locations in the config
--- @param surface LuaSurface
--- @param offset MapPosition
local function create_belts(surface, offset)
local belt_type = config.afk_belts.belt_type
for _, position in pairs(config.afk_belts.locations) do
position = apply_offset(position, offset)
for _, belt in pairs(belt_details) do
local pos = apply_offset(position, belt)
local entity = surface.create_entity{ name = belt_type, position = pos, force = "neutral", direction = belt[3] }
if entity and config.afk_belts.protected then
protect_entity(entity)
end
end
end
end
-- Generates extra tiles in a set pattern as defined in the config
--- @param surface LuaSurface
--- @param offset MapPosition
local function create_pattern_tiles(surface, offset)
local tiles_to_make = {}
local pattern_tile = config.pattern.pattern_tile
for index, position in pairs(config.pattern.locations) do
tiles_to_make[index] = { name = pattern_tile, position = apply_offset(position, offset) }
end
surface.set_tiles(tiles_to_make)
end
-- Generates extra water as defined in the config
--- @param surface LuaSurface
--- @param offset MapPosition
local function create_water_tiles(surface, offset)
local tiles_to_make = {}
local water_tile = config.water.water_tile
for _, position in pairs(config.water.locations) do
table.insert(tiles_to_make, { name = water_tile, position = apply_offset(position, offset) })
end
surface.set_tiles(tiles_to_make)
end
--- Generates the entities that are in the config
--- @param surface LuaSurface
--- @param offset MapPosition
local function create_entities(surface, offset)
for _, entity_details in pairs(config.entities.locations) do
local pos = apply_offset({ entity_details[2], entity_details[3] }, offset)
local entity = surface.create_entity{ name = entity_details[1], position = pos, force = "neutral" }
if entity and config.entities.protected then
protect_entity(entity)
end
entity.operable = config.entities.operable
end
end
--- Generates an area with no water or entities, no water area is larger
--- @param surface LuaSurface
--- @param offset MapPosition
local function clear_spawn_area(surface, offset)
local get_tile = surface.get_tile
-- Make sure a non water tile is used for filling
--- @diagnostic disable-next-line Incorrect Api Type: https://forums.factorio.com/viewtopic.php?f=233&t=109145&p=593761&hilit=get_tile#p593761
local starting_tile = get_tile(offset)
local fill_tile = starting_tile.collides_with("player") and "landfill" or starting_tile.name
local fill_radius = config.spawn_area.landfill_radius
local fill_radius_sqr = fill_radius ^ 2
-- Select the deconstruction tile
local decon_radius = config.spawn_area.deconstruction_radius
local decon_tile = config.spawn_area.deconstruction_tile or fill_tile
local tiles_to_make = {}
local tile_radius_sqr = config.spawn_area.tile_radius ^ 2
for x = -fill_radius, fill_radius do -- loop over x
local x_sqr = (x + 0.5) ^ 2
for y = -fill_radius, fill_radius do -- loop over y
local y_sqr = (y + 0.5) ^ 2
local dst = x_sqr + y_sqr
local pos = apply_offset({ x, y }, offset)
if dst < tile_radius_sqr then
-- If it is inside the decon radius always set the tile
tiles_to_make[#tiles_to_make + 1] = { name = decon_tile, position = pos }
--- @diagnostic disable-next-line Incorrect Api Type: https://forums.factorio.com/viewtopic.php?f=233&t=109145&p=593761&hilit=get_tile#p593761
elseif dst < fill_radius_sqr and get_tile(pos).collides_with("player") then
-- If it is inside the fill radius only set the tile if it is water
tiles_to_make[#tiles_to_make + 1] = { name = fill_tile, position = pos }
end
end
end
-- Remove entities then set the tiles
local entities_to_remove = surface.find_entities_filtered{ position = offset, radius = decon_radius, name = "character", invert = true }
for _, entity in pairs(entities_to_remove) do
entity.destroy()
end
surface.set_tiles(tiles_to_make)
end
--- Spawn the resource tiles
--- @param surface LuaSurface
--- @param offset MapPosition
local function create_resources_tiles(surface, offset)
for _, resource in ipairs(config.resource_tiles.resources) do
if resource.enabled then
local pos = apply_offset(resource.offset, offset)
for x = pos.x, pos.x + resource.size[1] do
for y = pos.y, pos.y + resource.size[2] do
surface.create_entity{ name = resource.name, amount = resource.amount, position = { x, y } }
end
end
end
end
end
--- Spawn the resource entities
--- @param surface LuaSurface
--- @param offset MapPosition
local function create_resource_patches(surface, offset)
for _, resource in ipairs(config.resource_patches.resources) do
if resource.enabled then
local pos = apply_offset(resource.offset, offset)
for i = 1, resource.num_patches do
surface.create_entity{ name = resource.name, amount = resource.amount, position = { pos.x + resource.offset_next[1] * (i - 1), pos.y + resource.offset_next[2] * (i - 1) } }
end
end
end
end
local on_nth_tick = {}
if config.turrets.enabled then
--- Refill the ammo in the spawn turrets
on_nth_tick[config.turrets.refill_time] = function()
if game.tick < 10 then return end
update_turrets()
end
end
if config.resource_refill_nearby.enabled then
---
on_nth_tick[config.resource_refill_nearby.refill_time] = function()
if game.tick < 10 then return end
local force = game.forces.player
local surface = assert(game.get_surface("nauvis"))
local entities = surface.find_entities_filtered{
position = force.get_spawn_position(surface),
radius = config.resource_refill_nearby.range,
name = config.resource_refill_nearby.resources_name
}
for _, ore in ipairs(entities) do
ore.amount = ore.amount + math.random(config.resource_refill_nearby.amount[1], config.resource_refill_nearby.amount[2])
end
end
end
--- When the first player joins create the spawn area
--- @param event EventData.on_player_created
local function on_player_created(event)
if event.player_index ~= 1 then return end
local player = assert(game.get_player(event.player_index))
local surface = player.physical_surface
local offset = { x = 0, y = 0 }
clear_spawn_area(surface, offset)
if config.pattern.enabled then create_pattern_tiles(surface, offset) end
if config.water.enabled then create_water_tiles(surface, offset) end
if config.afk_belts.enabled then create_belts(surface, offset) end
if config.entities.enabled then create_entities(surface, offset) end
if config.resource_tiles.enabled then create_resources_tiles(surface, offset) end
if config.resource_patches.enabled then create_resource_patches(surface, offset) end
if config.turrets.enabled then update_turrets() end
player.force.set_spawn_position(offset, surface)
player.teleport(offset, surface)
end
local e = defines.events
return {
on_nth_tick = on_nth_tick,
events = {
[e.on_player_created] = on_player_created,
}
}

View File

@@ -0,0 +1,104 @@
--[[-- Control - Station Auto Name
Automatically name stations when they are placed based on closest resource and direction from spawn
]]
local config = require("modules.exp_legacy.config.station_auto_name")
local get_direction do
local directions = {
["W"] = -0.875,
["NW"] = -0.625,
["N"] = -0.375,
["NE"] = -0.125,
["E"] = 0.125,
["SE"] = 0.375,
["S"] = 0.625,
["SW"] = 0.875,
}
--- Get the direction of a position from the centre of the surface
--- @param position MapPosition
--- @return string
function get_direction(position)
local angle = math.atan2(position.y, position.x) / math.pi
for direction, required_angle in pairs(directions) do
if angle < required_angle then
return direction
end
end
return "W"
end
end
-- Custom strings are used to detect backer names from ghosts
local custom_string = " *"
local custom_string_len = #custom_string
--- Change the name of a station when it is placed
--- @param event EventData.on_built_entity | EventData.on_robot_built_entity
local function rename_station(event)
local entity = event.entity
local name = entity.name
if name == "entity-ghost" and entity.ghost_name == "train-stop" then
local backer_name = entity.backer_name
if backer_name ~= "" then
entity.backer_name = backer_name .. custom_string
end
elseif name == "train-stop" then
-- Restore the backer name
local backer_name = entity.backer_name or ""
if backer_name:sub(-custom_string_len) == custom_string then
entity.backer_name = backer_name:sub(1, -custom_string_len - 1)
return
end
-- Find the closest resource
local icon = ""
local item_name = ""
local bounding_box = entity.bounding_box
local resources = entity.surface.find_entities_filtered{ position = entity.position, radius = 250, type = "resource" }
if #resources > 0 then
local closest_recourse --- @type LuaEntity?
local closest_distance = 250 * 250 -- search radius + 1
local px, py = bounding_box.left_top.x, bounding_box.left_top.y
-- Check which recourse is closest
for _, resource in ipairs(resources) do
local dx = px - resource.bounding_box.left_top.x
local dy = py - resource.bounding_box.left_top.y
local distance = (dx * dx) + (dy * dy)
if distance < closest_distance then
closest_distance = distance
closest_recourse = resource
end
end
-- Set the item name and icon
if closest_recourse then
item_name = closest_recourse.name:gsub("^%l", string.upper):gsub("-", " ") -- remove dashes and making first letter capital
local product = closest_recourse.prototype.mineable_properties.products[1]
icon = string.format("[img=%s.%s]", product.type, product.name)
end
end
-- Rename the station
entity.backer_name = config.station_name
:gsub("__icon__", icon)
:gsub("__item_name__", item_name)
:gsub("__backer_name__", entity.backer_name)
:gsub("__direction__", get_direction(entity.position))
:gsub("__x__", math.floor(entity.position.x))
:gsub("__y__", math.floor(entity.position.y))
end
end
local e = defines.events
return {
events = {
[e.on_built_entity] = rename_station,
[e.on_robot_built_entity] = rename_station,
}
}

View File

@@ -284,7 +284,7 @@ Elements.container = Gui.define("autofill/container")
})
-- Setup the player data, this is used by section and item category so needs to be done here
local player = assert(game.get_player(parent.player_index))
local player = Gui.get_player(parent)
--- @type table<string, ExpGui_Autofill.entity_settings>
local player_data = def.data[player] or table.deep_copy(config.default_entities)
def.data[player] = player_data

View File

@@ -484,7 +484,7 @@ local function on_entity_settings_pasted(event)
planner.set_mapper(1, "to", mapper)
-- Apply the planner
local player = assert(game.get_player(event.player_index))
local player = Gui.get_player(event)
player.surface.upgrade_area{
area = destination.bounding_box,
item = planner,

View File

@@ -463,7 +463,7 @@ Gui.toolbar.create_button{
--- Recalculate and apply the bonus for a player
local function recalculate_bonus(event)
local player = assert(game.get_player(event.player_index))
local player = Gui.get_player(event)
if event.name == Roles.events.on_role_assigned or event.name == Roles.events.on_role_unassigned then
-- If the player's roles changed then we will need to recalculate their limit
Elements.bonus_used._clear_points_limit_cache(player)

View File

@@ -375,3 +375,69 @@ caption-set-location=Set
type-player=Player
type-static=Static
type-loop=Loop
[exp_afk-kick]
kick-message=All players were kicked because everyone was AFK.
[exp_chat-auto-reply]
chat-reply=[Compilatron] __1__
chat-disallowed=You can't use global chat commands
reply-online=There are __1__ players online
reply-players=There have been __1__ players on this map
reply-dev=Cooldude2606 is a dev for this server and makes the scenario and is not a factorio dev.
reply-softmod=A softmod is a custom scenario that runs on this server, an example is the player list.
reply-clusterio=Clusterio is a factorio server hosting platform allowing internet enabled mods.
reply-blame=Blame __1__ for what just happened!
reply-afk=You're afk? Look at __1__, that player has been afk for: __2__
reply-evolution=Current evolution factor is __1__
reply-magic=Fear the admin magic (ノ゚∀゚)ノ⌒・*:.。. .。.:*・゜゚・*☆
reply-aids=≖ ‿ ≖ Fear the aids of a public server ≖ ‿ ≖
reply-riot=(admins) ┬┴┬┴┤ᵒ_ᵒ)├┬┴┬┴ \(´ω` )/\ (  ´)/\ ( ´ω`)/ (rest of server)
reply-loops=NO LOOPS; LOOPS ARE BAD; JUST NO LOOPS!!!!!; IF YOU MAKE A LOOP.... IT WILL NOT END WELL!!!!!!!
reply-lenny=( ͡° ͜ʖ ͡°)
reply-hodor=Hodor
reply-popcorn-1=Heating the oil and waiting for the popping sound...
reply-popcorn-2=__1__ your popcorn is finished. Lean backwards and watch the drama unfold.
reply-snaps-1=Pouring the glasses and finding the correct song book...
reply-snaps-2=Singing a song...🎤🎶
reply-snaps-3=schkål, my friends!
reply-cocktail-1= 🍸 Inintiating mind reading unit... 🍸
reply-cocktail-2= 🍸 Mixing favourite ingredients of __1__ 🍸
reply-cocktail-3=🍸 __1__ your cocktail is done.🍸
reply-coffee-1= ☕ Boiling the water and grinding the coffee beans... ☕
reply-coffee-2= ☕ __1__ we ran out of coffe beans! Have some tea instead. ☕
reply-pizza-1= 🍕 Finding nearest pizza supplier... 🍕
reply-pizza-2= 🍕 Figuring out the favourite pizza of __1__ 🍕
reply-pizza-3= 🍕 __1__ your pizza is here! 🍕
reply-tea-1= ☕ Boiling the water... ☕
reply-tea-2= ☕ __1__ your tea is done! ☕
reply-mead-1= Filling the drinking horn
reply-mead-2= Skål!
reply-beer-1= 🍺 Pouring A Glass 🍺
reply-beer-2= 🍻 Chears Mate 🍻
[exp_chat-popup]
flying-text-message=__1__: __2__
flying-text-ping=You have been mentioned in chat by __1__.
[exp_damage-popup]
flying-text-health=__1__
flying-text-damage=__1__
[exp_deconstruction-log]
chat-admin=__1__ tried to deconstruct from __2__ to __3__ which has __4__ items.
[exp_fast-decon]
tooltip-main=Toggle fast tree decon
chat-enabled=enabled
chat-disabled=disabled
chat-toggle=Fast decon has been __1__
[exp_nuke-protection]
chat-found=You cannot have __1__ in your inventory, so it was placed into the chests at spawn.
[exp_protection-jail]
chat-jailed=__1__ was jailed because they removed too many protected entities. Please wait for a moderator.
[exp_report-jail]
chat-jailed=__1__ was jailed because they were reported too many times. Please wait for a moderator.

View File

@@ -375,3 +375,69 @@ caption-set-location=設
type-player=用戶
type-static=靜態
type-loop=循環
[exp_afk-kick]
kick-message=因地圖中沒有活躍玩家,所以所有人都已被請離。
[exp_chat-auto-reply]
chat-reply=[Compilatron] __1__
chat-disallowed=你沒有權限使用這個指令。
reply-online=現在有 __1__ 人上線。
reply-players=本地圖現在有 __1__ 人曾上線。
reply-dev=Cooldude2606 只是本場境的開發者
reply-softmod=這裹用了自設情境。
reply-clusterio=Clusterio is a factorio server hosting platform allowing internet enabled mods.
reply-blame=責怪 __1__ 吧。
reply-afk=看看 __1__, 他已掛機 __2__ 。
reply-evolution=現在敵人進化度為 __1__ 。
reply-magic=Fear the admin magic (ノ゚∀゚)ノ⌒・*:.。. .。.:*・゜゚・*☆
reply-aids=≖ ‿ ≖ Fear the aids of a public server ≖ ‿ ≖
reply-riot=(admins) ┬┴┬┴┤ᵒ_ᵒ)├┬┴┬┴ \(´ω` )/\ (  ´)/\ ( ´ω`)/ (rest of server)
reply-loops=架設迴旋處最終後果都不好。
reply-lenny=( ͡° ͜ʖ ͡°)
reply-hodor=Hodor
reply-popcorn-1=Heating the oil and waiting for the popping sound...
reply-popcorn-2=__1__ your popcorn is finished. Lean backwards and watch the drama unfold.
reply-snaps-1=Pouring the glasses and finding the correct song book...
reply-snaps-2=Singing a song...🎤🎶
reply-snaps-3=schkål, my friends!
reply-cocktail-1= 🍸 Inintiating mind reading unit... 🍸
reply-cocktail-2= 🍸 Mixing favourite ingredients of __1__ 🍸
reply-cocktail-3=🍸 __1__ your cocktail is done.🍸
reply-coffee-1= ☕ Boiling the water and grinding the coffee beans... ☕
reply-coffee-2= ☕ __1__ we ran out of coffe beans! Have some tea instead. ☕
reply-pizza-1= 🍕 Finding nearest pizza supplier... 🍕
reply-pizza-2= 🍕 Figuring out the favourite pizza of __1__ 🍕
reply-pizza-3= 🍕 __1__ your pizza is here! 🍕
reply-tea-1= ☕ Boiling the water... ☕
reply-tea-2= ☕ __1__ your tea is done! ☕
reply-mead-1= Filling the drinking horn
reply-mead-2= Skål!
reply-beer-1= 🍺 Pouring A Glass 🍺
reply-beer-2= 🍻 Chears Mate 🍻
[exp_chat-popup]
flying-text-message=__1__: __2__
flying-text-ping=__1__ 在信息中提到了你。
[exp_damage-popup]
flying-text-health=__1__
flying-text-damage=__1__
[exp_deconstruction-log]
chat-admin=__1__ 試圖拆除在 __2__ 到 __3__ ,有 __4__ 個物品。
[exp_fast-decon]
tooltip-main=樹木快速拆除選項
chat-enabled=啟用
chat-disabled=停用
chat-toggle=樹木快速拆除已 __1__
[exp_nuke-protection]
chat-found=你的用戶組不允許你有 __1__ ,所以該物品已放在出生點的箱子。
[exp_protection-jail]
chat-jailed=__1__ 因被多次拆除受保護物體而被禁止行動。請等候管理員作出下一步處理。
[exp_report-jail]
chat-jailed=__1__ 因被多次舉報而被禁止行動。請等候管理員作出下一步處理。

View File

@@ -375,3 +375,69 @@ caption-set-location=設
type-player=用戶
type-static=靜態
type-loop=循環
[exp_afk-kick]
kick-message=因地圖中沒有活躍玩家,所以所有人都已被請離。
[exp_chat-auto-reply]
chat-reply=[Compilatron] __1__
chat-disallowed=你沒有權限使用這個指令。
reply-online=現在有 __1__ 人上線。
reply-players=本地圖現在有 __1__ 人曾上線。
reply-dev=Cooldude2606 只是本場境的開發者
reply-softmod=這裹用了自設情境。
reply-clusterio=Clusterio is a factorio server hosting platform allowing internet enabled mods.
reply-blame=責怪 __1__ 吧。
reply-afk=看看 __1__, 他已掛機 __2__ 。
reply-evolution=現在敵人進化度為 __1__ 。
reply-magic=Fear the admin magic (ノ゚∀゚)ノ⌒・*:.。. .。.:*・゜゚・*☆
reply-aids=≖ ‿ ≖ Fear the aids of a public server ≖ ‿ ≖
reply-riot=(admins) ┬┴┬┴┤ᵒ_ᵒ)├┬┴┬┴ \(´ω` )/\ (  ´)/\ ( ´ω`)/ (rest of server)
reply-loops=架設迴旋處最終後果都不好。
reply-lenny=( ͡° ͜ʖ ͡°)
reply-hodor=Hodor
reply-popcorn-1=Heating the oil and waiting for the popping sound...
reply-popcorn-2=__1__ your popcorn is finished. Lean backwards and watch the drama unfold.
reply-snaps-1=Pouring the glasses and finding the correct song book...
reply-snaps-2=Singing a song...🎤🎶
reply-snaps-3=schkål, my friends!
reply-cocktail-1= 🍸 Inintiating mind reading unit... 🍸
reply-cocktail-2= 🍸 Mixing favourite ingredients of __1__ 🍸
reply-cocktail-3=🍸 __1__ your cocktail is done.🍸
reply-coffee-1= ☕ Boiling the water and grinding the coffee beans... ☕
reply-coffee-2= ☕ __1__ we ran out of coffe beans! Have some tea instead. ☕
reply-pizza-1= 🍕 Finding nearest pizza supplier... 🍕
reply-pizza-2= 🍕 Figuring out the favourite pizza of __1__ 🍕
reply-pizza-3= 🍕 __1__ your pizza is here! 🍕
reply-tea-1= ☕ Boiling the water... ☕
reply-tea-2= ☕ __1__ your tea is done! ☕
reply-mead-1= Filling the drinking horn
reply-mead-2= Skål!
reply-beer-1= 🍺 Pouring A Glass 🍺
reply-beer-2= 🍻 Chears Mate 🍻
[exp_chat-popup]
flying-text-message=__1__: __2__
flying-text-ping=__1__ 在信息中提到了你。
[exp_damage-popup]
flying-text-health=__1__
flying-text-damage=__1__
[exp_deconstruction-log]
chat-admin=__1__ 試圖拆除在 __2__ 到 __3__ ,有 __4__ 個物品。
[exp_fast-decon]
tooltip-main=樹木快速拆除選項
chat-enabled=啟用
chat-disabled=停用
chat-toggle=樹木快速拆除已 __1__
[exp_nuke-protection]
chat-found=你的用戶組不允許你有 __1__ ,所以該物品已放在出生點的箱子。
[exp_protection-jail]
chat-jailed=__1__ 因被多次拆除受保護物體而被禁止行動。請等候管理員作出下一步處理。
[exp_report-jail]
chat-jailed=__1__ 因被多次舉報而被禁止行動。請等候管理員作出下一步處理。