diff --git a/maps/mountain_fortress_v3/gui.lua b/maps/mountain_fortress_v3/gui.lua index 43ff25b7b..759b0ed7c 100644 --- a/maps/mountain_fortress_v3/gui.lua +++ b/maps/mountain_fortress_v3/gui.lua @@ -86,10 +86,10 @@ local function create_main_frame(player) player.gui.top['wave_defense'].visible = true end - local frame = player.gui.top.add({type = 'frame', name = main_frame_name}) + local frame = player.gui.top.add({type = 'frame', name = main_frame_name, style = 'finished_game_subheader_frame'}) frame.location = {x = 1, y = 40} - frame.style.minimal_height = 37 - frame.style.maximal_height = 37 + frame.style.minimal_height = 38 + frame.style.maximal_height = 38 label = frame.add({type = 'label', caption = ' ', name = 'label'}) label.style.font_color = {r = 0.88, g = 0.88, b = 0.88} diff --git a/modules/wave_defense/gui.lua b/modules/wave_defense/gui.lua index 758e00e2a..cb3bd6626 100644 --- a/modules/wave_defense/gui.lua +++ b/modules/wave_defense/gui.lua @@ -1,12 +1,11 @@ local Public = require 'modules.wave_defense.table' local BiterHealthBooster = require 'modules.biter_health_booster_v2' -local Gui = require 'utils.gui' local floor = math.floor local function create_gui(player) - local frame = player.gui.top.add({type = 'frame', name = 'wave_defense', style = Gui.frame_style}) - frame.style.maximal_height = 37 + local frame = player.gui.top.add({type = 'frame', name = 'wave_defense', style = 'finished_game_subheader_frame'}) + frame.style.maximal_height = 38 local label = frame.add({type = 'label', caption = ' ', name = 'label'}) label.style.font_color = {r = 0.88, g = 0.88, b = 0.88} diff --git a/utils/antigrief.lua b/utils/antigrief.lua index aba8c6632..4cdc75eb8 100644 --- a/utils/antigrief.lua +++ b/utils/antigrief.lua @@ -57,7 +57,8 @@ local this = { players_warn_when_decon = {}, players_warn_on_long_texts = {}, on_cancelled_deconstruction = {tick = 0, count = 0}, - limit = 2000 + limit = 2000, + admin_button_validation = {} } local blacklisted_types = { @@ -1199,6 +1200,21 @@ function Public.get(key) end end +--- Change or add a value +---@param key any +---@param value any +---@return boolean|string|integer|table +function Public.set(key, value) + if key and (value or value == false) then + this[key] = value + return this[key] + elseif key then + return this[key] + else + return this + end +end + Event.on_init(on_init) Event.add(de.on_player_mined_entity, on_player_mined_entity) Event.add(de.on_entity_died, on_entity_died) diff --git a/utils/datastore/jail_data.lua b/utils/datastore/jail_data.lua index 63e8ca4b7..7f5bd87f9 100644 --- a/utils/datastore/jail_data.lua +++ b/utils/datastore/jail_data.lua @@ -319,7 +319,7 @@ local function create_gulag_surface() return surface end -local function teleport_player_to_gulag(player, action) +local function teleport_player_to_gulag(player, action, mute) local p_data = get_player_data(player) if not p_data then return @@ -332,6 +332,7 @@ local function teleport_player_to_gulag(player, action) p_data.position = player.position p_data.p_group_id = player.permission_group.group_id p_data.locked = true + p_data.muted = mute or false end player.teleport(gulag.find_non_colliding_position('character', {0, 0}, 128, 1), gulag.name) local data = { @@ -436,12 +437,12 @@ local function validate_args(data) end if player.name == offender then - Utils.print_to(player, module_name .. 'You can´t jail yourself.') + Utils.print_to(player, module_name .. 'You can´t select yourself.') return false end if get_offender_player.admin and not player.admin then - Utils.print_to(player, module_name .. 'You can´t jail an admin.') + Utils.print_to(player, module_name .. 'You can´t select an admin.') return false end @@ -606,13 +607,13 @@ local function jail(player, offender, msg, raised, mute) return end - draw_notice_frame(to_jail_player) - if to_jail_player.character and to_jail_player.character.valid and to_jail_player.character.driving then to_jail_player.character.driving = false end - teleport_player_to_gulag(to_jail_player, 'jail') + teleport_player_to_gulag(to_jail_player, 'jail', mute) + + draw_notice_frame(to_jail_player) if mute then local gulag = get_super_gulag_permission_group() @@ -666,7 +667,7 @@ local function jail_temporary(player, offender, msg, mute) offender.character.driving = false end - teleport_player_to_gulag(offender, 'jail') + teleport_player_to_gulag(offender, 'jail', mute) if mute then local gulag = get_super_gulag_permission_group() @@ -897,6 +898,11 @@ draw_notice_frame = function(player) local warning_message = '[font=heading-2]You have been jailed.[/font]\nPlease respond to questions if you are asked something.' + local p_data = get_player_data(player) + if p_data and p_data.muted then + warning_message = '[font=heading-2]You have been jailed and muted.[/font]\nPlease seek out assistance at our discord: https://getcomfy.eu/discord.' + end + label_flow.style.horizontally_stretchable = false local label = label_flow.add {type = 'label', caption = warning_message} label.style.single_line = false @@ -1050,6 +1056,24 @@ function Public.normies_can_jail(value) settings.normies_can_jail = value or false end +--- +--- Mutes a player completely from chatting +---@param player LuaPlayer +function Public.mute_player(player) + if not player or not player.valid then + return error('Player was not valid.') + end + + local gulag = get_super_gulag_permission_group() + if not gulag.players[player.index] then + gulag.add_player(player) + return true + else + gulag.remove_player(player) + return false + end +end + Server.on_data_set_changed( jailed_data_set, function(data) @@ -1350,7 +1374,7 @@ Event.add( if player.surface.index ~= surface.index then local p_data = get_player_data(player) if jailed[player.name] and p_data and p_data.locked then - teleport_player_to_gulag(player, 'jail') + teleport_player_to_gulag(player, 'jail', p_data.muted or false) end end @@ -1379,7 +1403,7 @@ Event.add( if player.surface.index ~= surface.index then local p_data = get_player_data(player) if jailed[player.name] and p_data and p_data.locked then - teleport_player_to_gulag(player, 'jail') + teleport_player_to_gulag(player, 'jail', p_data.muted or false) draw_notice_frame(player) end end diff --git a/utils/datastore/session_data.lua b/utils/datastore/session_data.lua index ffddf8789..e13056548 100644 --- a/utils/datastore/session_data.lua +++ b/utils/datastore/session_data.lua @@ -158,11 +158,8 @@ local get_total_playtime_token = local nth_tick_token = Token.register( function(data) - local index = data.index - local player = game.get_player(index) - if player and player.valid then - Public.try_ul_data(player.name) - end + local player_name = data.name + Public.try_ul_data(player_name) end ) @@ -173,7 +170,7 @@ local function upload_data() for i = 1, #players do count = count + 10 local player = players[i] - set_timeout_in_ticks(count, nth_tick_token, {index = player.index}) + set_timeout_in_ticks(count, nth_tick_token, {name = player.name}) end end @@ -329,15 +326,9 @@ function Public.clear_player(player) local connected = player.connected if not connected then - if session[name] then - session[name] = nil - end - if online_track[name] then - online_track[name] = nil - end - if trusted[name] then - trusted[name] = nil - end + session[name] = nil + online_track[name] = nil + trusted[name] = nil end end end @@ -346,9 +337,7 @@ end ---@param player LuaPlayer function Public.reset_online_track(player) local name = player.name - if online_track[name] then - online_track[name] = 0 - end + online_track[name] = 0 end --- It's vital that we reset the online_track so we diff --git a/utils/gui/admin.lua b/utils/gui/admin.lua index 9fb5fbbc1..ba86b4288 100644 --- a/utils/gui/admin.lua +++ b/utils/gui/admin.lua @@ -5,11 +5,55 @@ local Jailed = require 'utils.datastore.jail_data' local Gui = require 'utils.gui' local AntiGrief = require 'utils.antigrief' local SpamProtection = require 'utils.spam_protection' +local Color = require 'utils.color_presets' +local Server = require 'utils.server' +local Task = require 'utils.task' local Token = require 'utils.token' local lower = string.lower local module_name = Gui.uid_name() +local function clear_validation_action(player_name, action) + local admin_button_validation = AntiGrief.get('admin_button_validation') + if admin_button_validation and admin_button_validation[action] then + admin_button_validation[action][player_name] = nil + end +end + +local clear_validation_token = + Token.register( + function(event) + local action = event.action + if not action then + return + end + local player_name = event.player_name + if not player_name then + return + end + + local admin_button_validation = AntiGrief.get('admin_button_validation') + if admin_button_validation and admin_button_validation[action] then + admin_button_validation[action][player_name] = nil + end + end +) + +local function validate_action(player, action) + local admin_button_validation = AntiGrief.get('admin_button_validation') + if not admin_button_validation[action] then + admin_button_validation[action] = {} + end + + if not admin_button_validation[action][player.name] then + admin_button_validation[action][player.name] = true + Task.set_timeout_in_ticks(100, clear_validation_token, {player_name = player.name, action = action}) + player.print('Please run this again if you are certain that you want to run this action[' .. action .. '].', Color.warning) + return true + end + return false +end + local function admin_only_message(str) for _, player in pairs(game.connected_players) do if player.admin == true then @@ -19,17 +63,48 @@ local function admin_only_message(str) end local function jail(player, source_player) + if validate_action(source_player, 'jail') then + return + end + if player.name == source_player.name then - return player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + clear_validation_action(player.name, 'jail') + return end - Jailed.try_ul_data(player.name, true, source_player.name, 'Jailed by script!') + Jailed.try_ul_data(player.name, true, source_player.name, 'Jailed by ' .. source_player.name .. '!') + clear_validation_action(player.name, 'jail') +end + +local function mute(player, source_player) + if validate_action(source_player, 'mute') then + return + end + + if player.name == source_player.name then + player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + clear_validation_action(player.name, 'mute') + return + end + Jailed.try_ul_data(player.name, true, source_player.name, 'Jailed and muted by ' .. source_player.name .. '!', true) + local muted = Jailed.mute_player(player) + local muted_str = muted and 'muted' or 'unmuted' + clear_validation_action(player.name, 'jail') + game.print(player.name .. ' was ' .. muted_str .. ' by player ' .. source_player.name .. '!', {r = 1, g = 0.5, b = 0.1}) end local function free(player, source_player) + if validate_action(source_player, 'free') then + return + end + if player.name == source_player.name then - return player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + clear_validation_action(player.name, 'free') + return end Jailed.try_ul_data(player.name, false, source_player.name) + clear_validation_action(player.name, 'free') end local bring_player_messages = { @@ -39,17 +114,25 @@ local bring_player_messages = { } local function bring_player(player, source_player) + if validate_action(source_player, 'bring_player') then + return + end + if player.name == source_player.name then - return player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + clear_validation_action(player.name, 'bring_player') + return end if player.driving == true then source_player.print('Target player is in a vehicle, teleport not available.', {r = 0.88, g = 0.88, b = 0.88}) + clear_validation_action(player.name, 'bring_player') return end local pos = source_player.surface.find_non_colliding_position('character', source_player.position, 50, 1) if pos then player.teleport(pos, source_player.surface) game.print(player.name .. ' has been teleported to ' .. source_player.name .. '. ' .. bring_player_messages[math.random(1, #bring_player_messages)], {r = 0.98, g = 0.66, b = 0.22}) + clear_validation_action(player.name, 'bring_player') end end @@ -58,13 +141,20 @@ local go_to_player_messages = { 'What are you up to?' } local function go_to_player(player, source_player) + if validate_action(source_player, 'go_to_player') then + return + end + if player.name == source_player.name then - return player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + clear_validation_action(player.name, 'go_to_player') + return end local pos = player.surface.find_non_colliding_position('character', player.position, 50, 1) if pos then source_player.teleport(pos, player.surface) game.print(source_player.name .. ' is visiting ' .. player.name .. '. ' .. go_to_player_messages[math.random(1, #go_to_player_messages)], {r = 0.98, g = 0.66, b = 0.22}) + clear_validation_action(player.name, 'go_to_player') end end @@ -108,13 +198,19 @@ local kill_messages = { ' was struck by lightning.' } local function kill(player, source_player) + if validate_action(source_player, 'kill') then + return + end if player.name == source_player.name then - return player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + clear_validation_action(player.name, 'kill') + return end if player.character then player.character.die('player') game.print(player.name .. kill_messages[math.random(1, #kill_messages)], {r = 0.98, g = 0.66, b = 0.22}) admin_only_message(source_player.name .. ' killed ' .. player.name) + clear_validation_action(player.name, 'kill') end end @@ -123,8 +219,14 @@ local enemy_messages = { 'Wanted dead or alive!' } local function enemy(player, source_player) + if validate_action(source_player, 'enemy') then + return + end + if player.name == source_player.name then - return player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + clear_validation_action(player.name, 'enemy') + return end if not game.forces.enemy_players then game.create_force('enemy_players') @@ -132,22 +234,34 @@ local function enemy(player, source_player) player.force = game.forces.enemy_players game.print(player.name .. ' is now an enemy! ' .. enemy_messages[math.random(1, #enemy_messages)], {r = 0.95, g = 0.15, b = 0.15}) admin_only_message(source_player.name .. ' has turned ' .. player.name .. ' into an enemy') + clear_validation_action(player.name, 'enemy') end local function ally(player, source_player) + if validate_action(source_player, 'ally') then + return + end + if player.name == source_player.name then - return player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + player.print("You can't select yourself!", {r = 1, g = 0.5, b = 0.1}) + clear_validation_action(player.name, 'ally') + return end player.force = game.forces.player game.print(player.name .. ' is our ally again!', {r = 0.98, g = 0.66, b = 0.22}) admin_only_message(source_player.name .. ' made ' .. player.name .. ' our ally') + clear_validation_action(player.name, 'ally') end local function turn_off_global_speakers(player) + if validate_action(player, 'turn_off_global_speakers') then + return + end + local counter = 0 for _, surface in pairs(game.surfaces) do local speakers = surface.find_entities_filtered({name = 'programmable-speaker'}) - for i, speaker in pairs(speakers) do + for _, speaker in pairs(speakers) do if speaker.parameters.playback_globally == true then speaker.surface.create_entity({name = 'massive-explosion', position = speaker.position}) speaker.destroy() @@ -163,9 +277,14 @@ local function turn_off_global_speakers(player) else game.print(player.name .. ' has nuked ' .. counter .. ' global speakers.', {r = 0.98, g = 0.66, b = 0.22}) end + clear_validation_action(player.name, 'turn_off_global_speakers') end local function delete_all_blueprints(player) + if validate_action(player, 'delete_all_blueprints') then + return + end + local counter = 0 for _, surface in pairs(game.surfaces) do for _, ghost in pairs(surface.find_entities_filtered({type = {'entity-ghost', 'tile-ghost'}})) do @@ -174,6 +293,7 @@ local function delete_all_blueprints(player) end end if counter == 0 then + clear_validation_action(player.name, 'delete_all_blueprints') return end if counter == 1 then @@ -182,6 +302,29 @@ local function delete_all_blueprints(player) game.print(counter .. ' blueprints have been cleared!', {r = 0.98, g = 0.66, b = 0.22}) end admin_only_message(player.name .. ' has cleared all blueprints.') + clear_validation_action(player.name, 'delete_all_blueprints') +end + +local function pause_game_tick(player) + if validate_action(player, 'pause_game_tick') then + return + end + + local paused = game.tick_paused + local paused_str = paused and 'unpaused' or 'paused' + game.tick_paused = not paused + game.print('Game has been ' .. paused_str .. ' by ' .. player.name, {r = 0.98, g = 0.66, b = 0.22}) + clear_validation_action(player.name, 'pause_game_tick') +end + +local function save_game(player) + if validate_action(player, 'save_game') then + return + end + + local date = Server.get_start_time() or game.tick + game.server_save('_currently_running' .. tostring(date) .. '_' .. player.name) + clear_validation_action(player.name, 'save_game') end local function create_mini_camera_gui(player, caption, position, surface) @@ -386,7 +529,7 @@ local function create_admin_panel(data) drop_down.style.right_padding = 12 drop_down.style.left_padding = 12 - local t = frame.add({type = 'table', column_count = 3}) + local t = frame.add({type = 'table', column_count = 4}) local buttons = { t.add( { @@ -396,6 +539,14 @@ local function create_admin_panel(data) tooltip = 'Jails the player, they will no longer be able to perform any actions except writing in chat.' } ), + t.add( + { + type = 'button', + caption = 'Mute', + name = 'mute', + tooltip = 'Jails and mutes the player, they will no longer be able to chat.' + } + ), t.add({type = 'button', caption = 'Free', name = 'free', tooltip = 'Frees the player from jail.'}), t.add( { @@ -410,7 +561,7 @@ local function create_admin_panel(data) type = 'button', caption = 'Make Enemy', name = 'enemy', - tooltip = 'Sets the selected players force to enemy_players. DO NOT USE IN PVP MAPS!!' + tooltip = 'Sets the selected players force to enemy_players.\nDO NOT USE IN PVP MAPS!!' } ), t.add( @@ -418,7 +569,7 @@ local function create_admin_panel(data) type = 'button', caption = 'Make Ally', name = 'ally', - tooltip = 'Sets the selected players force back to the default player force. DO NOT USE IN PVP MAPS!!' + tooltip = 'Sets the selected players force back to the default player force.\nDO NOT USE IN PVP MAPS!!' } ), t.add( @@ -434,7 +585,7 @@ local function create_admin_panel(data) type = 'button', caption = 'Spank', name = 'spank', - tooltip = 'Hurts the selected player with minor damage. Can not kill the player.' + tooltip = 'Hurts the selected player with minor damage.\nCan not kill the player.' } ), t.add( @@ -442,7 +593,7 @@ local function create_admin_panel(data) type = 'button', caption = 'Damage', name = 'damage', - tooltip = 'Damages the selected player with greater damage. Can not kill the player.' + tooltip = 'Damages the selected player with greater damage.\nCan not kill the player.' } ), t.add({type = 'button', caption = 'Kill', name = 'kill', tooltip = 'Kills the selected player instantly.'}) @@ -450,7 +601,6 @@ local function create_admin_panel(data) for _, button in pairs(buttons) do button.style.font = 'default-bold' --button.style.font_color = { r=0.99, g=0.11, b=0.11} - button.style.font_color = {r = 0.99, g = 0.99, b = 0.99} button.style.minimal_width = 106 end @@ -459,7 +609,7 @@ local function create_admin_panel(data) line.style.bottom_margin = 8 frame.add({type = 'label', caption = 'Global Actions:'}) - local actionTable = frame.add({type = 'table', column_count = 2}) + local actionTable = frame.add({type = 'table', column_count = 4}) local bottomButtons = { actionTable.add( { @@ -476,12 +626,27 @@ local function create_admin_panel(data) name = 'delete_all_blueprints', tooltip = 'Deletes all placed blueprints on the map.' } + ), + actionTable.add( + { + type = 'button', + caption = 'Pause game tick', + name = 'pause_game_tick', + tooltip = 'Pauses the game tick.' + } + ), + actionTable.add( + { + type = 'button', + caption = 'Save game', + name = 'save_game', + tooltip = 'Saves the game.' + } ) --- t.add({type = "button", caption = "Cancel all deconstruction orders", name = "remove_all_deconstruction_orders"}) } for _, button in pairs(bottomButtons) do button.style.font = 'default-bold' - button.style.font_color = {r = 0.98, g = 0.66, b = 0.22} button.style.minimal_width = 80 end @@ -557,6 +722,7 @@ local create_admin_panel_token = Token.register(create_admin_panel) local admin_functions = { ['jail'] = jail, + ['mute'] = mute, ['free'] = free, ['bring_player'] = bring_player, ['spank'] = spank, @@ -569,7 +735,9 @@ local admin_functions = { local admin_global_functions = { ['turn_off_global_speakers'] = turn_off_global_speakers, - ['delete_all_blueprints'] = delete_all_blueprints + ['delete_all_blueprints'] = delete_all_blueprints, + ['pause_game_tick'] = pause_game_tick, + ['save_game'] = save_game } local function get_surface_from_string(str) diff --git a/utils/gui/group.lua b/utils/gui/group.lua index 4a1ca9700..549ad3028 100644 --- a/utils/gui/group.lua +++ b/utils/gui/group.lua @@ -5,13 +5,25 @@ local Global = require 'utils.global' local SpamProtection = require 'utils.spam_protection' local Event = require 'utils.event' local Token = require 'utils.token' +local Color = require 'utils.color_presets' local module_name = Gui.uid_name() local this = { player_group = {}, join_spam_protection = {}, - tag_groups = {}, + tag_groups = { + ['Miner'] = {name = 'Miner', founder = 'script', description = '[img=item/electric-mining-drill]', static = true}, + ['Smeltery'] = {name = 'Smeltery', founder = 'script', description = '[img=item/stone-furnace]', static = true}, + ['Power'] = {name = 'Power', founder = 'script', description = '[img=item/big-electric-pole]', static = true}, + ['Production'] = {name = 'Production', founder = 'script', description = '[img=item/assembling-machine-1]', static = true}, + ['Science'] = {name = 'Science', founder = 'script', description = '[img=item/chemical-science-pack]', static = true}, + ['Trainman'] = {name = 'Trainman', founder = 'script', description = '[img=item/locomotive]', static = true}, + ['Oil processing'] = {name = 'processing', founder = 'script', description = '[img=fluid/crude-oil]', static = true}, + ['Trooper'] = {name = 'Trooper', founder = 'script', description = '[img=item/submachine-gun]', static = true}, + ['Fortifications'] = {name = 'Fortifications', founder = 'script', description = '[img=item/stone-wall]', static = true}, + ['Support'] = {name = 'Support', founder = 'script', description = '[img=item/repair-pack]', static = true} + }, alphanumeric = true } @@ -37,6 +49,7 @@ local function build_group_gui(data) frame.clear() local t = frame.add({type = 'table', column_count = 5}) + local headings = { {{'gui.title'}, group_name_width}, {{'gui.description'}, description_width}, @@ -136,21 +149,24 @@ local function build_group_gui(data) b.style.minimal_width = actions_width b.style.maximal_width = actions_width end - if player.admin == true or group.founder == player.name then + if (player.admin == true or group.founder == player.name) and not group.static then local b = tt.add({type = 'button', caption = {'gui.delete'}}) b.style.font = 'default-bold' b.style.minimal_width = actions_width b.style.maximal_width = actions_width else - l = tt.add({type = 'label', caption = ''}) - l.style.minimal_width = actions_width - l.style.maximal_width = actions_width + local b = tt.add({type = 'button', caption = {'gui.delete'}}) + b.enabled = false + b.style.font = 'default-bold' + b.style.minimal_width = actions_width + b.style.maximal_width = actions_width end + tt.add {type = 'line'} end end local frame2 = frame.add({type = 'frame', name = 'frame2'}) - t = frame2.add({type = 'table', name = 'group_table', column_count = 3}) + t = frame2.add({type = 'table', name = 'group_table', column_count = 4}) local textfield = t.add({type = 'textfield', name = 'new_group_name', text = 'Name'}) textfield.style.minimal_width = 200 textfield = t.add({type = 'textfield', name = 'new_group_description', text = 'Description'}) @@ -343,6 +359,10 @@ local function on_gui_click(event) end if element.type == 'button' and caption == 'gui.delete' then + if this.tag_groups[element.parent.name] and this.tag_groups[element.parent.name].static then + player.print('Static groups cannot be deleted.', Color.warning) + return + end for _, players in pairs(game.players) do if this.player_group[players.name] then if this.player_group[players.name] == element.parent.name then @@ -382,7 +402,13 @@ function Public.reset_groups() this.player_group[player.name] = '[Group]' this.join_spam_protection[player.name] = game.tick end - this.tag_groups = {} + if this.tag_groups and next(this.tag_groups) then + for name, data in pairs(this.tag_groups) do + if not data.static then + this.tag_groups[name] = nil + end + end + end end Gui.add_tab_to_gui({name = module_name, caption = 'Groups', id = build_group_gui_token, admin = false})