diff --git a/exp_groups/controller.ts b/exp_groups/controller.ts index a6034433..846fc0fd 100644 --- a/exp_groups/controller.ts +++ b/exp_groups/controller.ts @@ -1,15 +1,23 @@ -import { BaseControllerPlugin } from "@clusterio/controller"; +import { BaseControllerPlugin, type Controller } from "@clusterio/controller"; import * as lib from "@clusterio/lib"; import * as messages from "./messages.js"; import * as path from "node:path"; +const loaded = new WeakMap(); + export class ControllerPlugin extends BaseControllerPlugin { groups!: lib.SubscribableDatastore; roleMappings!: lib.SubscribableDatastore; manualAssignments!: lib.SubscribableDatastore; resolvedAssignments!: lib.SubscribableDatastore; + /** The plugin loaded on a controller, for the exp_scenario seed. */ + static get(controller: Controller) { + return loaded.get(controller); + } + async init() { + loaded.set(this.controller, this); const databaseDirectory = this.controller.config.get("controller.database_directory"); this.groups = new lib.SubscribableDatastore( diff --git a/exp_legacy/module/config/_file_loader.lua b/exp_legacy/module/config/_file_loader.lua index 4edd462e..d9eb6ad4 100644 --- a/exp_legacy/module/config/_file_loader.lua +++ b/exp_legacy/module/config/_file_loader.lua @@ -6,9 +6,6 @@ return { "expcore.player_data", -- must be loaded first to register event handlers - -- Control - "modules.control.vlayer", - --- Data "modules.data.statistics", "modules.data.player-colours", @@ -17,11 +14,5 @@ return { "modules.data.alt-view", "modules.data.tag", "modules.data.language", - --"modules.data.toolbar", - - --- GUI - "modules.gui.warp-list", - "modules.gui.vlayer", - "modules.graftorio.require", -- graftorio } diff --git a/exp_legacy/module/locale/en.cfg b/exp_legacy/module/locale/en.cfg index 708296e0..58870d42 100644 --- a/exp_legacy/module/locale/en.cfg +++ b/exp_legacy/module/locale/en.cfg @@ -158,37 +158,6 @@ ban-player=Ban player afk-time=__1__% of total map time\nLast moved __2__ ago open-map=__1__ __2__\n__3__\nClick to open map -[warp-list] -main-caption=Warp List [img=info] -main-tooltip=Warp List -sub-tooltip=Warps can only be used every __1__ seconds and when within __2__ tiles\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ -sub-tooltip-current= - __1__ This is your current warp point; -sub-tooltip-connected= - __1__ You can travel to this warp point; -sub-tooltip-different= - __1__ This warp is on a different network; -sub-tooltip-cooldown= - __1__ You are currently on cooldown; -sub-tooltip-not_available= - __1__ You are not in range of a warp point; -sub-tooltip-bypass= - __1__ Your role allows you to travel here; -too-close=Cannot create warp; too close to existing warp point: __1__ -too-close-to-water=Cannot create warp; too close to water, please move __1__ tiles away from the water body or landfill it -too-close-to-entities=Cannot create warp; too close to other entities, please move __1__ tiles away from the entities or remove them -last-edit=Last edited by __1__ at __2__\nClick to view on map -add-tooltip=Add new warp -confirm-tooltip=Save changes -cancel-tooltip=Discard changes -edit-tooltip=Currently being edited by: __1__ -edit-tooltip-none=Currently being edited by: Nobody -remove-tooltip=Remove warp -timer-tooltip=Please wait __1__ seconds before you can warp -timer-tooltip-zero=After each warp you will need to wait __1__ seconds before you can warp again -goto-tooltip=Go to x __1__ y __2__ -goto-bypass=Go to x __1__ y __2__, bypass mode -goto-bypass-different-network=Go to x __1__ y __2__, bypass mode different network -goto-same-warp=You are already on this warp -goto-different-network=This warp is on a different network, use power poles to connect it -goto-cooldown=You are on cooldown, wait for your cooldown to recharge -goto-disabled=You are not on a warp point, walk to a warp point -goto-edit=Edit warp icon - [bonus] description=Get / Set the amount of bonus you receive. arg-amount=Amount to set your bonus to, 0 will disable bonus. @@ -215,42 +184,3 @@ display-fwrsb=Robot Storage display-fwrsb-tooltip=Force worker robots storage bonus display-ffrlm=Robot Lifetime display-ffrlm-tooltip=Force following robots lifetime modifier - -[vlayer] -main-tooltip=vlayer -description-pbr=Recharge Player Battery upto a portion with vlayer -description-vi=Vlayer Info -pbr-not-running=vlayer need to be running -display-item-solar=[img=entity/solar-panel] Solar Panel -display-item-accumulator=[img=entity/accumulator] Accumulator -display-current-production=[virtual-signal=signal-P] Current Production -display-sustained-production=[virtual-signal=signal-S] Sustained Production -display-max-capacity=[virtual-signal=signal-C] Battery Capacity -display-current-capacity=[virtual-signal=signal-E] Battery Charge -display-remaining-surface-area=[virtual-signal=signal-R] Remaining Surface -display-item-solar-tooltip=The amount of Solar Panel -display-item-accumulator-tooltip=The amount of Accumulator -display-current-production-tooltip=Current Power Production -display-sustained-production-tooltip=Sustained Power Production -display-max-capacity-tooltip=Battery Max Capacity -display-current-capacity-tooltip=Battery Current Charge -display-remaining-surface-area-tooltip=Remaining Surface. Insert Landfill to increase -steel-chest-detect=No steel chest detected -steel-chest-empty=Chest is not emptied -control-refresh=Refresh List -control-see=See Interface -control-build=Build Interface -control-remove=Remove Interface -result-build=built -result-remove=removed -result-interface-location=__1__ interface was located at __2__ -interface-result=__1__ __2__ __4__ interface at __3__ -result-unable=Unable to build __1__ interface, because of __2__ -result-multiple=Multiple steel chest detected -result-limit=Max limit exceeded -result-space=Not enough space -control-type-energy=Energy -control-type-circuit=Circuit -control-type-storage-input=Storage Input -control-type-storage-output=Storage Output -power-on-space-research=You need to research [technology=__1__] at __2__ level for the space platform vlayer feature. diff --git a/exp_legacy/module/locale/zh-CN.cfg b/exp_legacy/module/locale/zh-CN.cfg index b355f354..c86023f1 100644 --- a/exp_legacy/module/locale/zh-CN.cfg +++ b/exp_legacy/module/locale/zh-CN.cfg @@ -8,7 +8,7 @@ status=伺服器狀態: __1__ github=增加功能或回報錯誤: __1__ patreon=支持我們: __1__ custom-commands=這裹使用了自制指令,如 /tag 和 /me。可使用 /chelp 查看更多。 -read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看。 +read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看 softmod=這裹用了自設情境,是一種軟裝模組。 redmew=We don't talk about redmew here; they beat us to 1000 members ;-; lhd=列車必須是左則通行。這是本服務器長久以來的規則。 @@ -18,7 +18,7 @@ lhd=列車必須是左則通行。這是本服務器長久以來的規則。 description-add=設定 / 取得你的加入信息。 description-remove=移除你的加入信息。 arg-message=你的加入信息。 -greet=[color=0,1,0] 歡迎來到 __1__ 的服務器! 若果你喜歡本服務器可加入 DISCORD: __2__ [/color] +greet=[color=0,1,0] 歡迎來到 __1__ 的服務器! 若果你喜歡本服務器可加入 Discord: __2__ [/color] message-set=你的加入信息已更新。 message-get=你的加入信息為: __1__ message-cleared=你的加入信息已清除。 @@ -127,7 +127,7 @@ Locale-tooltip=用戶介面語言 # expcore [expcore-data] description-preference=允許寫入資料。 -description-data=把用戶資料寫入電腦 +description-data=把用戶資料寫入電腦。 arg-option="All", "Statistics", "Settings", "Required" 之中 set-preference=你的喜好已設定為 __1__ 。在你重新加入前將不會有任何改動。 get-preference=你的喜好為 __1__ 。使用 /set-preference 來更改,使用 /save-data 來獲得一份資料副本。 @@ -158,37 +158,6 @@ ban-player=封禁用戶 afk-time=__1__% 地圖時間\n上次移動: __2__ open-map=__1__ __2__\n__3__\n按下打開地圖 -[warp-list] -main-caption=傳送陣清單 [img=info] -main-tooltip=傳送陣清單 -sub-tooltip=傳送陣在 __1__ 秒內只能使用一次。 亦需在 __2__ 格之內使用\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ -sub-tooltip-current= - __1__ 你目前的傳送陣; -sub-tooltip-connected= - __1__ 你可以到本傳送陣; -sub-tooltip-different= - __1__ 該傳送陣未連接; -sub-tooltip-cooldown= - __1__ 傳送冷卻中; -sub-tooltip-not_available= - __1__ 你不在傳送陣範圍; -sub-tooltip-bypass= - __1__ 你可以傳送到這裏; -too-close=無法新建傳送陣; 太接近 __1__ 了 -too-close-to-water=無法新建傳送陣; 太接近水源了, 請移動 __1__ 格又或者填平該地方 -too-close-to-entities=無法新建傳送陣; 太接近其他東西, 請移動 __1__ 格又或者拆除該東西 -last-edit=最後由 __1__ 在 __2__ 更改\n按這在地圖上看 -add-tooltip=新建傳送陣 -confirm-tooltip=儲存更改 -cancel-tooltip=放棄更改 -edit-tooltip=現被 __1__ 修改中 -edit-tooltip-none=現沒有被人修改 -remove-tooltip=拆除傳送陣 -timer-tooltip=請等 __1__ 秒才再次傳送 -timer-tooltip-zero=你要等 __1__ 秒才可以再次傳送 -goto-tooltip=到 X __1__ Y __2__ -goto-bypass=到 X __1__ Y __2__, 繞行模式 -goto-bypass-different-network=到 X __1__ Y __2__, 繞行模式 (不同網絡) -goto-same-warp=你已在該傳送陣 -goto-different-network=該傳送陣未連接, 你可使用電纜連接 -goto-cooldown=傳送冷卻中, 請等候冷卻 -goto-disabled=你不在傳送陣, 請移至任意傳送陣 -goto-edit=修改傳送陣圖案 - [bonus] description=取得 / 設定 Bonus 量。 arg-amount=Bonus 數量, 0 來停用。 @@ -216,45 +185,6 @@ display-fwrsb-tooltip=勢力機械人儲存 display-ffrlm=機械人生命 display-ffrlm-tooltip=勢力戰鬥機械人生命時間 -[vlayer] -main-tooltip=vlayer -description-pbr=使用 Vlayer 為玩家電池充電 -description-vi=Vlayer 資訊 -pbr-not-running=vlayer 沒有運行 -display-item-solar=[img=entity/solar-panel] 太陽能板 -display-item-accumulator=[img=entity/accumulator] 蓄電池 -display-current-production=[virtual-signal=signal-P] 現時產能 -display-sustained-production=[virtual-signal=signal-S] 持續產能 -display-max-capacity=[virtual-signal=signal-C] 電池容量 -display-current-capacity=[virtual-signal=signal-E] 電池電量 -display-remaining-surface-area=[virtual-signal=signal-R] 餘下土地 -display-item-solar-tooltip=太陽能板數量 -display-item-accumulator-tooltip=蓄電池數量 -display-current-production-tooltip=現時電力產能 -display-sustained-production-tooltip=持續電力產能 -display-max-capacity-tooltip=電池最大容量 -display-current-capacity-tooltip=現時電池電量 -display-remaining-surface-area-tooltip=餘下土地。 放入堆填來增加 -steel-chest-detect=沒檢測到鋼箱 -steel-chest-empty=鋼箱不是空的 -control-refresh=刷新列表 -control-see=查看介面 -control-build=建造介面 -control-remove=拆除介面 -result-build=建造了 -result-remove=拆除了 -result-interface-location=__1__ 介面在 __2__ -interface-result=__1__ 在 __2__ __3__ __4__ 介面 -result-unable=無法建造 __1__ 介面,因 __2__ -result-multiple=檢測到多個鋼箱 -result-limit=超出最大限制 -result-space=空間不足 -control-type-energy=電力 -control-type-circuit=回路 -control-type-storage-input=放入箱 -control-type-storage-output=提取箱 -power-on-space-research=你要研究 [technology=__1__] 在 __2__ 級 才可使用太空平台 vlayer 功能。 - [toolbar] main-caption=工具箱 main-tooltip=工具箱設定\n選上來設定喜好 diff --git a/exp_legacy/module/locale/zh-TW.cfg b/exp_legacy/module/locale/zh-TW.cfg index b355f354..c86023f1 100644 --- a/exp_legacy/module/locale/zh-TW.cfg +++ b/exp_legacy/module/locale/zh-TW.cfg @@ -8,7 +8,7 @@ status=伺服器狀態: __1__ github=增加功能或回報錯誤: __1__ patreon=支持我們: __1__ custom-commands=這裹使用了自制指令,如 /tag 和 /me。可使用 /chelp 查看更多。 -read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看。 +read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看 softmod=這裹用了自設情境,是一種軟裝模組。 redmew=We don't talk about redmew here; they beat us to 1000 members ;-; lhd=列車必須是左則通行。這是本服務器長久以來的規則。 @@ -18,7 +18,7 @@ lhd=列車必須是左則通行。這是本服務器長久以來的規則。 description-add=設定 / 取得你的加入信息。 description-remove=移除你的加入信息。 arg-message=你的加入信息。 -greet=[color=0,1,0] 歡迎來到 __1__ 的服務器! 若果你喜歡本服務器可加入 DISCORD: __2__ [/color] +greet=[color=0,1,0] 歡迎來到 __1__ 的服務器! 若果你喜歡本服務器可加入 Discord: __2__ [/color] message-set=你的加入信息已更新。 message-get=你的加入信息為: __1__ message-cleared=你的加入信息已清除。 @@ -127,7 +127,7 @@ Locale-tooltip=用戶介面語言 # expcore [expcore-data] description-preference=允許寫入資料。 -description-data=把用戶資料寫入電腦 +description-data=把用戶資料寫入電腦。 arg-option="All", "Statistics", "Settings", "Required" 之中 set-preference=你的喜好已設定為 __1__ 。在你重新加入前將不會有任何改動。 get-preference=你的喜好為 __1__ 。使用 /set-preference 來更改,使用 /save-data 來獲得一份資料副本。 @@ -158,37 +158,6 @@ ban-player=封禁用戶 afk-time=__1__% 地圖時間\n上次移動: __2__ open-map=__1__ __2__\n__3__\n按下打開地圖 -[warp-list] -main-caption=傳送陣清單 [img=info] -main-tooltip=傳送陣清單 -sub-tooltip=傳送陣在 __1__ 秒內只能使用一次。 亦需在 __2__ 格之內使用\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ -sub-tooltip-current= - __1__ 你目前的傳送陣; -sub-tooltip-connected= - __1__ 你可以到本傳送陣; -sub-tooltip-different= - __1__ 該傳送陣未連接; -sub-tooltip-cooldown= - __1__ 傳送冷卻中; -sub-tooltip-not_available= - __1__ 你不在傳送陣範圍; -sub-tooltip-bypass= - __1__ 你可以傳送到這裏; -too-close=無法新建傳送陣; 太接近 __1__ 了 -too-close-to-water=無法新建傳送陣; 太接近水源了, 請移動 __1__ 格又或者填平該地方 -too-close-to-entities=無法新建傳送陣; 太接近其他東西, 請移動 __1__ 格又或者拆除該東西 -last-edit=最後由 __1__ 在 __2__ 更改\n按這在地圖上看 -add-tooltip=新建傳送陣 -confirm-tooltip=儲存更改 -cancel-tooltip=放棄更改 -edit-tooltip=現被 __1__ 修改中 -edit-tooltip-none=現沒有被人修改 -remove-tooltip=拆除傳送陣 -timer-tooltip=請等 __1__ 秒才再次傳送 -timer-tooltip-zero=你要等 __1__ 秒才可以再次傳送 -goto-tooltip=到 X __1__ Y __2__ -goto-bypass=到 X __1__ Y __2__, 繞行模式 -goto-bypass-different-network=到 X __1__ Y __2__, 繞行模式 (不同網絡) -goto-same-warp=你已在該傳送陣 -goto-different-network=該傳送陣未連接, 你可使用電纜連接 -goto-cooldown=傳送冷卻中, 請等候冷卻 -goto-disabled=你不在傳送陣, 請移至任意傳送陣 -goto-edit=修改傳送陣圖案 - [bonus] description=取得 / 設定 Bonus 量。 arg-amount=Bonus 數量, 0 來停用。 @@ -216,45 +185,6 @@ display-fwrsb-tooltip=勢力機械人儲存 display-ffrlm=機械人生命 display-ffrlm-tooltip=勢力戰鬥機械人生命時間 -[vlayer] -main-tooltip=vlayer -description-pbr=使用 Vlayer 為玩家電池充電 -description-vi=Vlayer 資訊 -pbr-not-running=vlayer 沒有運行 -display-item-solar=[img=entity/solar-panel] 太陽能板 -display-item-accumulator=[img=entity/accumulator] 蓄電池 -display-current-production=[virtual-signal=signal-P] 現時產能 -display-sustained-production=[virtual-signal=signal-S] 持續產能 -display-max-capacity=[virtual-signal=signal-C] 電池容量 -display-current-capacity=[virtual-signal=signal-E] 電池電量 -display-remaining-surface-area=[virtual-signal=signal-R] 餘下土地 -display-item-solar-tooltip=太陽能板數量 -display-item-accumulator-tooltip=蓄電池數量 -display-current-production-tooltip=現時電力產能 -display-sustained-production-tooltip=持續電力產能 -display-max-capacity-tooltip=電池最大容量 -display-current-capacity-tooltip=現時電池電量 -display-remaining-surface-area-tooltip=餘下土地。 放入堆填來增加 -steel-chest-detect=沒檢測到鋼箱 -steel-chest-empty=鋼箱不是空的 -control-refresh=刷新列表 -control-see=查看介面 -control-build=建造介面 -control-remove=拆除介面 -result-build=建造了 -result-remove=拆除了 -result-interface-location=__1__ 介面在 __2__ -interface-result=__1__ 在 __2__ __3__ __4__ 介面 -result-unable=無法建造 __1__ 介面,因 __2__ -result-multiple=檢測到多個鋼箱 -result-limit=超出最大限制 -result-space=空間不足 -control-type-energy=電力 -control-type-circuit=回路 -control-type-storage-input=放入箱 -control-type-storage-output=提取箱 -power-on-space-research=你要研究 [technology=__1__] 在 __2__ 級 才可使用太空平台 vlayer 功能。 - [toolbar] main-caption=工具箱 main-tooltip=工具箱設定\n選上來設定喜好 diff --git a/exp_legacy/module/modules/control/vlayer.lua b/exp_legacy/module/modules/control/vlayer.lua deleted file mode 100644 index 72afba6c..00000000 --- a/exp_legacy/module/modules/control/vlayer.lua +++ /dev/null @@ -1,807 +0,0 @@ ---[[-- Control Module - vlayer - - Adds a virtual layer to store power to save space. - @control vlayer - @alias vlayer -]] - -local ExpUtil = require("modules/exp_util") -local Storage = require("modules/exp_util/storage") -local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event -local config = require("modules.exp_legacy.config.vlayer") --- @dep config.vlayer - -local mega = 1000000 - -local vlayer = {} -local vlayer_data = { - entity_interfaces = { - energy = {}, --- @type LuaEntity[] - circuit = {}, --- @type LuaEntity[] - storage_input = {}, --- @type LuaEntity[] - storage_output = {}, --- @type LuaEntity[] - }, - properties = { - total_surface_area = 0, - used_surface_area = 0, - total_production = 0, - production = 0, - discharge = 0, - capacity = 0, - }, - storage = { - items = {}, --- @type table - power_items = {}, --- @type table - energy = 0, - unallocated = {}, --- @type table - }, - surface = table.deep_copy(config.surface), -} - -Storage.register(vlayer_data, function(tbl) - vlayer_data = tbl -end) - -for name, properties in pairs(config.allowed_items) do - properties.modded = false - - if properties.power then - vlayer_data.storage.power_items[name] = { - value = assert(properties.fuel_value) * 1000000, - count = 0, - } - end -end - --- For all modded items, create a config for them -for item_name, properties in pairs(config.modded_items) do - local base_properties = config.allowed_items[properties.base_game_equivalent] - local m = properties.multiplier - - config.allowed_items[item_name] = { - starting_value = properties.starting_value or 0, - required_area = base_properties.required_area or 0, - surface_area = (base_properties.surface_area or 0) * m, - production = (base_properties.production or 0) * m, - capacity = (base_properties.capacity or 0) * m, - modded = true, - } -end - ---- Get all items in storage, do not modify --- @treturn table a dictionary of all items stored in the vlayer -function vlayer.get_items() - return vlayer_data.storage.items -end - ---- Get all unallocated items in storage --- @treturn table a dictionary of all unallocated items stored in the vlayer -function vlayer.get_unallocated_items() - return vlayer_data.storage.unallocated -end - ---- Get all allocated items in storage --- @treturn table a dictionary of all allocated items stored in the vlayer -function vlayer.get_allocated_items() - local r = {} - - for k, v in pairs(vlayer_data.storage.items) do - r[k] = v - - if vlayer_data.storage.unallocated[k] then - r[k] = r[k] - vlayer_data.storage.unallocated[k] - end - end - - return r -end - ---- Get the actual defecit of land -local function get_actual_land_defecit() - local n = vlayer_data.properties.total_surface_area - vlayer_data.properties.used_surface_area - - for k, v in pairs(vlayer.get_unallocated_items()) do - n = n - (config.allowed_items[k].required_area * v) - end - - return n -end - ---- Get interface counts --- @treturn table a dictionary of the vlayer interface counts -function vlayer.get_interface_counts() - local interfaces = vlayer_data.entity_interfaces - - return { - energy = #interfaces.energy, - circuit = #interfaces.circuit, - storage_input = #interfaces.storage_input, - storage_output = #interfaces.storage_output, - } -end - ---- Get interfaces --- @treturn table a dictionary of the vlayer interfaces -function vlayer.get_interfaces() - local interfaces = vlayer_data.entity_interfaces - - return { - energy = interfaces.energy, - circuit = interfaces.circuit, - storage_input = interfaces.storage_input, - storage_output = interfaces.storage_output, - } -end - ---[[ - 25,200 / 420 s - 昼 210秒 ソーラー効率100% - 夕方 84秒 1秒ごとにソーラー発電量が約1.2%ずつ下がり、やがて0%になる - 夜 42秒 ソーラー発電量が0%になる - 朝方 84秒 1秒ごとにソーラー発電量が約1.2%ずつ上がり、やがて100%になる - - (surface.dawn) 0.75 18,900 Day 12,600 210s - 0.00 0 Noon - (surface.dusk) 0.25 6,300 Sunset 5,040 84s - (surface.evening) 0.45 11,340 Night 2,520 42s - (surface.morning) 0.55 13,860 Sunrise 5,040 84s -]] - ---- Get the power multiplier based on the surface time -local function get_production_multiplier() - local mul = vlayer_data.surface.solar_power_multiplier - local surface = vlayer_data.surface - - if surface.always_day then - -- Surface is always day, so full production is used - return mul - end - - if surface.darkness then - -- We are using a real surface, our config does not contain "darkness" - local brightness = 1 - surface.darkness - - if brightness >= surface.min_brightness then - return mul * (brightness - surface.min_brightness) / (1 - surface.min_brightness) - else - return 0 - end - end - - -- Caused by using a set config rather than a surface - local tick = game.tick % surface.ticks_per_day - local daytime = tick / surface.ticks_per_day - surface.daytime = daytime - - if daytime <= surface.dusk then -- Noon to Sunset - return mul - elseif daytime <= surface.evening then -- Sunset to Night - return mul * (1 - ((daytime - surface.dusk) / (surface.evening - surface.dusk))) - elseif daytime <= surface.morning then -- Night to Sunrise - return 0 - elseif daytime <= surface.dawn then -- Sunrise to Morning - return mul * ((surface.daytime - surface.morning) / (surface.dawn - surface.morning)) - else -- Morning to Noon - return mul - end -end - ---- Get the sustained power multiplier, this needs improving -local function get_sustained_multiplier() - local mul = vlayer_data.surface.solar_power_multiplier - local surface = vlayer_data.surface - - if surface.always_day then - -- Surface is always day, so full production is used - return mul - end - - -- For nauvis vanilla: 210s + (1/2 x (84s + 84s)) - local day_duration = 1 - surface.dawn + surface.dusk - local sunset_duration = surface.evening - surface.dusk - local sunrise_duration = surface.dawn - surface.morning - - return mul * (day_duration + (0.5 * (sunset_duration + sunrise_duration))) -end - ---- Internal, Allocate items in the vlayer, this will increase the property values of the vlayer such as production and capacity --- Does not increment item storage, so should not be called before insert_item unless during init --- Does not validate area requirements, so checks must be performed before calling this function --- Accepts negative count for deallocating items --- @tparam string item_name The name of the item to allocate --- @tparam number count The count of the item to allocate -function vlayer.allocate_item(item_name, count) - local item_properties = config.allowed_items[item_name] - assert(item_properties, "Item not allowed in vlayer: " .. tostring(item_name)) - - if item_properties.production then - local nc = item_properties.production * count - vlayer_data.properties.production = vlayer_data.properties.production + nc - vlayer_data.properties.total_production = vlayer_data.properties.total_production + nc - end - - if item_properties.capacity then - vlayer_data.properties.capacity = vlayer_data.properties.capacity + item_properties.capacity * count - end - - if item_properties.discharge then - vlayer_data.properties.discharge = vlayer_data.properties.discharge + item_properties.discharge * count - end - - if item_properties.surface_area then - vlayer_data.properties.total_surface_area = vlayer_data.properties.total_surface_area + item_properties.surface_area * count - end - - if item_properties.required_area and item_properties.required_area > 0 then - vlayer_data.properties.used_surface_area = vlayer_data.properties.used_surface_area + item_properties.required_area * count - end -end - -function vlayer.unable_alloc_item_pwr_calc(item_name, count) - local item_properties = config.allowed_items[item_name] - - if item_properties.production then - vlayer_data.properties.total_production = vlayer_data.properties.total_production + item_properties.production * count - end -end - --- For all allowed items, setup their starting values, default 0 -for item_name, properties in pairs(config.allowed_items) do - vlayer_data.storage.items[item_name] = properties.starting_value or 0 - - if properties.required_area and properties.required_area > 0 then - vlayer_data.storage.unallocated[item_name] = 0 - end - - vlayer.allocate_item(item_name, properties.starting_value) -end - ---- Insert an item into the vlayer, this will increment its count in storage and allocate it if possible --- @tparam string item_name The name of the item to insert --- @tparam number count The count of the item to insert -function vlayer.insert_item(item_name, count) - local item_properties = config.allowed_items[item_name] - assert(item_properties, "Item not allowed in vlayer: " .. tostring(item_name)) - vlayer_data.storage.items[item_name] = vlayer_data.storage.items[item_name] + count - - if not config.unlimited_surface_area and item_properties.required_area and item_properties.required_area > 0 then - -- Calculate how many can be allocated - local surplus_area = vlayer_data.properties.total_surface_area - vlayer_data.properties.used_surface_area - local allocate_count = math.min(count, math.floor(surplus_area / item_properties.required_area)) - - if allocate_count > 0 then - vlayer.allocate_item(item_name, allocate_count) - end - - local unallocated = count - allocate_count - vlayer_data.storage.unallocated[item_name] = vlayer_data.storage.unallocated[item_name] + unallocated - vlayer.unable_alloc_item_pwr_calc(item_name, unallocated) - else - vlayer.allocate_item(item_name, count) - end -end - ---- Remove an item from the vlayer, this will decrement its count in storage and prioritise unallocated items over deallocation --- Can not always fulfil the remove request for items which provide surface area, therefore returns the amount actually removed --- @tparam string item_name The name of the item to remove --- @tparam number count The count of the item to remove --- @treturn number The count of the item actually removed -function vlayer.remove_item(item_name, count) - local item_properties = config.allowed_items[item_name] - assert(item_properties, "Item not allowed in vlayer: " .. tostring(item_name)) - local remove_unallocated = 0 - - if not config.unlimited_surface_area and item_properties.required_area and item_properties.required_area > 0 then - -- Remove from the unallocated storage first - remove_unallocated = math.min(count, vlayer_data.storage.unallocated[item_name]) - - if remove_unallocated > 0 then - vlayer_data.storage.items[item_name] = vlayer_data.storage.items[item_name] - count - vlayer_data.storage.unallocated[item_name] = vlayer_data.storage.unallocated[item_name] - count - end - - -- Check if any more items need to be removed - count = count - remove_unallocated - - if count == 0 then - return remove_unallocated - end - end - - -- Calculate the amount to remove based on items in storage - local remove_count = math.min(count, vlayer_data.storage.items[item_name]) - - if item_properties.surface_area and item_properties.surface_area > 0 then - -- If the item provides surface area then it has additional limitations - local surplus_area = vlayer_data.properties.total_surface_area - vlayer_data.properties.used_surface_area - remove_count = math.min(remove_count, math.floor(surplus_area / item_properties.surface_area)) - - if remove_count <= 0 then - return remove_unallocated - end - end - - -- Remove the item from allocated storage - vlayer_data.storage.items[item_name] = vlayer_data.storage.items[item_name] - remove_count - vlayer.allocate_item(item_name, -remove_count) - - return remove_unallocated + remove_count -end - ---- Create a new storage input interface --- @tparam LuaSurface surface The surface to place the interface onto --- @tparam MapPosition position The position on the surface to place the interface at --- @tparam[opt] LuaPlayer player The player to show as the last user of the interface --- @treturn LuaEntity The entity that was created for the interface -function vlayer.create_input_interface(surface, position, circuit, last_user) - local interface = surface.create_entity{ name = "storage-chest", position = position, force = "neutral" } - interface.storage_filter = { name = "deconstruction-planner", quality = "normal" } - table.insert(vlayer_data.entity_interfaces.storage_input, interface) - - if last_user then - interface.last_user = last_user - end - - if circuit then - for k, _ in pairs(circuit) do - for _, v in pairs(circuit[k]) do - interface.connect_neighbour{ wire = defines.wire_type[k], target_entity = v } - end - end - end - - interface.destructible = false - interface.minable_flag = false - interface.operable = true - - return interface -end - ---- Handle all input interfaces, will take their contents and insert it into the vlayer storage -local function handle_input_interfaces() - for index, interface in pairs(vlayer_data.entity_interfaces.storage_input) do - if not interface.valid then - vlayer_data.entity_interfaces.storage_input[index] = nil - else - local inventory = assert(interface.get_inventory(defines.inventory.chest)) - - for _, v in pairs(inventory.get_contents()) do - if config.allowed_items[v.name] then - --[[ - there are no quality support currently. - so instead, using the stats projection value for higher quality. - ]] - local count_deduct - local count_add - - if prototypes.quality[v.quality].level == 0 then - count_deduct = v.count - count_add = v.count - - elseif prototypes.quality[v.quality].level > 0 and v.count >= 10 then - local batch = math.floor(v.count / 10) - count_deduct = batch * 10 - count_add = batch * (10 + (prototypes.quality[v.quality].level * 3)) - end - - if count_deduct and count_add then - if config.allowed_items[v.name].modded then - local modded_item = assert(config.modded_items[v.name]) - if config.modded_auto_downgrade then - vlayer.insert_item(modded_item.base_game_equivalent, count_add * modded_item.multiplier) - else - vlayer.insert_item(v.name, count_add) - end - else - if vlayer_data.storage.power_items[v.name] then - vlayer_data.storage.power_items[v.name].count = vlayer_data.storage.power_items[v.name].count + count_add - else - vlayer.insert_item(v.name, count_add) - end - end - - inventory.remove{ name = v.name, count = count_deduct, quality = v.quality } - end - end - end - end - end -end - ---- Create a new storage output interface --- @tparam LuaSurface surface The surface to place the interface onto --- @tparam MapPosition position The position on the surface to place the interface at --- @tparam[opt] LuaPlayer player The player to show as the last user of the interface --- @treturn LuaEntity The entity that was created for the interface -function vlayer.create_output_interface(surface, position, circuit, last_user) - local interface = surface.create_entity{ name = "requester-chest", position = position, force = "neutral" } - table.insert(vlayer_data.entity_interfaces.storage_output, interface) - - if last_user then - interface.last_user = last_user - end - - if circuit then - for k, _ in pairs(circuit) do - for _, v in pairs(circuit[k]) do - interface.connect_neighbour{ wire = defines.wire_type[k], target_entity = v } - end - end - end - - interface.destructible = false - interface.minable_flag = false - interface.operable = true - - return interface -end - ---- Handle all output interfaces, will take their requests and remove it from the vlayer storage -local function handle_output_interfaces() - for index, interface in pairs(vlayer_data.entity_interfaces.storage_output) do - if not interface.valid then - vlayer_data.entity_interfaces.storage_output[index] = nil - else - local inventory = assert(interface.get_inventory(defines.inventory.chest)) - local inventory_request_sections = assert(interface.get_logistic_sections()).sections - - for i = 1, #inventory_request_sections do - for _, v in pairs(inventory_request_sections[i].filters) do - if v.value and config.allowed_items[v.value.name] then - local current_amount = inventory.get_item_count(v.value.name) - local request_amount = math.min(assert(v.min) - current_amount, vlayer_data.storage.items[v.value.name]) - - if request_amount > 0 and inventory.can_insert{ name = v.value.name, count = request_amount } then - local removed_item_count = vlayer.remove_item(v.value.name, request_amount) - - if removed_item_count > 0 then - inventory.insert{ name = v.value.name, count = removed_item_count, quality = "normal" } - end - end - end - end - end - end - end -end - ---- Handle the unallocated items because more surface area may have been added -local function handle_unallocated() - -- unallocated cant happen when its unlimited - if config.unlimited_surface_area then - return - end - - -- Get the total unallocated area so items can be allocated in equal amounts - local unallocated_area = 0 - - for item_name, count in pairs(vlayer_data.storage.unallocated) do - local item_properties = config.allowed_items[item_name] - unallocated_area = unallocated_area + item_properties.required_area * count - end - - if unallocated_area == 0 then - return - end - - -- Allocate items in an equal distribution - local surplus_area = vlayer_data.properties.total_surface_area - vlayer_data.properties.used_surface_area - - for item_name, count in pairs(vlayer_data.storage.unallocated) do - local allocation_count = math.min(count, math.floor(count * surplus_area / unallocated_area)) - - if allocation_count > 0 then - vlayer_data.storage.unallocated[item_name] = vlayer_data.storage.unallocated[item_name] - allocation_count - vlayer.allocate_item(item_name, allocation_count) - vlayer.unable_alloc_item_pwr_calc(item_name, -allocation_count) - end - end -end - ---- Get the statistics for the vlayer -function vlayer.get_statistics() - local vdp = vlayer_data.properties.production * mega - local gdm = get_production_multiplier() - local gsm = get_sustained_multiplier() - local gald = get_actual_land_defecit() - - return { - total_surface_area = vlayer_data.properties.total_surface_area, - used_surface_area = vlayer_data.properties.used_surface_area, - remaining_surface_area = gald, - surface_area = vlayer_data.properties.total_surface_area - gald, - production_multiplier = gdm, - energy_max = vdp, - energy_production = vdp * gdm, - energy_total_production = vlayer_data.properties.total_production * gsm * mega, - energy_sustained = vdp * gsm, - energy_capacity = vlayer_data.properties.capacity * mega, - energy_storage = vlayer_data.storage.energy, - } -end - ---- add or reduce vlayer power ---- @param power number ---- @return number -function vlayer.energy_changed(power) - local new_value = vlayer_data.storage.energy + power - vlayer_data.storage.energy = new_value - return new_value -end - ---- Circuit signals used for the statistics -function vlayer.get_circuits() - return { - total_surface_area = "signal-A", - used_surface_area = "signal-U", - remaining_surface_area = "signal-R", - production_multiplier = "signal-M", - energy_production = "signal-P", - energy_sustained = "signal-S", - energy_capacity = "signal-C", - energy_storage = "signal-E", - } -end - -local vlayer_circuits_string = "" - -for key, value in pairs(vlayer.get_circuits()) do - vlayer_circuits_string = vlayer_circuits_string .. string.format("[virtual-signal=%s] = %s\n", value, key:gsub("_", " ")) -end - ---- Create a new circuit interface --- @tparam LuaSurface surface The surface to place the interface onto --- @tparam MapPosition position The position on the surface to place the interface at --- @tparam[opt] LuaPlayer player The player to show as the last user of the interface --- @treturn LuaEntity The entity that was created for the interface -function vlayer.create_circuit_interface(surface, position, circuit, last_user) - local interface = surface.create_entity{ name = "constant-combinator", position = position, force = "neutral" } - table.insert(vlayer_data.entity_interfaces.circuit, interface) - - if last_user then - interface.last_user = last_user - end - - if circuit then - for k, _ in pairs(circuit) do - for _, v in pairs(circuit[k]) do - interface.connect_neighbour{ wire = defines.wire_type[k], target_entity = v } - end - end - end - - interface.destructible = false - interface.minable_flag = false - interface.operable = true - - return interface -end - ---- Handle all circuit interfaces, updating their signals to match the vlayer statistics -local function handle_circuit_interfaces() - local stats = vlayer.get_statistics() - - for index, interface in pairs(vlayer_data.entity_interfaces.circuit) do - if not interface.valid then - vlayer_data.entity_interfaces.circuit[index] = nil - else - local control = interface.get_or_create_control_behavior() --[[@as LuaConstantCombinatorControlBehavior]] - if control.sections_count == 0 then - control.add_section() - end - local circuit_oc = assert(control.sections[1]) - local signal_index = 1 - local circuit = vlayer.get_circuits() - - -- Set the virtual signals based on the vlayer stats - for stat_name, signal_name in pairs(circuit) do - if stat_name:find("energy") then - circuit_oc.set_slot(signal_index, { value = { type = "virtual", name = signal_name, quality = "normal" }, min = math.floor(stats[stat_name] / mega) }) - elseif stat_name == "production_multiplier" then - circuit_oc.set_slot(signal_index, { value = { type = "virtual", name = signal_name, quality = "normal" }, min = math.floor(stats[stat_name] * 10000) }) - else - circuit_oc.set_slot(signal_index, { value = { type = "virtual", name = signal_name, quality = "normal" }, min = math.floor(stats[stat_name]) }) - end - - signal_index = signal_index + 1 - end - - -- Set the item signals based on stored items - for item_name, count in pairs(vlayer_data.storage.items) do - if prototypes.item[item_name] and count > 0 then - local existing_index = circuit_oc.set_slot(signal_index, { value = { type = "item", name = item_name, quality = "normal" }, min = count }) - if existing_index then - circuit_oc.clear_slot(existing_index) - circuit_oc.set_slot(signal_index, { value = { type = "item", name = item_name, quality = "normal" }, min = count }) - end - signal_index = signal_index + 1 - end - end - - -- Clear remaining signals to prevent outdated values being present (caused by count > 0 check) - for clear_index = signal_index, #circuit do - if not circuit_oc.get_slot(clear_index).value then - break -- There are no more signals to clear - end - - circuit_oc.clear_slot(clear_index) - end - - interface.combinator_description = vlayer_circuits_string - end - end -end - ---- Create a new energy interface --- @tparam LuaSurface surface The surface to place the interface onto --- @tparam MapPosition position The position on the surface to place the interface at --- @tparam[opt] LuaPlayer player The player to show as the last user of the interface --- @treturn LuaEntity The entity that was created for the interface, or nil if it could not be created -function vlayer.create_energy_interface(surface, position, last_user) - if not surface.can_place_entity{ name = "electric-energy-interface", position = position } then - return nil - end - - local interface = surface.create_entity{ name = "electric-energy-interface", position = position, force = "neutral" } - table.insert(vlayer_data.entity_interfaces.energy, interface) - - if last_user then - interface.last_user = last_user - end - - interface.destructible = false - interface.minable_flag = false - interface.operable = false - interface.electric_buffer_size = 0 - interface.power_production = 0 - interface.power_usage = 0 - interface.energy = 0 - - return interface -end - ---- Handle all energy interfaces as well as the energy storage -local function handle_energy_interfaces() - -- Add the newly produced power - local production = vlayer_data.properties.production * mega * (config.update_tick_energy / 60) - vlayer_data.storage.energy = vlayer_data.storage.energy + math.floor(production * get_production_multiplier()) - - -- Calculate how much power is present in the network, that is storage + all interfaces - if #vlayer_data.entity_interfaces.energy > 0 then - local available_energy = vlayer_data.storage.energy - - for index, interface in pairs(vlayer_data.entity_interfaces.energy) do - if not interface.valid then - vlayer_data.entity_interfaces.energy[index] = nil - else - available_energy = available_energy + interface.energy - end - end - - -- Distribute the energy between all interfaces - local discharge_rate = 2 * (production + vlayer_data.properties.discharge * mega) / #vlayer_data.entity_interfaces.energy - local fill_to = math.min(discharge_rate, math.floor(available_energy / #vlayer_data.entity_interfaces.energy)) - - for _, interface in pairs(vlayer_data.entity_interfaces.energy) do - interface.electric_buffer_size = math.max(discharge_rate, interface.energy) -- prevent energy loss - local delta = fill_to - interface.energy -- positive means storage to interface - vlayer_data.storage.energy = vlayer_data.storage.energy - delta - interface.energy = interface.energy + delta - end - end - - -- Cap the stored energy to the allowed capacity - if not config.unlimited_capacity and vlayer_data.storage.energy > vlayer_data.properties.capacity * mega then - vlayer_data.storage.energy = vlayer_data.properties.capacity * mega - - -- burn the trash to produce power - elseif vlayer_data.storage.power_items then - for k, v in pairs(vlayer_data.storage.power_items) do - local max_burning = (vlayer_data.properties.capacity * mega / 2) - vlayer_data.storage.energy - - if v.count > 0 and max_burning > 0 then - local to_burn = math.min(v.count, math.floor(max_burning / v.value)) - - vlayer_data.storage.energy = vlayer_data.storage.energy + (to_burn * v.value) - vlayer_data.storage.power_items[k].count = vlayer_data.storage.power_items[k].count - to_burn - end - end - end -end - ---- Remove the entity interface using the given position --- @tparam LuaSurface surface The surface to search for an interface on --- @tparam MapPosition position The position of the item --- @treturn string The type of interface that was removed, or nil if no interface was found --- @treturn MapPosition The position the interface was at, or nil if no interface was found -function vlayer.remove_interface(surface, position) - local entities = surface.find_entities_filtered{ - name = { "storage-chest", "requester-chest", "constant-combinator", "electric-energy-interface" }, - force = "neutral", - position = position, - radius = 2, - limit = 1, - } - - -- Get the details which will be returned - if #entities == 0 then - return nil, nil, nil - end - - local interface = entities[1] - local name = interface.name - local sur = interface.surface - local pos = interface.position - - -- Return the type of interface removed and do some clean up - if name == "storage-chest" then - local inventory = assert(interface.get_inventory(defines.inventory.chest)) - ExpUtil.transfer_inventory_to_surface{ - inventory = inventory, - surface = interface.surface, - name = "iron-chest", - allow_creation = true, - } - table.remove_element(vlayer_data.entity_interfaces.storage_input, interface) - interface.destroy() - - return "storage-input", sur, pos - elseif name == "requester-chest" then - local inventory = assert(interface.get_inventory(defines.inventory.chest)) - ExpUtil.transfer_inventory_to_surface{ - inventory = inventory, - surface = interface.surface, - name = "iron-chest", - allow_creation = true, - } - table.remove_element(vlayer_data.entity_interfaces.storage_output, interface) - interface.destroy() - - return "storage-output", sur, pos - elseif name == "constant-combinator" then - table.remove_element(vlayer_data.entity_interfaces.circuit, interface) - interface.destroy() - - return "circuit", sur, pos - elseif name == "electric-energy-interface" then - vlayer_data.storage.energy = vlayer_data.storage.energy + interface.energy - table.remove_element(vlayer_data.entity_interfaces.energy, interface) - interface.destroy() - - return "energy", sur, pos - end -end - -local function on_surface_event() - if config.mimic_surface then - local surface = game.get_surface(config.mimic_surface) - - if surface then - vlayer_data.surface = surface - return - end - end - - if not vlayer_data.surface.index then - -- Our fake surface data never has an index, we test for this to avoid unneeded copies from the config - vlayer_data.surface = table.deep_copy(config.surface) - end -end - ---- Handle all storage IO and attempt allocation of unallocated items -Event.on_nth_tick(config.update_tick_storage, function(_) - handle_input_interfaces() - handle_output_interfaces() - handle_unallocated() -end) - ---- Handle all energy and circuit updates -Event.on_nth_tick(config.update_tick_energy, function(_) - handle_circuit_interfaces() - handle_energy_interfaces() -end) - -Event.add(defines.events.on_surface_created, on_surface_event) -Event.add(defines.events.on_surface_renamed, on_surface_event) -Event.add(defines.events.on_surface_imported, on_surface_event) -Event.on_init(on_surface_event) -- Default surface always exists, does not trigger on_surface_created - -return vlayer diff --git a/exp_legacy/module/modules/control/warps.lua b/exp_legacy/module/modules/control/warps.lua deleted file mode 100644 index fdca5b62..00000000 --- a/exp_legacy/module/modules/control/warps.lua +++ /dev/null @@ -1,488 +0,0 @@ ---[[-- Control Module - Warps -- Stores warps for each force. -@control Warps -@alias Warps - -@usage-- Making a new spawn warp -local player = game.player -local force = player.force -local spawn_id = Warps.add_warp(force.name, player.physical_surface, player.physical_position, player.name, 'Spawn') - -Warps.set_spawn_warp(spawn_id, force) -Warps.make_warp_tag(spawn_id) - -@usage-- Making a new warp with a warp area -local player = game.player -local force = player.force -local warp_id = Warps.add_warp(force.name, player.physical_surface, player.physical_position, player.name) - -Warps.make_warp_area(warp_id) -Warps.make_warp_tag(warp_id) - -]] - -local Datastore = require("modules.exp_legacy.expcore.datastore") --- @dep expcore.datastore -local Storage = require("modules/exp_util/storage") -local config = require("modules.exp_legacy.config.gui.warps") --- @dep config.warps - ---- Stores all data for the warp system -local WrapData = Datastore.connect("WrapData") -WrapData:set_serializer(function(raw_key) return raw_key.warp_id end) - -local Warps = {} - ---- @class ExpScenario_Warps.force_warps: { spawn: string, [number]: string } - --- Storage lookup table for force name to task ids -local force_warps = { _uid = 1 } ---- @cast force_warps table -Storage.register(force_warps, function(tbl) - force_warps = tbl -end) - --- Create an array of entity names that will be added to the remove filter -local remove_warp_area_entity_names = {} -for _, entity in pairs(config.entities) do - table.insert(remove_warp_area_entity_names, entity[1]) -end - --- When a warp is updated change its chat tag and restore the warp order -WrapData:on_update(function(warp_id, warp, old_warp) - if warp then - warp.updates = warp.updates + 1 - -- Update the map chart tag if there is one - if warp.tag then - Warps.make_warp_tag(warp_id) - end - - -- Check that the name of the warp has been changed - if not old_warp or warp.name == old_warp.name then return end - - -- Get the names of all the warp points for this force - local force_name = warp.force_name - local warp_ids = force_warps[force_name] - local spawn_id = warp_ids.spawn - - local warp_names = {} --- @type table - for _, next_warp_id in pairs(warp_ids) do - local next_warp = WrapData:get(next_warp_id) - if next_warp_id ~= spawn_id then - warp_names[next_warp.name .. next_warp_id] = next_warp_id - end - end - - -- Sort the warp names in alphabetical order - local new_warp_ids = table.get_values(table.key_sort(warp_names)) - --- @cast new_warp_ids ExpScenario_Warps.force_warps - table.insert(new_warp_ids, 1, spawn_id) - new_warp_ids.spawn = spawn_id - force_warps[force_name] = new_warp_ids - end -end) - ---- Map Integration. --- functions used to create and alter warps with in the map --- @section mapIntegration - ---[[-- Add or update the chat tag for this warp -@tparam string warp_id the uid of the warp you want the chart tag for -@treturn boolean true if a new tag was made, false if it was updated - -@usage-- Adding a chart tag for a new warp -local tag_added = Warps.make_warp_tag(warp_id) - -]] -function Warps.make_warp_tag(warp_id) - local warp = WrapData:get(warp_id) - local name = warp.name - local icon = warp.icon - - -- Edit the existing tag if it is present - local tag = warp.tag - - if tag and tag.valid then - tag.text = "Warp: " .. name - tag.icon = icon - return false - end - - -- Make a new tag if one did not exist - local force = game.forces[warp.force_name] - local surface = warp.surface - local position = warp.position - - tag = force.add_chart_tag(surface, { - position = { position.x + 0.5, position.y + 0.5 }, - text = "Warp: " .. name, - icon = icon, - }) - - -- Add the tag to this warp, store.update not needed as we dont want it to trigger - warp.tag = tag - return true -end - ---[[-- Remove the chart tag for this warp -@tparam string warp_id the uid for the warp that you want to remove the chart tag from -@treturn boolean true if the tag was valid and was removed, false if the tag was invalid - -@usage-- Removing the chart tag from a warp -local removed = Warps.remove_warp_tag(warp_id) - -]] -function Warps.remove_warp_tag(warp_id) - local warp = WrapData:get(warp_id) - - -- Check there is a tag to remove - local tag = warp.tag - if not tag or not tag.valid then - warp.tag = nil - return false - end - - -- Remove the warp chart tag if it is valid - tag.destroy() - warp.tag = nil - - return true -end - ---[[-- Add a warp area for the warp, purely cosmetic -@tparam string warp_id the uid of the warp you want the area for - -@usage-- Adding a warp area for a warp -Warps.make_warp_area(warp_id) - -]] -function Warps.make_warp_area(warp_id) - local warp = WrapData:get(warp_id) - local surface = warp.surface - local position = warp.position - local posx = position.x - local posy = position.y - - -- Get the tile that is being replaced, store.update not needed as we don't want it to trigger - local old_tile = surface.get_tile(position).name - warp.old_tile = old_tile - - -- Add a tile pattern on top of the base - local tiles = {} - for _, tile in pairs(config.tiles) do - table.insert(tiles, { name = tile[1], position = { tile[2] + posx, tile[3] + posy } }) - end - - surface.set_tiles(tiles) - - -- Add entities to the warp structure - for _, entity in pairs(config.entities) do - entity = surface.create_entity{ - name = entity[1], - position = { entity[2] + posx, entity[3] + posy }, - force = "neutral", - } - entity.destructible = false - entity.health = 0 - entity.minable_flag = false - entity.rotatable = false - - -- Save reference of the last power pole - if entity.type == "electric-pole" then - warp.electric_pole = entity - end - end -end - ---[[-- Remove the warp area for a warp -@tparam string warp_id the uid of the warp that you want to remove the area for - -@usage-- Remove the warp area for a warp -Warps.remove_warp_area(warp_id) - -]] -function Warps.remove_warp_area(warp_id) - local warp = WrapData:get(warp_id) - local position = warp.position - local surface = warp.surface - local radius = config.standard_proximity_radius - - -- Check that a warp area was created previously - local old_tile = warp.old_tile - if not old_tile then return end - - -- Restore the original tiles before the creation of the warp - local tiles = {} - - for _, tile in pairs(config.tiles) do - table.insert(tiles, { name = old_tile, position = { tile[2] + position.x, tile[3] + position.y } }) - end - - surface.set_tiles(tiles) - - local area = { - { position.x - radius, position.y - radius }, - { position.x + radius, position.y + radius }, - } - - -- Remove warp structure entities - local entities = surface.find_entities_filtered{ force = "neutral", area = area, name = remove_warp_area_entity_names } - for _, entity in pairs(entities) do - -- Destroy them, this will leave corpses of the entities that it destroyed. - if entity and entity.valid and entity.destructible == false then - entity.destructible = true - entity.die(entity.force) - end - end - - -- Rechart map area, useful if warp is not covered by a radar - game.forces[warp.force_name].chart(surface, area) -end - ---[[-- Set a warp to be the spawn point for a force, force must own this warp -@tparam string warp_id the uid of the warp that you want to be the spawn for the force -@tparam LuaForce force the force that you want to set the spawn for - -@usage-- Set your forces spawn to a warp -Warps.set_spawn_warp(warp_id, game.player.force) - -]] -function Warps.set_spawn_warp(warp_id, force) - -- Check the force owns this warp - local warp = WrapData:get(warp_id) - if warp.force_name ~= force.name then - return - end - -- Set this warp as the spawn - local warp_ids = force_warps[warp.force_name] - if not warp_ids then - warp_ids = {} - force_warps[warp.force_name] = warp_ids - end - warp_ids.spawn = warp_id - -- Set the forces spawn to this warp - force.set_spawn_position(warp.position, warp.surface) -end - ---[[-- Teleport a player to a warp point -@tparam string warp_id the uid of the warp to send the player to -@tparam LuaPlayer player the player to teleport to the warp - -@usage-- Teleport yourself to a warp point -Warps.teleport_player(warp_id, game.player) - -]] -function Warps.teleport_player(warp_id, player) - local warp = WrapData:get(warp_id) - local surface = warp.surface - local position = { - x = warp.position.x + 0.5, - y = warp.position.y + 0.5, - } - - if player.vehicle then - -- Teleport the entity - local entity = player.vehicle - local goto_position = surface.find_non_colliding_position(entity.name, position, 32, 1) - -- Surface teleport can only be done for players and cars at the moment. (with surface as an peramitor it gives this error) - if entity.type == "car" then - entity.teleport(goto_position, surface) - elseif surface.index == entity.surface.index then - -- Try teleport the entity - if not entity.teleport(goto_position) then - player.driving = false - -- Need to calculate new goto_position because entities have different collision boxes - goto_position = surface.find_non_colliding_position("character", position, 32, 1) - player.teleport(goto_position, surface) - end - end - else - -- Teleport the player - local goto_position = surface.find_non_colliding_position("character", position, 32, 1) - if player.driving then player.driving = false end - player.teleport(goto_position, surface) - end -end - ---- Setters. --- functions used to created and alter warps --- @section setters - ---[[-- Add a new warp for a force, the warp must have a surface and a position -@tparam string force_name the name of the force to add the warp for -@tparam LuaSurface surface the surface that the warp will be on -@tparam Concepts.Position position the position that the warp will be on -@tparam[opt] string player_name the name of the player that is adding the warp -@tparam[opt] string warp_name the name of the warp that is being added, if omited default is used -@treturn string the uid of the warp which was created - -@usage-- Adding a new warp for your force at your position -local player = game.player -local warp_id = Warps.add_warp(player.force.name, player.physical_surface, player.physical_position, player.name) - -]] -function Warps.add_warp(force_name, surface, position, player_name, warp_name) - -- Get new warp id - local warp_id = tostring(force_warps._uid) - force_warps._uid = force_warps._uid + 1 - warp_name = warp_name or "New warp" - - -- Get the existing warps for this force - local warp_ids = force_warps[force_name] - if not warp_ids then - warp_ids = {} - force_warps[force_name] = warp_ids - end - - -- Insert the warp id into the force warps - table.insert(warp_ids, warp_id) - - -- Create the editing table - local editing = {} - if player_name then - editing[player_name] = true - end - - -- Add the new warp to the store - WrapData:set(warp_id, { - warp_id = warp_id, - force_name = force_name, - name = warp_name, - icon = { type = config.default_icon.type, name = config.default_icon.name }, - surface = surface, - position = { - x = math.floor(position.x), - y = math.floor(position.y), - }, - last_edit_name = player_name or "", - last_edit_time = game.tick, - currently_editing = editing, - updates = 0, - }) - - return warp_id -end - ---[[-- Removes a warp and any data linked to it -@tparam string warp_id the uid of the warp that you want to remove - -@usage-- Removing a warp -Warps.remove_warp(warp_id) - -]] -function Warps.remove_warp(warp_id) - local warp = WrapData:get(warp_id) - local force_name = warp.force_name - Warps.remove_warp_tag(warp_id) - Warps.remove_warp_area(warp_id) - WrapData:remove(warp_id) - table.remove_element(force_warps[force_name], warp_id) -end - ---[[-- Update the name and icon for a warp -@tparam string warp_id the uid of the warp that you want to update -@tparam[opt] string new_name the new name that you want the warp to have -@tparam[opt] string new_icon the new icon that you want the warp to have -@tparam[opt='server'] string player_name the name of the player that made the edit - -@usage-- Changing the name and icon for a warp -Warps.update_warp(warp_id, 'My Warp', 'iron-plate', game.player.name) - -]] -function Warps.update_warp(warp_id, new_name, new_icon, player_name) - WrapData:update(warp_id, function(_, warp) - -- If the icon is not valid then replace with the old icon - if new_icon and not new_icon.name or not new_icon.type then - new_icon = warp.icon - end - - warp.last_edit_name = player_name or "" - warp.last_edit_time = game.tick - warp.name = new_name or warp.name - warp.icon = new_icon or warp.icon - end) -end - ---[[-- Set the editing state for a player, can be used as a warning or to display a text field -@tparam string warp_id the uid of the warp that you want to effect -@tparam string player_name the name of the player you want to set the state for -@tparam boolean state the new state to set editing to - -@usage-- Setting your editing state to true -Warps.set_editing(warp_id, game.player.name, true) - -]] -function Warps.set_editing(warp_id, player_name, state) - WrapData:update(warp_id, function(_, warp) - warp.currently_editing[player_name] = state - end) -end - ---[[-- Adds an update handler for when a warp is added, removed, or updated -@tparam function handler the handler which is called when a warp is updated - -@usage-- Add a game print when a warp is updated -Warps.on_update(function(warp) - game.print(warp.force_name..' now has the warp: '..warp.name) -end) - -]] -function Warps.on_update(handler) - WrapData:on_update(handler) -end - ---- Getters. --- function used to get information about warps --- @section getters - ---[[-- Gets the warp information that is linked with this id -@tparam string warp_id the uid of the warp you want to get -@treturn table the warp information - -@usage-- Getting warp information outside of on_update -local warp = Warps.get_warp(warp_id) - -]] -function Warps.get_warp(warp_id) - return WrapData:get(warp_id) -end - ---[[-- Gets all the warp ids that a force has -@tparam string force_name the name of the force that you want the warp ids for -@treturn table an array of all the warp ids - -@usage-- Getting the warp ids for a force -local warp_ids = Warps.get_force_warp_ids(game.player.force.name) - -]] -function Warps.get_force_warp_ids(force_name) - return force_warps[force_name] or {} -end - ---[[-- Get the id of the spawn warp -@tparam string force_name the name of the force that you want to get the spawn warp for -@treturn ?string|nil the uid of the spawn warp for this force if there is one - -@usage-- Getting the spawn warp id -local spawn_id = Warps.get_spawn_warp_id(game.player.force.name) - -]] -function Warps.get_spawn_warp_id(force_name) - local warp_ids = force_warps[force_name] or {} - return warp_ids.spawn -end - ---[[-- Gets the editing state for a player -@tparam string warp_id the uid of the warp you want to check -@tparam string player_name the name of the player that you want to check -@treturn boolean weather the player is currently editing this warp - -@usage-- Check if a player is editing a warp or not -local editing = Warps.get_editing(warp_id, game.player.name) - -]] -function Warps.get_editing(warp_id, player_name) - local warp = WrapData:get(warp_id) - return warp.currently_editing[player_name] -end - --- Module return -return Warps diff --git a/exp_legacy/module/modules/gui/vlayer.lua b/exp_legacy/module/modules/gui/vlayer.lua deleted file mode 100644 index 1060b786..00000000 --- a/exp_legacy/module/modules/gui/vlayer.lua +++ /dev/null @@ -1,547 +0,0 @@ ---[[-- Gui Module - Virtual Layer - - Adds a virtual layer to store power to save space. - @gui Virtual Layer - @alias vlayer_container -]] - -local Gui = require("modules/exp_gui") -local Roles = require("modules/exp_roles") -local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event -local format_number = require("util").format_number --- @dep util -local config = require("modules.exp_legacy.config.vlayer") --- @dep config.vlayer -local vlayer = require("modules.exp_legacy.modules.control.vlayer") - -local Selection = require("modules/exp_util/selection") -local SelectArea = Selection.connect("ExpGui_VLayer") - ---- Align an aabb to the grid by expanding it -local function aabb_align_expand(aabb) - return { - left_top = { x = math.floor(aabb.left_top.x), y = math.floor(aabb.left_top.y) }, - right_bottom = { x = math.ceil(aabb.right_bottom.x), y = math.ceil(aabb.right_bottom.y) }, - } -end - -local vlayer_container --- @type ExpElement -local vlayer_gui_control_type --- @type ExpElement -local vlayer_gui_control_list --- @type ExpElement - -local vlayer_control_type_list = { - [1] = "energy", - [2] = "circuit", - [3] = "storage_input", - [4] = "storage_output", -} - -local function pos_to_gps_string(pos, surface_name) - return "[gps=" .. string.format("%.1f", pos.x) .. "," .. string.format("%.1f", pos.y) .. "," .. surface_name .. "]" -end - -local function format_energy(amount, unit) - if amount < 1 then - return "0 " .. unit - end - - local suffix = "" - local suffix_list = { - ["P"] = 1000000000000000, - ["T"] = 1000000000000, - ["G"] = 1000000000, - ["M"] = 1000000, - ["k"] = 1000, - } - - for letter, limit in pairs(suffix_list) do - if math.abs(amount) >= limit then - amount = string.format("%.1f", amount / limit) - suffix = letter - break - end - end - - local k - local formatted = amount - - while true do - formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", "%1,%2") - - if (k == 0) then - break - end - end - - return formatted .. " " .. suffix .. unit -end - -local ExpUtil = require("modules/exp_util") ---- When an area is selected to add protection to the area -SelectArea:on_selection(function(event) - log(ExpUtil.format_any(event)) - local area = aabb_align_expand(event.area) - local player = game.players[event.player_index] - - if not player then - return - end - - local container = Gui.get_left_element(vlayer_container, player) - local disp = container.frame["vlayer_st_2"].disp.table - local target = vlayer_control_type_list[disp[vlayer_gui_control_type.name].selected_index] - local entities --- @type LuaEntity[] - - if config.power_on_space and event.surface and event.surface.platform and target == "energy" then - if (player.force --[[@as LuaForce]]).technologies[config.power_on_space_research.name].level >= config.power_on_space_research.level then - entities = event.surface.find_entities_filtered{ area = area, name = "constant-combinator", force = player.force } - else - player.print{ "vlayer.power-on-space-research", config.power_on_space_research.name, config.power_on_space_research.level } - return - end - else - entities = event.surface.find_entities_filtered{ area = area, name = "steel-chest", force = player.force } - end - - if #entities == 0 then - player.print{ "vlayer.steel-chest-detect" } - return - elseif #entities > 1 then - player.print{ "vlayer.result-unable", { "vlayer.control-type-" .. target:gsub("_", "-") }, { "vlayer.result-multiple" } } - return - end - - if not entities[1] then - return - end - - local e = entities[1] - local e_pos = { x = string.format("%.1f", e.position.x), y = string.format("%.1f", e.position.y) } - local e_circ = nil -- e.get_wire_connectors{ or_create = false } - - if e.name and e.name == "steel-chest" and (not assert(e.get_inventory(defines.inventory.chest)).is_empty()) then - player.print{ "vlayer.steel-chest-empty" } - return - end - - if (vlayer.get_interface_counts()[target] >= config.interface_limit[target]) then - player.print{ "vlayer.result-unable", { "vlayer.control-type-" .. target:gsub("_", "-") }, { "vlayer.result-limit" } } - return - end - - e.destroy() - - if target == "energy" then - if not vlayer.create_energy_interface(event.surface, e_pos, player) then - player.print{ "vlayer.result-unable", { "vlayer.control-type-energy" }, { "vlayer.result-space" } } - return - end - elseif target == "circuit" then - vlayer.create_circuit_interface(event.surface, e_pos, e_circ, player) - elseif target == "storage_input" then - vlayer.create_input_interface(event.surface, e_pos, e_circ, player) - elseif target == "storage_output" then - vlayer.create_output_interface(event.surface, e_pos, e_circ, player) - end - - game.print{ "vlayer.interface-result", player.name, pos_to_gps_string(e_pos, event.surface.name), { "vlayer.result-build" }, { "vlayer.control-type-" .. target:gsub("_", "-") } } -end) - ---- Display label for the number of solar panels --- @element vlayer_gui_display_item_solar_name -local vlayer_gui_display_item_solar_name = Gui.define("vlayer_gui_display_item_solar_name") - :draw{ - type = "label", - name = Gui.from_name, - caption = { "vlayer.display-item-solar" }, - style = "heading_2_label", - }:style{ - width = 200, - } - -local vlayer_gui_display_item_solar_count = Gui.define("vlayer_gui_display_item_solar_count") - :draw{ - type = "progressbar", - name = Gui.from_name, - caption = "", - value = 0, - style = "electric_satisfaction_statistics_progressbar", - }:style{ - width = 200, - font = "heading-2", - } - ---- Display label for the number of accumulators --- @element vlayer_gui_display_item_accumulator_name -local vlayer_gui_display_item_accumulator_name = Gui.define("vlayer_gui_display_item_accumulator_name") - :draw{ - type = "label", - name = Gui.from_name, - caption = { "vlayer.display-item-accumulator" }, - style = "heading_2_label", - }:style{ - width = 200, - } - -local vlayer_gui_display_item_accumulator_count = Gui.define("vlayer_gui_display_item_accumulator_count") - :draw{ - type = "progressbar", - name = Gui.from_name, - caption = "", - value = 0, - style = "electric_satisfaction_statistics_progressbar", - }:style{ - width = 200, - font = "heading-2", - } - ---- Display label for the surface area --- @element vlayer_gui_display_signal_surface_area_name -local vlayer_gui_display_signal_surface_area_name = Gui.define("vlayer_gui_display_signal_surface_area_name") - :draw{ - type = "label", - name = Gui.from_name, - caption = { "vlayer.display-remaining-surface-area" }, - tooltip = { "vlayer.display-remaining-surface-area-tooltip" }, - style = "heading_2_label", - }:style{ - width = 200, - } - -local vlayer_gui_display_signal_surface_area_count = Gui.define("vlayer_gui_display_signal_surface_area_count") - :draw{ - type = "progressbar", - name = Gui.from_name, - caption = "", - value = 0, - style = "electric_satisfaction_statistics_progressbar", - }:style{ - width = 200, - font = "heading-2", - } - ---- Display label for the sustained energy production --- @element vlayer_gui_display_signal_sustained_name -local vlayer_gui_display_signal_sustained_name = Gui.define("vlayer_gui_display_signal_sustained_name") - :draw{ - type = "label", - name = Gui.from_name, - caption = { "vlayer.display-sustained-production" }, - tooltip = { "vlayer.display-sustained-production-tooltip" }, - style = "heading_2_label", - }:style{ - width = 200, - } - -local vlayer_gui_display_signal_sustained_count = Gui.define("vlayer_gui_display_signal_sustained_count") - :draw{ - type = "progressbar", - name = Gui.from_name, - caption = "", - value = 0, - style = "electric_satisfaction_statistics_progressbar", - }:style{ - width = 200, - font = "heading-2", - } - ---- Display label for the current energy production --- @element vlayer_gui_display_signal_production_name -local vlayer_gui_display_signal_production_name = Gui.define("vlayer_gui_display_signal_production_name") - :draw{ - type = "label", - name = Gui.from_name, - caption = { "vlayer.display-current-production" }, - tooltip = { "vlayer.display-current-production-tooltip" }, - style = "heading_2_label", - }:style{ - width = 200, - } - -local vlayer_gui_display_signal_production_count = Gui.define("vlayer_gui_display_signal_production_count") - :draw{ - type = "progressbar", - name = Gui.from_name, - caption = "", - value = 0, - style = "electric_satisfaction_statistics_progressbar", - }:style{ - width = 200, - font = "heading-2", - } - ---- Display label for the sustained energy capacity --- @element vlayer_gui_display_signal_capacity_name -local vlayer_gui_display_signal_capacity_name = Gui.define("vlayer_gui_display_signal_capacity_name") - :draw{ - type = "label", - name = Gui.from_name, - caption = { "vlayer.display-current-capacity" }, - tooltip = { "vlayer.display-current-capacity-tooltip" }, - style = "heading_2_label", - }:style{ - width = 200, - } - -local vlayer_gui_display_signal_capacity_count = Gui.define("vlayer_gui_display_signal_capacity_count") - :draw{ - type = "progressbar", - name = Gui.from_name, - caption = "", - value = 0, - style = "electric_satisfaction_statistics_progressbar", - }:style{ - width = 200, - font = "heading-2", - } - ---- A vertical flow containing all the displays labels and their counts --- @element vlayer_display_set -local vlayer_display_set = Gui.define("vlayer_display_set") - :draw(function(_, parent, name) - local vlayer_set = parent.add{ type = "flow", direction = "vertical", name = name } - local disp = Gui.elements.scroll_table(vlayer_set, 400, 2, "disp") - - vlayer_gui_display_item_solar_name(disp) - vlayer_gui_display_item_solar_count(disp) - vlayer_gui_display_item_accumulator_name(disp) - vlayer_gui_display_item_accumulator_count(disp) - vlayer_gui_display_signal_surface_area_name(disp) - vlayer_gui_display_signal_surface_area_count(disp) - vlayer_gui_display_signal_sustained_name(disp) - vlayer_gui_display_signal_sustained_count(disp) - vlayer_gui_display_signal_production_name(disp) - vlayer_gui_display_signal_production_count(disp) - vlayer_gui_display_signal_capacity_name(disp) - vlayer_gui_display_signal_capacity_count(disp) - - return vlayer_set - end) - -local function vlayer_gui_list_refresh(player) - local container = Gui.get_left_element(vlayer_container, player) - local disp = container.frame["vlayer_st_2"].disp.table - local target = disp[vlayer_gui_control_type.name].selected_index - local full_list = {} - - if target then - local interface = vlayer.get_interfaces()[vlayer_control_type_list[target]] - - for i = 1, vlayer.get_interface_counts()[vlayer_control_type_list[target]], 1 do - local entity = assert(interface[i]) - local entity_position = entity.position - table.insert(full_list, i .. " X " .. entity_position.x .. " Y " .. entity_position.y) - end - - disp[vlayer_gui_control_list.name].items = full_list - end -end - ---- A drop down list filter by this type --- @element vlayer_gui_control_type -vlayer_gui_control_type = Gui.define("vlayer_gui_control_type") - :draw{ - type = "drop-down", - name = Gui.from_name, - items = { { "vlayer.control-type-energy" }, { "vlayer.control-type-circuit" }, { "vlayer.control-type-storage-input" }, { "vlayer.control-type-storage-output" } }, - selected_index = 1, - }:style{ - width = 200, - }:on_selection_state_changed(function(def, player, element) - vlayer_gui_list_refresh(player) - end) - ---- A drop down list to see the exact item to remove --- @element vlayer_gui_control_list -vlayer_gui_control_list = Gui.define("vlayer_gui_control_list") - :draw{ - type = "drop-down", - name = Gui.from_name, - }:style{ - width = 200, - } - ---- A button to refresh the remove list --- @element vlayer_gui_control_refresh -local vlayer_gui_control_refresh = Gui.define("vlayer_gui_control_refresh") - :draw{ - type = "button", - name = Gui.from_name, - caption = { "vlayer.control-refresh" }, - }:style{ - width = 200, - }:on_click(function(def, player, element) - vlayer_gui_list_refresh(player) - end) - ---- A button to check if the item is the one wanted to remove --- @element vlayer_gui_control_see -local vlayer_gui_control_see = Gui.define("vlayer_gui_control_see") - :draw{ - type = "button", - name = Gui.from_name, - caption = { "vlayer.control-see" }, - }:style{ - width = 200, - }:on_click(function(def, player, element, event) - local target = assert(element.parent)[vlayer_gui_control_type.name].selected_index - local n = assert(element.parent)[vlayer_gui_control_list.name].selected_index - - if target and vlayer_control_type_list[target] and n > 0 then - local i = vlayer.get_interfaces() - local entity = i[vlayer_control_type_list[target]][n] - if entity and entity.valid then - player.set_controller{ type = defines.controllers.remote, position = entity.position, surface = entity.surface } - player.print{ "vlayer.result-interface-location", { "vlayer.control-type-" .. vlayer_control_type_list[target]:gsub("_", "-") }, pos_to_gps_string(entity.position, entity.surface.name) } - end - end - end) - ---- A button used to build the vlayer interface --- @element vlayer_gui_control_build -local vlayer_gui_control_build = Gui.define("vlayer_gui_control_build") - :draw{ - type = "button", - name = Gui.from_name, - caption = { "vlayer.control-build" }, - }:style{ - width = 200, - }:on_click(function(def, player, element) - if SelectArea:stop(player) then - player.print{ "exp_util.selection_exit", { "vlayer.main-tooltip" } } - else - SelectArea:start(player) - player.print{ "exp_util.selection_enter", { "vlayer.main-tooltip" } } - end - - vlayer_gui_list_refresh(player) - end) - ---- A button used to remove the vlayer interface --- @element vlayer_gui_control_remove -local vlayer_gui_control_remove = Gui.define("vlayer_gui_control_remove") - :draw{ - type = "button", - name = Gui.from_name, - caption = { "vlayer.control-remove" }, - }:style{ - width = 200, - }:on_click(function(def, player, element) - local target = assert(element.parent)[vlayer_gui_control_type.name].selected_index - local n = assert(element.parent)[vlayer_gui_control_list.name].selected_index - - if target and vlayer_control_type_list[target] and n > 0 then - local i = vlayer.get_interfaces() - - if i and i[vlayer_control_type_list[target]] then - local entity = assert(i[vlayer_control_type_list[target]][n]) - local interface_type, interface_surface, interface_position = vlayer.remove_interface(entity.surface, entity.position) - - if interface_type then - game.print{ "vlayer.interface-result", player.name, pos_to_gps_string(interface_position, interface_surface.name), { "vlayer.result-remove" }, { "vlayer.control-type-" .. interface_type } } - end - end - end - - vlayer_gui_list_refresh(player) - end) - ---- A vertical flow containing all the control buttons --- @element vlayer_control_set -local vlayer_control_set = Gui.define("vlayer_control_set") - :draw(function(_, parent, name) - local player = Gui.get_player(parent) - local vlayer_set = parent.add{ type = "flow", direction = "vertical", name = name } - local disp = Gui.elements.scroll_table(vlayer_set, 400, 2, "disp") - - vlayer_gui_control_type(disp) - vlayer_gui_control_list(disp) - vlayer_gui_control_refresh(disp) - vlayer_gui_control_see(disp) - local b = vlayer_gui_control_build(disp) - local r = vlayer_gui_control_remove(disp) - local v = Roles.player_has_permission(player, "exp_scenario.gui.vlayer_edit") - b.visible = v - r.visible = v - - return vlayer_set - end) - ---- The main container for the vlayer gui --- @element vlayer_container -vlayer_container = Gui.define("vlayer_container") - :draw(function(definition, parent) - local container = Gui.elements.container(parent, 400) - - vlayer_display_set(container, "vlayer_st_1") - vlayer_control_set(container, "vlayer_st_2") - - return container.parent - end) - ---- Add the element to the left flow with a toolbar button -Gui.add_left_element(vlayer_container, false) -Gui.toolbar.create_button{ - name = "vlayer_toggle", - left_element = vlayer_container, - sprite = "entity/solar-panel", - tooltip = { "vlayer.main-tooltip" }, - visible = function(player, element) - return Roles.player_has_permission(player, "exp_scenario.gui.vlayer") - end -} - ---- Update the visibly of the buttons based on a players roles -local function role_update_event(event) - local player = game.players[event.player_index] - local visible = Roles.player_has_permission(player, "exp_scenario.gui.vlayer_edit") - local container = Gui.get_left_element(vlayer_container, player) - local disp = container.frame["vlayer_st_2"].disp.table - disp[vlayer_gui_control_build.name].visible = visible - disp[vlayer_gui_control_remove.name].visible = visible -end - -Event.add(Roles.events.on_player_roles_changed, role_update_event) - -Event.on_nth_tick(config.update_tick_gui, function(_) - local stats = vlayer.get_statistics() - local items = vlayer.get_items() - local items_alloc = vlayer.get_allocated_items() - - local vlayer_display = { - [vlayer_gui_display_item_solar_count.name] = { - val = (items_alloc["solar-panel"] / math.max(items["solar-panel"], 1)), - cap = format_number(items_alloc["solar-panel"], false) .. " / " .. format_number(items["solar-panel"], false), - }, - [vlayer_gui_display_item_accumulator_count.name] = { - val = (items_alloc["accumulator"] / math.max(items["accumulator"], 1)), - cap = format_number(items_alloc["accumulator"], false) .. " / " .. format_number(items["accumulator"], false), - }, - [vlayer_gui_display_signal_surface_area_count.name] = { - val = (stats.total_surface_area / math.max(stats.surface_area, 1)), - cap = format_number(stats.remaining_surface_area) - }, - [vlayer_gui_display_signal_sustained_count.name] = { - val = (stats.energy_sustained / math.max(stats.energy_total_production, 1)), - cap = format_energy(stats.energy_sustained, "W") .. " / " .. format_energy(stats.energy_total_production, "W") - }, - [vlayer_gui_display_signal_production_count.name] = { - val = (stats.energy_production / math.max(stats.energy_max, 1)), - cap = format_energy(stats.energy_production, "W") .. " / " .. format_energy(stats.energy_max, "W"), - }, - [vlayer_gui_display_signal_capacity_count.name] = { - val = (stats.energy_storage / math.max(stats.energy_capacity, 1)), - cap = format_energy(stats.energy_storage, "J") .. " / " .. format_energy(stats.energy_capacity, "J"), - }, - } - - for _, player in pairs(game.connected_players) do - local container = Gui.get_left_element(vlayer_container, player) - local disp = container.frame["vlayer_st_1"].disp.table - - for k, v in pairs(vlayer_display) do - disp[k].caption = v.cap - - if v.val then - disp[k].value = v.val - end - end - end -end) diff --git a/exp_legacy/module/modules/gui/warp-list.lua b/exp_legacy/module/modules/gui/warp-list.lua deleted file mode 100644 index 5c456558..00000000 --- a/exp_legacy/module/modules/gui/warp-list.lua +++ /dev/null @@ -1,909 +0,0 @@ ---[[-- Gui Module - Warp List - - Adds a warp list gui which allows players to add and remove warp points - @gui Warps-List - @alias warp_list -]] - -local ExpUtil = require("modules/exp_util") -local Gui = require("modules/exp_gui") -local Datastore = require("modules.exp_legacy.expcore.datastore") --- @dep expcore.datastore -local Storage = require("modules/exp_util/storage") -local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event -local Roles = require("modules/exp_roles") -local Colors = require("modules/exp_util/include/color") -local config = require("modules.exp_legacy.config.gui.warps") --- @dep config.gui.warps -local Warps = require("modules.exp_legacy.modules.control.warps") --- @dep modules.control.warps - -local format_time = ExpUtil.format_time_factory_locale{ format = "short", hours = true, minutes = true } - ---- Stores all data for the warp gui -local WrapGuiData = Datastore.connect("WrapGuiData") -WrapGuiData:set_serializer(Datastore.name_serializer) -local PlayerInRange = WrapGuiData:combine("PlayerInRange") -PlayerInRange:set_default(false) -local PlayerCooldown = WrapGuiData:combine("PlayerCooldown") -PlayerCooldown:set_default(0) - ---- Table that stores a boolean value of weather to keep the warp gui open -local keep_gui_open = {} -Storage.register(keep_gui_open, function(tbl) - keep_gui_open = tbl -end) - ---- Styles used for sprite buttons -local Styles = { - sprite22 = { height = 22, width = 22, padding = -2 }, - sprite32 = { height = 32, width = 32, left_margin = 1 }, -} ---- Status icon of a warp -local warp_status_icons = { - cooldown = "[img=utility/multiplayer_waiting_icon]", - not_available = "[img=utility/set_bar_slot]", - bypass = "[img=utility/side_menu_bonus_icon]", - current = "[img=utility/side_menu_map_icon]", - connected = "[img=utility/logistic_network_panel_white]", - different = "[img=utility/warning_white]", -} - ---- Returns if a player is allowed to edit the given warp ---- If a player is allowed to use the edit buttons -local function check_player_permissions(player, action, warp) - -- Check if the action is allow edit and then check bypass settings - if action == "allow_edit_warp" then - -- Check if the warp is the spawn then it cant be edited - local spawn_id = Warps.get_spawn_warp_id(player.force.name) - if spawn_id == warp.warp_id then - return false - end - - -- Check if the player being the last to edit will override existing permisisons - if config.user_can_edit_own_warps and warp.last_edit_name == player.name then - return true - end - end - - -- Check player has permission based on value in the config - local action_config = config[action] - if action_config == "all" then - return true - elseif action_config == "admin" then - return player.admin - elseif action_config == "exp_roles" then - return Roles.player_has_permission(player, config["exp_roles_" .. action]) - end - - -- Return false as all other conditions have not been met - return false -end - ---- Will add a new warp to the list, checks if the player is too close to an existing one --- @element add_new_warp -local add_new_warp = Gui.define("add_new_warp") - :draw{ - type = "sprite-button", - sprite = "utility/add", - tooltip = { "warp-list.add-tooltip" }, - style = "shortcut_bar_button", - name = Gui.from_name, - } - :style(Styles.sprite22) - :on_click(function(def, player, element) - if player.controller_type ~= defines.controllers.character then return end - -- Add the new warp - local force_name = player.force.name - local surface = player.physical_surface - local position = player.physical_position - - -- Check if the warp is too close to water - local water_tiles = surface.find_tiles_filtered{ collision_mask = "water_tile", radius = config.standard_proximity_radius + 1, position = position } - if #water_tiles > 0 then - player.print( - { "expcore-commands.command-fail", { "warp-list.too-close-to-water", config.standard_proximity_radius + 1 } }, - { color = Colors.orange_red } - ) - if game.player then game.player.play_sound{ path = "utility/wire_pickup" } end - for _, tile in pairs(water_tiles) do - rendering.draw_sprite{ - sprite = "utility/rail_path_not_possible", - x_scale = 0.5, - y_scale = 0.5, - target = tile.position, - surface = surface, - players = { player }, - time_to_live = 60, - } - end - - return - end - - -- Check if there are player entities in the way (has a bigger radius because the enities that can be placed by a player are larger) - local entities = surface.find_entities_filtered{ - radius = config.standard_proximity_radius + 2.5, - position = position, - collision_mask = { - "item", "object", "player", "water_tile", - }, - } - -- Remove 1 because that is the current player - if #entities > 1 then - player.print( - { "expcore-commands.command-fail", { "warp-list.too-close-to-entities", config.standard_proximity_radius + 2.5 } }, - { color = Colors.orange_red } - ) - if game.player then game.player.play_sound{ path = "utility/wire_pickup" } end - local character = player.character - for _, entity in pairs(entities) do - if entity ~= character then - rendering.draw_sprite{ - sprite = "utility/rail_path_not_possible", - x_scale = 0.5, - y_scale = 0.5, - target = entity, - surface = surface, - players = { player }, - time_to_live = 60, - } - end - end - - return - end - - -- Create the warp - local warp_id = Warps.add_warp(force_name, surface, position, player.name) - Warps.make_warp_tag(warp_id) - Warps.make_warp_area(warp_id) - end) - ---- Warp icon button, this will trigger a warp when the player is able to --- @element warp_icon_button -local warp_icon_button = Gui.define("warp_icon_button") - :draw(function(def, parent, warp) - local warp_position = warp.position - - -- The SpritePath type is not the same as the SignalID type - local sprite = warp.icon.type .. "/" .. warp.icon.name - if warp.icon.type == "virtual" then - sprite = "virtual-signal/" .. warp.icon.name - end - - -- Draw the element - return parent.add{ - type = "sprite-button", - sprite = sprite, - name = def.name, - tooltip = { "warp-list.goto-tooltip", warp_position.x, warp_position.y }, - style = "slot_button", - } - end) - :style(Styles.sprite32) - :on_click(function(def, player, element) - if element.type == "choose-elem-button" then return end - local warp_id = assert(element.parent).caption - Warps.teleport_player(warp_id, player) - - -- Reset the warp cooldown if the player does not have unlimited warps - if not check_player_permissions(player, "bypass_warp_cooldown") then - PlayerCooldown:set(player, config.update_smoothing * config.cooldown_duration) - end - - PlayerInRange:set(player, warp_id) - end) - ---- The button that is visible when the warp is in edit state --- @element warp_icon_editing -local warp_icon_editing = Gui.define("warp_icon_editing") - :draw(function(def, parent, warp) - return parent.add{ - name = def.name, - type = "choose-elem-button", - elem_type = "signal", - signal = { type = warp.icon.type, name = warp.icon.name }, - tooltip = { "warp-list.goto-edit" }, - } - end) - :style(Styles.sprite32) - ---- Warp label, visible if the player is not in edit state --- @element warp_label -local warp_label = Gui.define("warp_label") - :draw(function(def, parent, warp) - local last_edit_name = warp.last_edit_name - local last_edit_time = warp.last_edit_time - -- Draw the element - return parent.add{ - type = "label", - caption = warp.name, - tooltip = { "warp-list.last-edit", last_edit_name, format_time(last_edit_time) }, - name = def.name, - } - end) - :style{ - single_line = true, - left_padding = 2, - right_padding = 2, - horizontally_stretchable = true, - } - :on_click(function(def, player, element) - local warp_id = assert(element.parent).caption - local warp = Warps.get_warp(warp_id) - player.set_controller{ type = defines.controllers.remote, position = warp.position, surface = warp.surface } - end) - ---- Warp status, visible if the player is not in edit state ---- This will show if the warp is connected or not --- @element warp_status -local warp_status = Gui.define("warp_status") - :draw{ - type = "label", - caption = "[img=utility/electricity_icon_unplugged]", -- Temporary icon - name = Gui.from_name, - } - :style{ - -- When editing mode because textbox is larger the icon would move up. - top_padding = 1, - single_line = false, - } - ---- Warp textfield, visible if the player is in edit state --- @element warp_textfield -local warp_textfield = Gui.define("warp_textfield") - :draw(function(def, parent, warp) - -- Draw the element - return parent.add{ - type = "textfield", - text = warp.name, - clear_and_focus_on_right_click = true, - name = def.name, - } - end) - :style{ - -- Required fields to make it squashable and strechable. - minimal_width = 10, - maximal_width = 300, - horizontally_squashable = true, - horizontally_stretchable = true, - -- Other styling - height = 22, - padding = -2, - left_margin = 2, - right_margin = 2, - } - :on_confirmed(function(def, player, element) - local warp_id = assert(element.parent).caption - local warp_name = element.text - local warp_icon = assert(element.parent).parent["icon-" .. warp_id][warp_icon_editing.name].elem_value --[[@as SignalID]] - if warp_icon.type == nil then warp_icon.type = "item" end - Warps.set_editing(warp_id, player.name) - Warps.update_warp(warp_id, warp_name, warp_icon, player.name) - end) - ---- Confirms the edit to name or icon of the warp --- @element confirm_edit_button -local confirm_edit_button = Gui.define("confirm_edit_button") - :draw{ - type = "sprite-button", - sprite = "utility/confirm_slot", - tooltip = { "warp-list.confirm-tooltip" }, - style = "shortcut_bar_button_green", - name = Gui.from_name, - } - :style(Styles.sprite22) - :on_click(function(def, player, element) - local parent = assert(element.parent) - local grandparent = assert(parent.parent) - local warp_id = parent.caption --[[@as string]] - local name_flow = grandparent["name-" .. warp_id] - local icon_flow = grandparent["icon-" .. warp_id] - local warp_name = name_flow[warp_textfield.name].text - local warp_icon = icon_flow[warp_icon_editing.name].elem_value --[[@as SignalID]] - if warp_icon.type == nil then warp_icon.type = "item" end - Warps.set_editing(warp_id, player.name) - Warps.update_warp(warp_id, warp_name, warp_icon, player.name) - end) - ---- Cancels the editing changes of the selected warp name or icon --- @element cancel_edit_button -local cancel_edit_button = Gui.define("cancel_edit_button") - :draw{ - type = "sprite-button", - sprite = "utility/close_black", - tooltip = { "warp-list.cancel-tooltip" }, - style = "shortcut_bar_button_red", - name = Gui.from_name, - } - :style(Styles.sprite22) - :on_click(function(def, player, element) - local warp_id = assert(element.parent).caption - -- Check if this is the first edit, if so remove the warp. - local warp = Warps.get_warp(warp_id) - if warp.updates == 1 then - Warps.remove_warp(warp_id) - return - end - Warps.set_editing(warp_id, player.name) - end) - ---- Removes a warp from the list, including the physical area and map tag --- @element remove_warp_button -local remove_warp_button = Gui.define("remove_warp_button") - :draw{ - type = "sprite-button", - sprite = "utility/trash", - tooltip = { "warp-list.remove-tooltip" }, - style = "shortcut_bar_button_red", - name = Gui.from_name, - } - :style(Styles.sprite22) - :on_click(function(def, player, element) - local warp_id = assert(element.parent).caption - Warps.remove_warp(warp_id) - end) - ---- Opens edit mode for the warp --- @element edit_warp_button -local edit_warp_button = Gui.define("edit_warp_button") - :draw{ - type = "sprite-button", - sprite = "utility/rename_icon", - tooltip = { "warp-list.edit-tooltip-none" }, - style = "shortcut_bar_button", - name = Gui.from_name, - } - :style(Styles.sprite22) - :on_click(function(def, player, element) - local warp_id = assert(element.parent).caption - Warps.set_editing(warp_id, player.name, true) - end) - -local update_all_warp_elements ---- Set of three elements which make up each row of the warp table --- @element add_warp_elements -local add_warp_elements = Gui.define("add_warp_elements") - :draw(function(_, parent, warp) - -- Add icon flow, this will contain the warp button and warp icon edit button - local icon_flow = parent.add{ - name = "icon-" .. warp.warp_id, - type = "flow", - caption = warp.warp_id, - } - icon_flow.style.padding = 0 - - -- Add the button and the icon edit button - warp_icon_button(icon_flow, warp) - warp_icon_editing(icon_flow, warp) - - -- Add name flow, this will contain the warp label and textbox - local name_flow = parent.add{ - type = "flow", - name = "name-" .. warp.warp_id, - caption = warp.warp_id, - } - name_flow.style.padding = 0 - - -- Add the label and textfield of the warp - warp_status(name_flow) - warp_label(name_flow, warp) - warp_textfield(name_flow, warp) - - -- Add button flow, this will contain buttons to manage this specific warp - local button_flow = parent.add{ - type = "flow", - name = "button-" .. warp.warp_id, - caption = warp.warp_id, - } - button_flow.style.padding = 0 - - -- Add both edit state buttons - confirm_edit_button(button_flow) - cancel_edit_button(button_flow) - edit_warp_button(button_flow) - remove_warp_button(button_flow) - - return Gui.no_return() - end) - --- Removes the three elements that are added as part of the warp base -local function remove_warp_elements(parent, warp_id) - Gui.destroy_if_valid(parent["icon-" .. warp_id]) - Gui.destroy_if_valid(parent["name-" .. warp_id]) - Gui.destroy_if_valid(parent["button-" .. warp_id]) -end - ---- This timer controls when a player is able to warp, eg every 60 seconds --- @element warp_timer -local warp_timer = Gui.define("warp_timer") - :draw{ - type = "progressbar", - name = Gui.from_name, - tooltip = { "warp-list.timer-tooltip-zero", config.cooldown_duration }, - minimum_value = 0, - maximum_value = config.cooldown_duration * config.update_smoothing, - } - :style{ - horizontally_stretchable = true, - color = Colors.light_blue, - } - -local warp_list_container - --- Helper function to style and enable or disable a button element -local function update_warp_elements(element, warp, warp_player_is_on, on_cooldown, bypass_warp_proximity) - -- Check if button element is valid - if not element or not element.valid then return end - - local label_style = assert(element.parent).parent["name-" .. warp.warp_id][warp_label.name].style - local warp_status_element = assert(element.parent).parent["name-" .. warp.warp_id][warp_status.name] - - -- If player is not on a warp - if not warp_player_is_on then - -- If player is allowed to warp without being on a warp. If not then disable the warp location - if bypass_warp_proximity then - local position = warp.position - element.tooltip = { "warp-list.goto-bypass", position.x, position.y } - element.enabled = true - warp_status_element.tooltip = { "warp-list.goto-bypass", position.x, position.y } - warp_status_element.caption = warp_status_icons.bypass - label_style.font = "default-semibold" - else - element.tooltip = { "warp-list.goto-disabled" } - element.enabled = false - warp_status_element.tooltip = { "warp-list.goto-disabled" } - warp_status_element.caption = warp_status_icons.not_available - label_style.font = "default" - end - -- If player is on the warp that is being updated - elseif warp_player_is_on.warp_id == warp.warp_id then - element.tooltip = { "warp-list.goto-same-warp" } - element.enabled = false - warp_status_element.tooltip = { "warp-list.goto-same-warp" } - warp_status_element.caption = warp_status_icons.current - label_style.font = "default" - -- If player is on cooldown - elseif on_cooldown then - element.tooltip = { "warp-list.goto-cooldown" } - element.enabled = false - warp_status_element.tooltip = { "warp-list.goto-cooldown" } - warp_status_element.caption = warp_status_icons.cooldown - label_style.font = "default" - else - -- If the warp the player is standing on is the same as the warp that is being updated - local warp_electric_network_id = warp.electric_pole and warp.electric_pole.electric_network_id or -1 - local player_warp_electric_network_id = warp_player_is_on.electric_pole and warp_player_is_on.electric_pole.electric_network_id or -2 - if warp_electric_network_id == player_warp_electric_network_id then - local position = warp.position - element.tooltip = { "warp-list.goto-tooltip", position.x, position.y } - element.enabled = true - warp_status_element.tooltip = { "warp-list.goto-tooltip", position.x, position.y } - warp_status_element.caption = warp_status_icons.connected - label_style.font = "default-semibold" - -- If the warp is not on the same network but the player is allowed to warp without being on a warp - elseif bypass_warp_proximity then - local position = warp.position - element.tooltip = { "warp-list.goto-bypass-different-network", position.x, position.y } - element.enabled = true - warp_status_element.tooltip = { "warp-list.goto-bypass-different-network", position.x, position.y } - warp_status_element.caption = warp_status_icons.bypass - label_style.font = "default-semibold" - -- If the warp is on a different network than the one the player is standing on - else - element.tooltip = { "warp-list.goto-different-network" } - element.enabled = false - warp_status_element.tooltip = { "warp-list.goto-different-network" } - warp_status_element.caption = warp_status_icons.different - label_style.font = "default" - end - end -end - ---- Update the warp buttons for a player -function update_all_warp_elements(player, timer, warp_id) - -- Get the warp table - local container = Gui.get_left_element(warp_list_container, player) - local scroll_table = container.frame.scroll.table - - -- Check if the player is currently on cooldown - timer = timer or PlayerCooldown:get(player) - local on_cooldown = timer > 0 - -- Get the warp the player is on - warp_id = warp_id or PlayerInRange:get(player) - local warp_player_is_on = warp_id and Warps.get_warp(warp_id) or nil - -- Check player permission - local bypass_warp_proximity = check_player_permissions(player, "bypass_warp_proximity") - - -- Change the enabled state of the warp buttons - local warp_ids = Warps.get_force_warp_ids(player.force.name) - for _, next_warp_id in pairs(warp_ids) do - local element = scroll_table["icon-" .. next_warp_id][warp_icon_button.name] - local next_warp = Warps.get_warp(next_warp_id) - update_warp_elements(element, next_warp, warp_player_is_on, on_cooldown, bypass_warp_proximity) - end -end - ---- Updates a warp for a player -local function update_warp(player, warp_table, warp_id) - local warp = Warps.get_warp(warp_id) - - -- If the warp no longer exists then remove the warp elements from the warp table - if not warp then - remove_warp_elements(warp_table, warp_id) - return - end - - -- Create the warp elements if they do not already exist - if not warp_table["icon-" .. warp_id] then - add_warp_elements(warp_table, warp) - end - local icon_flow = warp_table["icon-" .. warp_id] - local name_flow = warp_table["name-" .. warp_id] - local button_flow = warp_table["button-" .. warp_id] - - -- Create local references to the elements for this warp - local warp_icon_element = icon_flow[warp_icon_button.name] - local warp_icon_edit_element = icon_flow[warp_icon_editing.name] - - local label_element = name_flow[warp_label.name] - local textfield_element = name_flow[warp_textfield.name] - - local cancel_edit_element = button_flow[cancel_edit_button.name] - local confirm_edit_element = button_flow[confirm_edit_button.name] - - local edit_warp_element = button_flow[edit_warp_button.name] - local remove_warp_element = button_flow[remove_warp_button.name] - - -- Hide the edit button if the player is not allowed to edit the warp - local player_allowed_edit = check_player_permissions(player, "allow_edit_warp", warp) - local players_editing = table.get_keys(warp.currently_editing) - edit_warp_element.visible = player_allowed_edit - - -- Set the tooltip of the edit button - if #players_editing > 0 then - edit_warp_element.hovered_sprite = "utility/warning_icon" - edit_warp_element.tooltip = { "warp-list.edit-tooltip", table.concat(players_editing, ", ") } - else - edit_warp_element.hovered_sprite = edit_warp_element.sprite - edit_warp_element.tooltip = { "warp-list.edit-tooltip-none" } - end - - -- Set the visibility of the warp elements based on whether the user is editing or not - local player_is_editing = warp.currently_editing[player.name] - if player_is_editing then - -- Set the icon elements visibility - warp_icon_element.visible = false - warp_icon_edit_element.visible = true - -- Set the name elements visibility - label_element.visible = false - textfield_element.visible = true - textfield_element.focus() - warp_table.parent.scroll_to_element(textfield_element, "top-third") - -- Set the edit buttons - cancel_edit_element.visible = true - confirm_edit_element.visible = true - -- Set the warp buttons - edit_warp_element.visible = false - remove_warp_element.visible = false - else - -- Set the icon elements visibility - warp_icon_element.visible = true - -- The SpritePath type is not the same as the SignalID type - local sprite = warp.icon.type .. "/" .. warp.icon.name - if warp.icon.type == "virtual" then - sprite = "virtual-signal/" .. warp.icon.name - end - warp_icon_element.sprite = sprite - -- Set icon edit to the warps icon - warp_icon_edit_element.elem_value = warp.icon - warp_icon_edit_element.visible = false - -- Set the name elements visibility - label_element.visible = true - label_element.caption = warp.name - textfield_element.visible = false - textfield_element.text = warp.name - -- Set the edit buttons - cancel_edit_element.visible = false - confirm_edit_element.visible = false - -- Set the warp buttons - edit_warp_element.visible = true and player_allowed_edit - remove_warp_element.visible = true and player_allowed_edit - end - - local timer = PlayerCooldown:get(player) - local current_warp_id = PlayerInRange:get(player) - local to_warp = current_warp_id and Warps.get_warp(current_warp_id) or nil - local bypass_warp_proximity = check_player_permissions(player, "bypass_warp_proximity") - update_warp_elements(warp_icon_element, warp, to_warp, timer > 0, bypass_warp_proximity) -end - --- Update all the warps for a player -local function update_all_warps(player, warp_table) - local warp_ids = Warps.get_force_warp_ids(player.force.name) - warp_table.clear() - for _, warp_id in ipairs(warp_ids) do - update_warp(player, warp_table, warp_id) - end -end - --- Update all warps for all players on a force -local function update_all_warp_force(force) - local warp_ids = Warps.get_force_warp_ids(force.name) - for _, player in pairs(force.connected_players) do - local container = Gui.get_left_element(warp_list_container, player) - local warp_table = container.frame.scroll.table - - warp_table.clear() -- Needed to re-sort the warps - for _, warp_id in ipairs(warp_ids) do - update_warp(player, warp_table, warp_id) - end - end -end - ---- Main warp list container for the left flow --- @element warp_list_container -warp_list_container = Gui.define("warp_list_container") - :draw(function(def, parent) - local player = Gui.get_player(parent) - -- Check if user has permission to add warps - local allow_add_warp = check_player_permissions(player, "allow_add_warp") - - -- Draw the internal container - local container = Gui.elements.container(parent, allow_add_warp and 268 or 220) - - -- Draw the header - local header = Gui.elements.header(container, { - name = "header", - caption = { "warp-list.main-caption" }, - tooltip = { - "warp-list.sub-tooltip", - config.cooldown_duration, - config.standard_proximity_radius, - { "warp-list.sub-tooltip-current", warp_status_icons.current }, - { "warp-list.sub-tooltip-connected", warp_status_icons.connected }, - { "warp-list.sub-tooltip-different", warp_status_icons.different }, - { "warp-list.sub-tooltip-cooldown", warp_status_icons.cooldown }, - { "warp-list.sub-tooltip-not_available", warp_status_icons.not_available }, - { "warp-list.sub-tooltip-bypass", warp_status_icons.bypass }, - }, - }) - - -- Draw the new warp button - local add_new_warp_element = add_new_warp(header) - add_new_warp_element.visible = allow_add_warp - - -- Draw the scroll table for the warps - local scroll_table = Gui.elements.scroll_table(container, 250, 3, "scroll") - -- Set the scroll panel to always show the scrollbar (not doing this will result in a changing gui size) - assert(scroll_table.parent).vertical_scroll_policy = "always" - - -- Change the style of the scroll table - local scroll_table_style = scroll_table.style - scroll_table_style.top_cell_padding = 3 - scroll_table_style.bottom_cell_padding = 3 - - -- Draw the warp cooldown progress bar - local warp_timer_element = warp_timer(container) - - -- Change the progress of the warp timer - local timer = PlayerCooldown:get(player) - if timer > 0 then - warp_timer_element.tooltip = { "warp-list.timer-tooltip", math.floor(timer / config.update_smoothing) } - warp_timer_element.value = 1 - (timer / config.update_smoothing / config.cooldown_duration) - else - warp_timer_element.tooltip = { "warp-list.timer-tooltip-zero", config.cooldown_duration } - warp_timer_element.value = 1 - end - - -- Add any existing warps - update_all_warps(player, scroll_table) - - -- Return the external container - return container.parent - end) - ---- Add the element to the left flow with a toolbar button -Gui.add_left_element(warp_list_container, false) -Gui.toolbar.create_button{ - name = "warp_list_toggle", - left_element = warp_list_container, - sprite = config.default_icon.type .. "/" .. config.default_icon.name, - tooltip = { "warp-list.main-tooltip" }, - visible = function(player, element) - return Roles.player_has_permission(player, "exp_scenario.gui.warp_list") - end -}:on_click(function(def, player, element) - -- Set gui keep open state for player that clicked the button: true if visible, false if invisible - keep_gui_open[player.name] = Gui.toolbar.get_button_toggled_state(def, player) -end) - ---- When the name of a warp is updated this is triggered -Warps.on_update(function(_, warp, old_warp) - -- Get the force to update, warp is nil when removed - if warp then - update_all_warp_force(game.forces[warp.force_name]) - else - update_all_warp_force(game.forces[old_warp.force_name]) - end -end) - ---- When the player leaves or enters range of a warp this is triggered -PlayerInRange:on_update(function(player_name, warp_id) - local player = game.players[player_name] - - -- Change if the frame is visible based on if the player is in range - if not keep_gui_open[player.name] then - Gui.toolbar.set_left_element_visible_state(warp_list_container, player, warp_id ~= nil) - end - - update_all_warp_elements(player, nil, warp_id) -end) - ---- Update the warp cooldown progress bars to match the current cooldown -PlayerCooldown:on_update(function(player_name, player_cooldown) - -- Get the progress bar element - local player = game.players[player_name] - local container = Gui.get_left_element(warp_list_container, player) - local warp_timer_element = container.frame[warp_timer.name] - - -- Set the progress - if player_cooldown and player_cooldown > 0 then - warp_timer_element.tooltip = { "warp-list.timer-tooltip", math.floor(player_cooldown / config.update_smoothing) } - warp_timer_element.value = 1 - (player_cooldown / config.update_smoothing / config.cooldown_duration) - else - warp_timer_element.tooltip = { "warp-list.timer-tooltip-zero", config.cooldown_duration } - warp_timer_element.value = 1 - end - - -- Trigger update of buttons if cooldown is now 0 - if player_cooldown == 0 then - update_all_warp_elements(player, player_cooldown, nil) - end -end) - ---- Handles updating the timer and checking distance from a warp -local r2 = config.standard_proximity_radius ^ 2 -local rs2 = config.spawn_proximity_radius ^ 2 -local mr2 = config.minimum_distance ^ 2 -Event.on_nth_tick(math.floor(60 / config.update_smoothing), function() - PlayerCooldown:update_all(function(_, player_cooldown) - if player_cooldown > 0 then return player_cooldown - 1 end - end) - - local force_warps = {} - local warps = {} - for _, player in pairs(game.connected_players) do - local was_in_range = PlayerInRange:get(player) - - -- Get the ids of all the warps on the players force - local force_name = player.force.name - local warp_ids = force_warps[force_name] - if not warp_ids then - warp_ids = Warps.get_force_warp_ids(force_name) - force_warps[force_name] = warp_ids - end - - -- Check if the force has any warps - local closest_warp = nil --- @type { warp_id: string, name: string }? - local closest_distance = nil - if #warp_ids > 0 then - local surface = player.surface - local pos = player.physical_position - local px, py = pos.x, pos.y - - -- Loop over each warp - for _, warp_id in ipairs(warp_ids) do - -- Check if warp id is cached - local warp = warps[warp_id] - if not warp then - warp = Warps.get_warp(warp_id) - warps[warp_id] = warp - end - - -- Check if the player is within range - local warp_pos = warp.position - if warp.surface == surface then - local dx, dy = px - warp_pos.x, py - warp_pos.y - local dist = (dx * dx) + (dy * dy) - if closest_distance == nil or dist < closest_distance then - closest_warp = warp - closest_distance = dist - if dist < r2 then break end - end - end - end - - -- Check the dist to the closest warp - local in_range = closest_warp ~= nil and (closest_warp.warp_id == warp_ids.spawn and closest_distance < rs2 or closest_distance < r2) - if was_in_range and not in_range then - PlayerInRange:set(player, nil) - elseif not was_in_range and in_range then - --- @cast closest_warp -nil - PlayerInRange:set(player, closest_warp.warp_id) - end - - -- Change the enabled state of the add warp button - local container = Gui.get_left_element(warp_list_container, player) - local add_warp_element = container.frame.header[add_new_warp.name] - local old_closest_warp_name = add_warp_element.tooltip[2] or closest_warp and closest_warp.name - local was_able_to_make_warp = add_warp_element.enabled - local can_make_warp = closest_distance == nil or closest_distance > mr2 - if can_make_warp and not was_able_to_make_warp then - add_warp_element.enabled = true - add_warp_element.tooltip = { "warp-list.add-tooltip" } - elseif not can_make_warp and was_able_to_make_warp or closest_warp and (old_closest_warp_name ~= closest_warp.name) then - --- @cast closest_warp -nil - add_warp_element.enabled = false - add_warp_element.tooltip = { "warp-list.too-close", closest_warp.name } - end - end - end -end) - ---- When a player is created make sure that there is a spawn warp created -Event.add(defines.events.on_player_created, function(event) - -- If the force has no spawn then make a spawn warp - local player = game.players[event.player_index] - local force = player.force --[[@as LuaForce]] - local spawn_id = Warps.get_spawn_warp_id(force.name) - if not spawn_id then - local spawn_position = force.get_spawn_position(player.surface) - spawn_id = Warps.add_warp(force.name, player.surface, spawn_position, nil, "Spawn") - Warps.set_spawn_warp(spawn_id, force) - Warps.make_warp_tag(spawn_id) - - local entities = player.surface.find_entities_filtered{ type = "electric-pole", position = spawn_position, radius = 20, limit = 1 } - if entities and entities[1] then - local warp = Warps.get_warp(spawn_id) - warp.electric_pole = entities[1] - end - end -end) - ---- Update the warps when the player joins -Event.add(defines.events.on_player_joined_game, function(event) - local player = game.players[event.player_index] - local container = Gui.get_left_element(warp_list_container, player) - local scroll_table = container.frame.scroll.table - update_all_warps(player, scroll_table) -end) - ---- Makes sure the right buttons are present when roles change -local function role_update_event(event) - local player = game.players[event.player_index] - local frame = Gui.get_left_element(warp_list_container, player).frame - - -- Check if user has permission to add warps - local allow_add_warp = check_player_permissions(player, "allow_add_warp") - -- Update container size depending on whether the player is allowed to add warps - assert(frame.parent).style.width = allow_add_warp and 268 or 220 - - -- Update the warps, in case the user can now edit them - local scroll_table = frame.scroll.table - update_all_warps(player, scroll_table) - - -- Update the new warp button in case the user can now add them - local add_new_warp_element = frame.header[add_new_warp.name] - add_new_warp_element.visible = allow_add_warp -end - -Event.add(Roles.events.on_player_roles_changed, role_update_event) - ---- When a chart tag is removed or edited make sure it is not one that belongs to a warp -local function maintain_tag(event) - if not event.player_index then return end - local tag = event.tag - local force_name = event.force.name - local warp_ids = Warps.get_force_warp_ids(force_name) - for _, warp_id in pairs(warp_ids) do - local warp = Warps.get_warp(warp_id) - local warp_tag = warp.tag - if not warp_tag or not warp_tag.valid or warp_tag == tag then - if event.name == defines.events.on_chart_tag_removed then - warp.tag = nil - end - Warps.make_warp_tag(warp_id) - end - end -end - -Event.add(defines.events.on_chart_tag_modified, maintain_tag) -Event.add(defines.events.on_chart_tag_removed, maintain_tag) diff --git a/exp_roles/controller.ts b/exp_roles/controller.ts index 96acc167..6d5b46f5 100644 --- a/exp_roles/controller.ts +++ b/exp_roles/controller.ts @@ -1,12 +1,20 @@ -import { BaseControllerPlugin, InstanceRecord } from "@clusterio/controller"; +import { BaseControllerPlugin, InstanceRecord, type Controller } from "@clusterio/controller"; import * as lib from "@clusterio/lib"; import * as messages from "./messages.js"; import * as path from "node:path"; +const loaded = new WeakMap(); + export class ControllerPlugin extends BaseControllerPlugin { roleMeta!: lib.SubscribableDatastore; + /** The plugin loaded on a controller, for the exp_scenario seed. */ + static get(controller: Controller) { + return loaded.get(controller); + } + async init() { + loaded.set(this.controller, this); const databaseDirectory = this.controller.config.get("controller.database_directory"); this.roleMeta = new lib.SubscribableDatastore( diff --git a/exp_roles/web/components/RoleProperties.tsx b/exp_roles/web/components/RoleProperties.tsx index 53f6ca99..c68d5f49 100644 --- a/exp_roles/web/components/RoleProperties.tsx +++ b/exp_roles/web/components/RoleProperties.tsx @@ -5,7 +5,7 @@ import * as lib from "@clusterio/lib"; import { ControlContext, SectionHeader, useAccount, notifyErrorHandler } from "@clusterio/web_ui"; import { RoleColor, RoleMetaRecord, RoleMetaUpdateRequest } from "../../messages.js"; -import type { WebPlugin } from ".."; +import { useRoles } from ".."; const MS_PER_HOUR = 3600000; @@ -25,13 +25,13 @@ type RoleFormValues = { * The name, description and permissions of a role are owned by clusterio, this * only covers the properties which only mean something in game. */ -export default function RoleProperties(props: { plugin: WebPlugin, role?: lib.Role }) { +export default function RoleProperties(props: { role?: lib.Role }) { const control = useContext(ControlContext); const account = useAccount(); + const [roles] = useRoles(); const [form] = Form.useForm(); const [applying, setApplying] = useState(false); - const [roles] = props.plugin.useRoles(); const record = props.role ? roles.get(props.role.id) : undefined; const meta = record?.meta; const canUpdate = account.hasPermission("core.role.update"); diff --git a/exp_roles/web/index.tsx b/exp_roles/web/index.tsx index 36e9de1d..0753cf9c 100644 --- a/exp_roles/web/index.tsx +++ b/exp_roles/web/index.tsx @@ -1,24 +1,22 @@ import React, { useCallback, useSyncExternalStore } from "react"; -import { BaseWebPlugin } from "@clusterio/web_ui"; +import { WebPluginContext } from "@clusterio/web_ui"; import * as lib from "@clusterio/lib"; import * as messages from "../messages.js"; import RoleProperties from "./components/RoleProperties"; -export class WebPlugin extends BaseWebPlugin { - roles = new lib.MapSubscriber(messages.RoleUpdatedEvent, this.control); +let roles: lib.MapSubscriber | undefined; - async init() { - // The core components pass a role and the plugin, which componentExtra - // does not carry in its type - this.componentExtra = { - RoleViewPage: RoleProperties as React.ComponentType, - }; - } +export function useRoles() { + const subscribe = useCallback((cb: () => void) => roles!.subscribe(cb), []); + return useSyncExternalStore(subscribe, () => roles!.getSnapshot()); +} - useRoles() { - const subscribe = useCallback((cb: () => void) => this.roles.subscribe(cb), []); - return useSyncExternalStore(subscribe, () => this.roles.getSnapshot()); - } +export default function registerPlugin(context: WebPluginContext) { + context.control.hooks.extensionComponents.attach("exp_roles", () => ({ + RoleViewPage: RoleProperties, + })); + + roles = new lib.MapSubscriber(messages.RoleUpdatedEvent, context.control); } diff --git a/exp_scenario/controller.ts b/exp_scenario/controller.ts index 05d3303e..cd19bd96 100644 --- a/exp_scenario/controller.ts +++ b/exp_scenario/controller.ts @@ -1,9 +1,9 @@ import * as lib from "@clusterio/lib"; import { BaseControllerPlugin } from "@clusterio/controller"; -import { RoleMetaRecord, type ControllerPlugin as RolesPlugin } from "@expcluster/roles"; -import { - GroupRecord, GroupPermissions, RoleMappingRecord, type ControllerPlugin as GroupsPlugin, -} from "@expcluster/permission-groups"; +import { RoleMetaRecord } from "@expcluster/roles"; +import { GroupRecord, GroupPermissions, RoleMappingRecord } from "@expcluster/permission-groups"; +import { ControllerPlugin as RolesPlugin } from "@expcluster/roles/dist/node/controller.js"; +import { ControllerPlugin as GroupsPlugin } from "@expcluster/permission-groups/dist/node/controller.js"; import * as messages from "./messages.js"; import { SeedRole, SeedGroup, seedRoles, seedGroups, flattenSeedPermissions } from "./seed.js"; @@ -19,8 +19,8 @@ export class ControllerPlugin extends BaseControllerPlugin { * permissions, groups which already exist by name are reset to the seed. */ async handleSeedRequest() { - const rolesPlugin = this.controller.plugins.get("exp_roles") as RolesPlugin | undefined; - const groupsPlugin = this.controller.plugins.get("exp_groups") as GroupsPlugin | undefined; + const rolesPlugin = RolesPlugin.get(this.controller); + const groupsPlugin = GroupsPlugin.get(this.controller); if (!rolesPlugin || !groupsPlugin) { throw new lib.RequestError("Seeding requires the exp_roles and exp_groups plugins"); } diff --git a/exp_scenario/module/commands/vlayer.lua b/exp_scenario/module/commands/vlayer.lua index f4b38e96..8ba856d9 100644 --- a/exp_scenario/module/commands/vlayer.lua +++ b/exp_scenario/module/commands/vlayer.lua @@ -3,7 +3,7 @@ Adds a virtual layer to store power to save space. ]] local Commands = require("modules/exp_commands") -local vlayer = require("modules.exp_legacy.modules.control.vlayer") +local Vlayer = require("modules/exp_scenario/control/vlayer") --- @class ExpCommand_vlayer.commands local commands = {} @@ -14,7 +14,7 @@ commands.vlayer = Commands.new("vlayer-info", { "exp-commands_vlayer.description :register(function(player) local index = 3 local response = { "", "exp-commands_vlayer.title" } --- @type LocalisedString - for title, value in pairs(vlayer.get_circuits()) do + for title, value in pairs(Vlayer.get_circuits()) do response[index] = { "exp-commands_vlayer.result", title, value } index = index + 1 end diff --git a/exp_scenario/module/control.lua b/exp_scenario/module/control.lua index 1cd47d66..3d9dbdd4 100644 --- a/exp_scenario/module/control.lua +++ b/exp_scenario/module/control.lua @@ -67,6 +67,7 @@ add(require("modules/exp_scenario/control/roles")) add(require("modules/exp_scenario/control/spawn_area")) add(require("modules/exp_scenario/control/spectate")) add(require("modules/exp_scenario/control/station_auto_name")) +add(require("modules/exp_scenario/control/vlayer")) --- Guis add(require("modules/exp_scenario/gui/autofill")) @@ -83,5 +84,7 @@ add(require("modules/exp_scenario/gui/rocket_info")) add(require("modules/exp_scenario/gui/science_production")) add(require("modules/exp_scenario/gui/surveillance")) add(require("modules/exp_scenario/gui/task_list")) +add(require("modules/exp_scenario/gui/vlayer")) +add(require("modules/exp_scenario/gui/warp_list")) add(require("modules/exp_scenario/gui/debug/shim")) add(require("modules/exp_scenario/gui/debug/event_view")) diff --git a/exp_scenario/module/control/roles.lua b/exp_scenario/module/control/roles.lua index 1c4ecf66..0ffdc3a0 100644 --- a/exp_scenario/module/control/roles.lua +++ b/exp_scenario/module/control/roles.lua @@ -9,9 +9,6 @@ Roles.define_permission_trigger("exp_scenario.player.admin", function(player, st player.admin = state end) -Roles.define_permission_trigger("exp_scenario.player.spectator", function(player, state) - player.spectator = state -end) return { events = { diff --git a/exp_scenario/module/control/vlayer.lua b/exp_scenario/module/control/vlayer.lua new file mode 100644 index 00000000..7d063557 --- /dev/null +++ b/exp_scenario/module/control/vlayer.lua @@ -0,0 +1,673 @@ +--[[-- Control - Virtual Layer +Stores solar panels, accumulators and landfill in a virtual layer which produces power through interface entities +]] + +local ExpUtil = require("modules/exp_util") +local Storage = require("modules/exp_util/storage") +local config = require("modules.exp_legacy.config.vlayer") + +local floor = math.floor +local min = math.min +local max = math.max +local mega = 1000000 + +--- @alias ExpScenario_Vlayer.InterfaceType "energy" | "circuit" | "storage_input" | "storage_output" + +--- @class ExpScenario_Vlayer.Interfaces +--- @field energy LuaEntity[] +--- @field circuit LuaEntity[] +--- @field storage_input LuaEntity[] +--- @field storage_output LuaEntity[] + +--- @class ExpScenario_Vlayer.Properties +--- @field total_surface_area number Tiles provided by landfill +--- @field used_surface_area number Tiles taken by allocated items +--- @field total_production number MW of every solar panel stored, allocated or not +--- @field production number MW of the allocated solar panels +--- @field discharge number MW the allocated accumulators can discharge +--- @field capacity number MJ the allocated accumulators can hold + +--- @class ExpScenario_Vlayer.PowerItem +--- @field value number J released by burning one +--- @field count number + +--- @class ExpScenario_Vlayer.Storage +--- @field items table Every stored item, allocated or not +--- @field unallocated table Items waiting for surface area +--- @field power_items table Fuel burned when the stored energy is low +--- @field energy number J + +--- @class ExpScenario_Vlayer.Statistics +--- @field total_surface_area number +--- @field used_surface_area number +--- @field remaining_surface_area number Surface area left after the unallocated items are counted +--- @field surface_area number +--- @field production_multiplier number +--- @field energy_max number W when the sun is up +--- @field energy_production number W right now +--- @field energy_total_production number W sustained if every stored solar panel was allocated +--- @field energy_sustained number W averaged over a day +--- @field energy_capacity number J +--- @field energy_storage number J + +--- @class ExpScenario_Vlayer.Data +--- @field interfaces ExpScenario_Vlayer.Interfaces +--- @field properties ExpScenario_Vlayer.Properties +--- @field storage ExpScenario_Vlayer.Storage +--- @field surface LuaSurface | table The surface the day cycle is taken from, or a copy of the config settings + +--- @class ExpScenario_Vlayer +local Vlayer = { + --- @package + events = {}, + --- @package + on_nth_tick = {}, +} + +--- The properties of every allowed item, modded items are derived from their base game equivalent +local allowed_items = {} --- @type table +for name, properties in pairs(config.allowed_items) do + allowed_items[name] = properties +end +for name, properties in pairs(config.modded_items) do + local base = config.allowed_items[properties.base_game_equivalent] + local multiplier = properties.multiplier + allowed_items[name] = { + starting_value = properties.starting_value or 0, + required_area = base.required_area or 0, + surface_area = (base.surface_area or 0) * multiplier, + production = (base.production or 0) * multiplier, + capacity = (base.capacity or 0) * multiplier, + } +end + +--- @type ExpScenario_Vlayer.Data +local vlayer_data = { + interfaces = { + energy = {}, + circuit = {}, + storage_input = {}, + storage_output = {}, + }, + properties = { + total_surface_area = 0, + used_surface_area = 0, + total_production = 0, + production = 0, + discharge = 0, + capacity = 0, + }, + storage = { + items = {}, + unallocated = {}, + power_items = {}, + energy = 0, + }, + surface = table.deep_copy(config.surface), +} + +Storage.register(vlayer_data, function(tbl) + vlayer_data = tbl +end) + +--- Interface entities are neutral so players can not mine them, these are the prototypes used for each type +local interface_names = { + energy = "electric-energy-interface", + circuit = "constant-combinator", + storage_input = "storage-chest", + storage_output = "requester-chest", +} --- @type table + +local interface_types = {} --- @type table +for interface_type, name in pairs(interface_names) do + interface_types[name] = interface_type +end + +--- Statistics exposed on circuit interfaces and the signal each is sent as +local circuit_signals = { + total_surface_area = "signal-A", + used_surface_area = "signal-U", + remaining_surface_area = "signal-R", + production_multiplier = "signal-M", + energy_production = "signal-P", + energy_sustained = "signal-S", + energy_capacity = "signal-C", + energy_storage = "signal-E", +} --- @type table + +local circuit_description = {} +for stat_name, signal_name in pairs(circuit_signals) do + circuit_description[#circuit_description + 1] = string.format("[virtual-signal=%s] = %s", signal_name, stat_name:gsub("_", " ")) +end +circuit_description = table.concat(circuit_description, "\n") + +--- Get every stored item keyed by name, allocated or not +--- @return table +function Vlayer.get_items() + return vlayer_data.storage.items +end + +--- Get the items waiting for surface area keyed by name +--- @return table +function Vlayer.get_unallocated_items() + return vlayer_data.storage.unallocated +end + +--- Get the items which contribute to the properties keyed by name +--- @return table +function Vlayer.get_allocated_items() + local allocated = {} + local unallocated = vlayer_data.storage.unallocated + for name, count in pairs(vlayer_data.storage.items) do + allocated[name] = count - (unallocated[name] or 0) + end + return allocated +end + +--- Get the interface entities of each type, do not modify +--- @return ExpScenario_Vlayer.Interfaces +function Vlayer.get_interfaces() + return vlayer_data.interfaces +end + +--- Get the signal each statistic is sent as on circuit interfaces +--- @return table +function Vlayer.get_circuits() + return circuit_signals +end + +--- Surface area left once the unallocated items have taken what they need +--- @return number +local function get_remaining_surface_area() + local properties = vlayer_data.properties + local remaining = properties.total_surface_area - properties.used_surface_area + for name, count in pairs(vlayer_data.storage.unallocated) do + remaining = remaining - allowed_items[name].required_area * count + end + return remaining +end + +--- Fraction of solar production available right now, from the darkness of a real surface or the config day cycle +--- @return number +local function get_production_multiplier() + local surface = vlayer_data.surface + local multiplier = surface.solar_power_multiplier + if surface.always_day then + return multiplier + end + + if surface.darkness then + local brightness = 1 - surface.darkness + if brightness < surface.min_brightness then + return 0 + end + return multiplier * (brightness - surface.min_brightness) / (1 - surface.min_brightness) + end + + local daytime = (game.tick % surface.ticks_per_day) / surface.ticks_per_day + surface.daytime = daytime + if daytime <= surface.dusk then + return multiplier + elseif daytime <= surface.evening then + return multiplier * (1 - (daytime - surface.dusk) / (surface.evening - surface.dusk)) + elseif daytime <= surface.morning then + return 0 + elseif daytime <= surface.dawn then + return multiplier * (daytime - surface.morning) / (surface.dawn - surface.morning) + end + return multiplier +end + +--- Fraction of solar production averaged over a whole day, sunrise and sunset count for half +--- @return number +local function get_sustained_multiplier() + local surface = vlayer_data.surface + local multiplier = surface.solar_power_multiplier + if surface.always_day then + return multiplier + end + + local day_duration = 1 - surface.dawn + surface.dusk + local sunset_duration = surface.evening - surface.dusk + local sunrise_duration = surface.dawn - surface.morning + return multiplier * (day_duration + 0.5 * (sunset_duration + sunrise_duration)) +end + +--- Apply the properties of an item to the layer, a negative count deallocates +--- @param name string +--- @param count number +local function allocate_item(name, count) + local item = allowed_items[name] + local properties = vlayer_data.properties + if item.production then + properties.production = properties.production + item.production * count + properties.total_production = properties.total_production + item.production * count + end + if item.capacity then + properties.capacity = properties.capacity + item.capacity * count + end + if item.discharge then + properties.discharge = properties.discharge + item.discharge * count + end + if item.surface_area then + properties.total_surface_area = properties.total_surface_area + item.surface_area * count + end + if item.required_area > 0 then + properties.used_surface_area = properties.used_surface_area + item.required_area * count + end +end + +--- Count the production of items which are stored but not allocated, so the total shows what more surface area would give +--- @param name string +--- @param count number +local function count_unallocated_production(name, count) + local item = allowed_items[name] + if item.production then + vlayer_data.properties.total_production = vlayer_data.properties.total_production + item.production * count + end +end + +for name, item in pairs(allowed_items) do + vlayer_data.storage.items[name] = item.starting_value + if item.required_area > 0 then + vlayer_data.storage.unallocated[name] = 0 + end + if item.power then + vlayer_data.storage.power_items[name] = { value = assert(item.fuel_value) * mega, count = 0 } + end + allocate_item(name, item.starting_value) +end + +--- Store an item, it is allocated straight away when there is surface area for it +--- @param name string +--- @param count number +function Vlayer.insert_item(name, count) + local item = assert(allowed_items[name], "Item not allowed in the vlayer: " .. name) + local storage = vlayer_data.storage + storage.items[name] = storage.items[name] + count + + if config.unlimited_surface_area or item.required_area == 0 then + allocate_item(name, count) + return + end + + local properties = vlayer_data.properties + local surplus_area = properties.total_surface_area - properties.used_surface_area + local allocated = min(count, floor(surplus_area / item.required_area)) + if allocated > 0 then + allocate_item(name, allocated) + end + + local unallocated = count - allocated + storage.unallocated[name] = storage.unallocated[name] + unallocated + count_unallocated_production(name, unallocated) +end + +--- Take an item out, unallocated items go first and landfill can only go while nothing is using its area +--- @param name string +--- @param count number +--- @return number # The count actually removed +function Vlayer.remove_item(name, count) + local item = assert(allowed_items[name], "Item not allowed in the vlayer: " .. name) + local storage = vlayer_data.storage + local properties = vlayer_data.properties + local removed = 0 + + if not config.unlimited_surface_area and item.required_area > 0 then + removed = min(count, storage.unallocated[name]) + storage.items[name] = storage.items[name] - removed + storage.unallocated[name] = storage.unallocated[name] - removed + count_unallocated_production(name, -removed) + count = count - removed + if count == 0 then + return removed + end + end + + local deallocate = min(count, storage.items[name]) + if item.surface_area and item.surface_area > 0 then + local surplus_area = properties.total_surface_area - properties.used_surface_area + deallocate = min(deallocate, floor(surplus_area / item.surface_area)) + if deallocate <= 0 then + return removed + end + end + + storage.items[name] = storage.items[name] - deallocate + allocate_item(name, -deallocate) + return removed + deallocate +end + +--- Add or take energy, used by other modules which spend the stored energy +--- @param energy number J, negative to take +--- @return number # J stored afterwards +function Vlayer.energy_changed(energy) + local storage = vlayer_data.storage + storage.energy = storage.energy + energy + return storage.energy +end + +--- Get the statistics of the layer +--- @return ExpScenario_Vlayer.Statistics +function Vlayer.get_statistics() + local properties = vlayer_data.properties + local production = properties.production * mega + local production_multiplier = get_production_multiplier() + local sustained_multiplier = get_sustained_multiplier() + local remaining_surface_area = get_remaining_surface_area() + + return { + total_surface_area = properties.total_surface_area, + used_surface_area = properties.used_surface_area, + remaining_surface_area = remaining_surface_area, + surface_area = properties.total_surface_area - remaining_surface_area, + production_multiplier = production_multiplier, + energy_max = production, + energy_production = production * production_multiplier, + energy_total_production = properties.total_production * sustained_multiplier * mega, + energy_sustained = production * sustained_multiplier, + energy_capacity = properties.capacity * mega, + energy_storage = vlayer_data.storage.energy, + } +end + +--- Make an entity into an interface which players can not mine or damage +--- @param interface LuaEntity +--- @param interface_type ExpScenario_Vlayer.InterfaceType +--- @param player LuaPlayer? +local function register_interface(interface, interface_type, player) + local interfaces = vlayer_data.interfaces[interface_type] + interfaces[#interfaces + 1] = interface + interface.last_user = player + interface.destructible = false + interface.minable_flag = false +end + +--- Create an interface which powers the electric network it is placed in +--- @param surface LuaSurface +--- @param position MapPosition +--- @param player LuaPlayer? +--- @return LuaEntity? # Nil when the interface does not fit +function Vlayer.create_energy_interface(surface, position, player) + if not surface.can_place_entity{ name = interface_names.energy, position = position } then + return nil + end + + local interface = assert(surface.create_entity{ name = interface_names.energy, position = position, force = "neutral" }) + register_interface(interface, "energy", player) + interface.operable = false + interface.electric_buffer_size = 0 + interface.power_production = 0 + interface.power_usage = 0 + interface.energy = 0 + return interface +end + +--- Create an interface which outputs the statistics and stored items as signals +--- @param surface LuaSurface +--- @param position MapPosition +--- @param player LuaPlayer? +--- @return LuaEntity +function Vlayer.create_circuit_interface(surface, position, player) + local interface = assert(surface.create_entity{ name = interface_names.circuit, position = position, force = "neutral" }) + register_interface(interface, "circuit", player) + interface.combinator_description = circuit_description + return interface +end + +--- Create an interface which stores every allowed item put into it +--- @param surface LuaSurface +--- @param position MapPosition +--- @param player LuaPlayer? +--- @return LuaEntity +function Vlayer.create_input_interface(surface, position, player) + local interface = assert(surface.create_entity{ name = interface_names.storage_input, position = position, force = "neutral" }) + register_interface(interface, "storage_input", player) + -- A filter nothing can match stops robots filling it with anything + interface.storage_filter = { name = "deconstruction-planner", quality = "normal" } + return interface +end + +--- Create an interface which takes stored items out to fulfil its requests +--- @param surface LuaSurface +--- @param position MapPosition +--- @param player LuaPlayer? +--- @return LuaEntity +function Vlayer.create_output_interface(surface, position, player) + local interface = assert(surface.create_entity{ name = interface_names.storage_output, position = position, force = "neutral" }) + register_interface(interface, "storage_output", player) + return interface +end + +--- Remove an interface, anything it holds is dropped in a chest next to it and stored energy is kept +--- @param interface LuaEntity +--- @return ExpScenario_Vlayer.InterfaceType +function Vlayer.remove_interface(interface) + local interface_type = assert(interface_types[interface.name], "Entity is not a vlayer interface: " .. interface.name) + if interface_type == "storage_input" or interface_type == "storage_output" then + ExpUtil.transfer_inventory_to_surface{ + inventory = assert(interface.get_inventory(defines.inventory.chest)), + surface = interface.surface, + name = "iron-chest", + allow_creation = true, + } + elseif interface_type == "energy" then + vlayer_data.storage.energy = vlayer_data.storage.energy + interface.energy + end + + table.remove_element(vlayer_data.interfaces[interface_type], interface) + interface.destroy() + return interface_type +end + +--- Forget interfaces which no longer exist +--- @param interfaces LuaEntity[] +local function prune_interfaces(interfaces) + for index = #interfaces, 1, -1 do + if not interfaces[index].valid then + table.remove(interfaces, index) + end + end +end + +--- Store the contents of the input interfaces +local function handle_input_interfaces() + local interfaces = vlayer_data.interfaces.storage_input + prune_interfaces(interfaces) + for _, interface in ipairs(interfaces) do + local inventory = assert(interface.get_inventory(defines.inventory.chest)) + for _, item in pairs(inventory.get_contents()) do + local name = item.name + if allowed_items[name] then + -- Quality is not tracked, so higher quality is stored as extra normal items in batches of ten + local quality_level = prototypes.quality[item.quality].level + local removed, stored = 0, 0 + if quality_level == 0 then + removed, stored = item.count, item.count + elseif item.count >= 10 then + local batches = floor(item.count / 10) + removed, stored = batches * 10, batches * (10 + quality_level * 3) + end + + if removed > 0 then + local modded = config.modded_items[name] + if modded and config.modded_auto_downgrade then + Vlayer.insert_item(modded.base_game_equivalent, stored * modded.multiplier) + elseif vlayer_data.storage.power_items[name] then + local power_item = vlayer_data.storage.power_items[name] + power_item.count = power_item.count + stored + else + Vlayer.insert_item(name, stored) + end + inventory.remove{ name = name, count = removed, quality = item.quality } + end + end + end + end +end + +--- Fill the requests of the output interfaces from storage +local function handle_output_interfaces() + local interfaces = vlayer_data.interfaces.storage_output + prune_interfaces(interfaces) + for _, interface in ipairs(interfaces) do + local inventory = assert(interface.get_inventory(defines.inventory.chest)) + for _, section in ipairs(assert(interface.get_logistic_sections()).sections) do + for _, filter in pairs(section.filters) do + local name = filter.value and filter.value.name + if name and allowed_items[name] then + local wanted = min(assert(filter.min) - inventory.get_item_count(name), vlayer_data.storage.items[name]) + if wanted > 0 and inventory.can_insert{ name = name, count = wanted } then + local removed = Vlayer.remove_item(name, wanted) + if removed > 0 then + inventory.insert{ name = name, count = removed, quality = "normal" } + end + end + end + end + end + end +end + +--- Allocate waiting items in proportion to the surface area that has become available +local function handle_unallocated() + if config.unlimited_surface_area then return end + + local unallocated_area = 0 + for name, count in pairs(vlayer_data.storage.unallocated) do + unallocated_area = unallocated_area + allowed_items[name].required_area * count + end + if unallocated_area == 0 then return end + + local properties = vlayer_data.properties + local surplus_area = properties.total_surface_area - properties.used_surface_area + for name, count in pairs(vlayer_data.storage.unallocated) do + local allocated = min(count, floor(count * surplus_area / unallocated_area)) + if allocated > 0 then + vlayer_data.storage.unallocated[name] = count - allocated + allocate_item(name, allocated) + count_unallocated_production(name, -allocated) + end + end +end + +--- Set the signals of the circuit interfaces to the statistics and stored items +local function handle_circuit_interfaces() + local interfaces = vlayer_data.interfaces.circuit + prune_interfaces(interfaces) + if #interfaces == 0 then return end + + local stats = Vlayer.get_statistics() + for _, interface in ipairs(interfaces) do + local control = interface.get_or_create_control_behavior() --[[@as LuaConstantCombinatorControlBehavior]] + if control.sections_count == 0 then + control.add_section() + end + local section = assert(control.sections[1]) + + -- Slots are cleared first because an item can not be set in two slots at once + for index = section.filters_count, 1, -1 do + section.clear_slot(index) + end + + local index = 1 + for stat_name, signal_name in pairs(circuit_signals) do + local value = stats[stat_name] --[[@as number]] + if stat_name:find("energy") then + value = value / mega + elseif stat_name == "production_multiplier" then + value = value * 10000 + end + section.set_slot(index, { value = { type = "virtual", name = signal_name, quality = "normal" }, min = floor(value) }) + index = index + 1 + end + + for name, count in pairs(vlayer_data.storage.items) do + if count > 0 and prototypes.item[name] then + section.set_slot(index, { value = { type = "item", name = name, quality = "normal" }, min = count }) + index = index + 1 + end + end + end +end + +--- Produce energy, share it between the energy interfaces, and burn fuel when storage runs low +local function handle_energy_interfaces() + local storage = vlayer_data.storage + local properties = vlayer_data.properties + local production = properties.production * mega * (config.update_tick_energy / 60) + storage.energy = storage.energy + floor(production * get_production_multiplier()) + + local interfaces = vlayer_data.interfaces.energy + prune_interfaces(interfaces) + if #interfaces > 0 then + local available_energy = storage.energy + for _, interface in ipairs(interfaces) do + available_energy = available_energy + interface.energy + end + + local discharge_rate = 2 * (production + properties.discharge * mega) / #interfaces + local fill_to = min(discharge_rate, floor(available_energy / #interfaces)) + for _, interface in ipairs(interfaces) do + interface.electric_buffer_size = max(discharge_rate, interface.energy) -- A smaller buffer would lose energy + local delta = fill_to - interface.energy + storage.energy = storage.energy - delta + interface.energy = interface.energy + delta + end + end + + local capacity = properties.capacity * mega + if not config.unlimited_capacity and storage.energy > capacity then + storage.energy = capacity + return + end + + for _, power_item in pairs(storage.power_items) do + local max_burn = capacity / 2 - storage.energy + if power_item.count > 0 and max_burn > 0 then + local burn = min(power_item.count, floor(max_burn / power_item.value)) + storage.energy = storage.energy + burn * power_item.value + power_item.count = power_item.count - burn + end + end +end + +--- Take the day cycle from the mimic surface when it exists, otherwise from the config +local function update_surface() + if config.mimic_surface then + local surface = game.get_surface(config.mimic_surface) + if surface then + vlayer_data.surface = surface + return + end + end + + -- The config copy never has an index, so this avoids replacing it every time a surface changes + if not vlayer_data.surface.index then + vlayer_data.surface = table.deep_copy(config.surface) + end +end + +--- Move items between the interfaces and storage +local function update_storage() + handle_input_interfaces() + handle_output_interfaces() + handle_unallocated() +end + +--- Update the signals and energy of the interfaces +local function update_energy() + handle_circuit_interfaces() + handle_energy_interfaces() +end + +local e = defines.events + +Vlayer.on_init = update_surface -- The default surface exists before on_surface_created can fire +Vlayer.events[e.on_surface_created] = update_surface +Vlayer.events[e.on_surface_renamed] = update_surface +Vlayer.events[e.on_surface_imported] = update_surface +Vlayer.on_nth_tick[config.update_tick_storage] = update_storage +Vlayer.on_nth_tick[config.update_tick_energy] = update_energy + +return Vlayer diff --git a/exp_scenario/module/gui/player_bonus.lua b/exp_scenario/module/gui/player_bonus.lua index 13317530..1fadeccc 100644 --- a/exp_scenario/module/gui/player_bonus.lua +++ b/exp_scenario/module/gui/player_bonus.lua @@ -5,7 +5,7 @@ Adds a gui that allows players to apply various bonuses local Gui = require("modules/exp_gui") local Roles = require("modules/exp_roles") local config = require("modules/exp_legacy/config/bonus") -local vlayer = require("modules/exp_legacy/modules/control/vlayer") +local Vlayer = require("modules/exp_scenario/control/vlayer") local format_number = require("util").format_number --- @class ExpGui_PlayerBonus.elements @@ -486,7 +486,7 @@ end --- Apply periodic bonus to a player --- @param player LuaPlayer local function apply_personal_battery_recharge(player) - local available_energy = vlayer.get_statistics()["energy_storage"] + local available_energy = Vlayer.get_statistics().energy_storage if available_energy <= 0 then return -- No power to give end @@ -508,7 +508,7 @@ local function apply_personal_battery_recharge(player) local energy_to_give = math.min(math.floor(equipment.max_energy - equipment.energy), available_energy, recharge_amount) equipment.energy = equipment.energy + energy_to_give recharge_amount = recharge_amount - energy_to_give - available_energy = vlayer.energy_changed(-energy_to_give) + available_energy = Vlayer.energy_changed(-energy_to_give) end end end diff --git a/exp_scenario/module/gui/vlayer.lua b/exp_scenario/module/gui/vlayer.lua new file mode 100644 index 00000000..b5bea951 --- /dev/null +++ b/exp_scenario/module/gui/vlayer.lua @@ -0,0 +1,531 @@ +--[[-- Gui - Virtual Layer +Shows what the virtual layer holds and lets players build and remove its interfaces +]] + +local Gui = require("modules/exp_gui") +local Roles = require("modules/exp_roles") +local Colors = require("modules/exp_util/include/color") +local config = require("modules.exp_legacy.config.vlayer") +local Vlayer = require("modules/exp_scenario/control/vlayer") +local format_number = require("util").format_number + +local Selection = require("modules/exp_util/selection") +local SelectArea = Selection.connect("ExpGui_Vlayer") + +local floor = math.floor +local ceil = math.ceil +local max = math.max +local format_string = string.format + +--- @class ExpGui_Vlayer.elements +local Elements = {} + +--- Interface types in the order the type dropdown lists them +local interface_types = { "energy", "circuit", "storage_input", "storage_output" } --- @type ExpScenario_Vlayer.InterfaceType[] + +--- Locale key of each interface type +local type_captions = {} --- @type table +for _, interface_type in ipairs(interface_types) do + type_captions[interface_type] = { "exp-gui_vlayer.type-" .. interface_type:gsub("_", "-") } +end + +--- Statistics shown as progress bars, in display order +--- @type { name: string, caption: LocalisedString, tooltip: LocalisedString }[] +local stats = {} +for _, name in ipairs{ "solar", "accumulator", "surface-area", "sustained", "production", "capacity" } do + stats[#stats + 1] = { + name = name, + caption = { "exp-gui_vlayer.caption-stat-" .. name }, + tooltip = { "exp-gui_vlayer.tooltip-stat-" .. name }, + } +end + +local energy_suffixes = { + { 1e15, "P" }, { 1e12, "T" }, { 1e9, "G" }, { 1e6, "M" }, { 1e3, "k" }, +} + +--- Format an amount of energy with an SI prefix, util.format_number uses B for giga +--- @param amount number +--- @param unit string +--- @return string +local function format_energy(amount, unit) + if amount < 1 then + return "0 " .. unit + end + for _, suffix in ipairs(energy_suffixes) do + if amount >= suffix[1] then + return format_string("%.1f %s%s", amount / suffix[1], suffix[2], unit) + end + end + return format_number(floor(amount), false) .. " " .. unit +end + +--- Expand an area to whole tiles +--- @param area BoundingBox +--- @return BoundingBox +local function expand_area(area) + return { + left_top = { x = floor(area.left_top.x), y = floor(area.left_top.y) }, + right_bottom = { x = ceil(area.right_bottom.x), y = ceil(area.right_bottom.y) }, + } +end + +--- Rich text link to a position +--- @param position MapPosition +--- @param surface LuaSurface +--- @return string +local function gps_string(position, surface) + return format_string("[gps=%.1f,%.1f,%s]", position.x, position.y, surface.name) +end + +--- Label naming a statistic +--- @class ExpGui_Vlayer.elements.stat_label: ExpElement +--- @overload fun(parent: LuaGuiElement, caption: LocalisedString, tooltip: LocalisedString): LuaGuiElement +Elements.stat_label = Gui.define("vlayer/stat_label") + :draw{ + type = "label", + caption = Gui.from_argument(1), + tooltip = Gui.from_argument(2), + style = "heading_2_label", + } + :style{ + width = 200, + } --[[@as any]] + +--- Bar showing a statistic as a fraction with the values as its caption +--- @class ExpGui_Vlayer.elements.stat_bar: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.stat_bar = Gui.define("vlayer/stat_bar") + :draw{ + type = "progressbar", + caption = "", + value = 0, + style = "electric_satisfaction_statistics_progressbar", + } + :style{ + width = 200, + font = "heading-2", + } --[[@as any]] + +--- @class ExpGui_Vlayer.elements.stat_bar.display_data +--- @field value number Fraction the bar is filled +--- @field caption string + +--- Refresh a bar +--- @param stat_bar LuaGuiElement +--- @param display_data ExpGui_Vlayer.elements.stat_bar.display_data +function Elements.stat_bar.refresh(stat_bar, display_data) + stat_bar.value = display_data.value + stat_bar.caption = display_data.caption +end + +--- Table of a label and bar for each statistic +--- @class ExpGui_Vlayer.elements.stats_table: ExpElement +--- @field data table> Bars keyed by statistic name +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.stats_table = Gui.define("vlayer/stats_table") + :track_all_elements() + :draw(function(def, parent) + --- @cast def ExpGui_Vlayer.elements.stats_table + local stats_table = Gui.elements.scroll_table(parent, 400, 2, "stats") + local stat_bars = {} + for _, stat in ipairs(stats) do + Elements.stat_label(stats_table, stat.caption, stat.tooltip) + stat_bars[stat.name] = Elements.stat_bar(stats_table) + end + def.data[stats_table] = stat_bars + return stats_table + end) --[[@as any]] + +--- Calculate the display data of every statistic, the same for every player +--- @return table +function Elements.stats_table.calculate_display_data() + local statistics = Vlayer.get_statistics() + local items = Vlayer.get_items() + local allocated = Vlayer.get_allocated_items() + return { + solar = { + value = allocated["solar-panel"] / max(items["solar-panel"], 1), + caption = format_number(allocated["solar-panel"], false) .. " / " .. format_number(items["solar-panel"], false), + }, + accumulator = { + value = allocated["accumulator"] / max(items["accumulator"], 1), + caption = format_number(allocated["accumulator"], false) .. " / " .. format_number(items["accumulator"], false), + }, + ["surface-area"] = { + value = statistics.total_surface_area / max(statistics.surface_area, 1), + caption = format_number(statistics.remaining_surface_area, false), + }, + sustained = { + value = statistics.energy_sustained / max(statistics.energy_total_production, 1), + caption = format_energy(statistics.energy_sustained, "W") .. " / " .. format_energy(statistics.energy_total_production, "W"), + }, + production = { + value = statistics.energy_production / max(statistics.energy_max, 1), + caption = format_energy(statistics.energy_production, "W") .. " / " .. format_energy(statistics.energy_max, "W"), + }, + capacity = { + value = statistics.energy_storage / max(statistics.energy_capacity, 1), + caption = format_energy(statistics.energy_storage, "J") .. " / " .. format_energy(statistics.energy_capacity, "J"), + }, + } +end + +--- Refresh every bar of a table +--- @param stats_table LuaGuiElement +--- @param display_data table +function Elements.stats_table.refresh(stats_table, display_data) + for name, stat_bar in pairs(Elements.stats_table.data[stats_table]) do + Elements.stat_bar.refresh(stat_bar, display_data[name]) + end +end + +--- Refresh the tables of every online player +function Elements.stats_table.refresh_online() + local display_data = Elements.stats_table.calculate_display_data() + for _, stats_table in Elements.stats_table:online_elements() do + Elements.stats_table.refresh(stats_table, display_data) + end +end + +--- @class ExpGui_Vlayer.elements.type_dropdown.element_data +--- @field controls LuaGuiElement + +--- Dropdown which picks the type of interface to build or list +--- @class ExpGui_Vlayer.elements.type_dropdown: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, controls: LuaGuiElement): LuaGuiElement +Elements.type_dropdown = Gui.define("vlayer/type_dropdown") + :draw{ + type = "drop-down", + items = { type_captions.energy, type_captions.circuit, type_captions.storage_input, type_captions.storage_output }, + selected_index = 1, + } + :style{ + width = 200, + } + :element_data{ + controls = Gui.from_argument(1), + } + :on_selection_state_changed(function(def, _, type_dropdown) + --- @cast def ExpGui_Vlayer.elements.type_dropdown + Elements.controls.refresh_interfaces(def.data[type_dropdown].controls) + end) --[[@as any]] + +--- Get the selected interface type +--- @param type_dropdown LuaGuiElement +--- @return ExpScenario_Vlayer.InterfaceType +function Elements.type_dropdown.get_selected(type_dropdown) + return (assert(interface_types[type_dropdown.selected_index])) +end + +--- Dropdown listing the interfaces of the selected type by position +--- @class ExpGui_Vlayer.elements.interface_dropdown: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.interface_dropdown = Gui.define("vlayer/interface_dropdown") + :draw{ + type = "drop-down", + } + :style{ + width = 200, + } --[[@as any]] + +--- Refresh the listed interfaces +--- @param interface_dropdown LuaGuiElement +--- @param interfaces LuaEntity[] +function Elements.interface_dropdown.refresh(interface_dropdown, interfaces) + local items = {} + for index, interface in ipairs(interfaces) do + local position = interface.position + items[index] = format_string("%d X %.1f Y %.1f", index, position.x, position.y) + end + interface_dropdown.items = items +end + +--- Get the selected interface +--- @param interface_dropdown LuaGuiElement +--- @param interfaces LuaEntity[] +--- @return LuaEntity? +function Elements.interface_dropdown.get_selected(interface_dropdown, interfaces) + local interface = interfaces[interface_dropdown.selected_index] + if interface and interface.valid then + return interface + end + return nil +end + +--- @class ExpGui_Vlayer.elements.control_button.element_data +--- @field controls LuaGuiElement + +--- Button which refreshes the listed interfaces +--- @class ExpGui_Vlayer.elements.refresh_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, controls: LuaGuiElement): LuaGuiElement +Elements.refresh_button = Gui.define("vlayer/refresh_button") + :draw{ + type = "button", + caption = { "exp-gui_vlayer.caption-refresh" }, + } + :style{ + width = 200, + } + :element_data{ + controls = Gui.from_argument(1), + } + :on_click(function(def, _, refresh_button) + --- @cast def ExpGui_Vlayer.elements.refresh_button + Elements.controls.refresh_interfaces(def.data[refresh_button].controls) + end) --[[@as any]] + +--- Button which opens the map at the selected interface +--- @class ExpGui_Vlayer.elements.view_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, controls: LuaGuiElement): LuaGuiElement +Elements.view_button = Gui.define("vlayer/view_button") + :draw{ + type = "button", + caption = { "exp-gui_vlayer.caption-view" }, + } + :style{ + width = 200, + } + :element_data{ + controls = Gui.from_argument(1), + } + :on_click(function(def, player, view_button) + --- @cast def ExpGui_Vlayer.elements.view_button + local controls = def.data[view_button].controls + local interface_type, interface = Elements.controls.get_selected_interface(controls) + if not interface then return end + player.set_controller{ type = defines.controllers.remote, position = interface.position, surface = interface.surface } + player.print{ "exp-gui_vlayer.message-located", type_captions[interface_type], gps_string(interface.position, interface.surface) } + end) --[[@as any]] + +--- Button which toggles selecting an area to build an interface in +--- @class ExpGui_Vlayer.elements.build_button: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.build_button = Gui.define("vlayer/build_button") + :track_all_elements() + :draw{ + type = "button", + caption = { "exp-gui_vlayer.caption-build" }, + } + :style{ + width = 200, + } + :on_click(function(_, player) + if SelectArea:stop(player) then + player.print{ "exp_util.selection_exit", { "exp-gui_vlayer.selection-name" } } + else + SelectArea:start(player) + player.print{ "exp_util.selection_enter", { "exp-gui_vlayer.selection-name" } } + end + end) --[[@as any]] + +--- Button which removes the selected interface +--- @class ExpGui_Vlayer.elements.remove_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, controls: LuaGuiElement): LuaGuiElement +Elements.remove_button = Gui.define("vlayer/remove_button") + :track_all_elements() + :draw{ + type = "button", + caption = { "exp-gui_vlayer.caption-remove" }, + } + :style{ + width = 200, + } + :element_data{ + controls = Gui.from_argument(1), + } + :on_click(function(def, player, remove_button) + --- @cast def ExpGui_Vlayer.elements.remove_button + local controls = def.data[remove_button].controls + local _, interface = Elements.controls.get_selected_interface(controls) + if not interface then return end + local position, surface = interface.position, interface.surface + local interface_type = Vlayer.remove_interface(interface) + game.print{ "exp-gui_vlayer.message-removed", player.name, type_captions[interface_type], gps_string(position, surface) } + Elements.controls.refresh_interfaces_online() + end) --[[@as any]] + +--- @class ExpGui_Vlayer.elements.controls.element_data +--- @field type_dropdown LuaGuiElement +--- @field interface_dropdown LuaGuiElement + +--- Table of the dropdowns and buttons used to manage interfaces +--- @class ExpGui_Vlayer.elements.controls: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.controls = Gui.define("vlayer/controls") + :track_all_elements() + :draw(function(def, parent) + --- @cast def ExpGui_Vlayer.elements.controls + local controls = Gui.elements.scroll_table(parent, 400, 2, "controls") + local player = Gui.get_player(parent) + local can_edit = Roles.player_has_permission(player, "exp_scenario.gui.vlayer_edit") + + local type_dropdown = Elements.type_dropdown(controls, controls) + local interface_dropdown = Elements.interface_dropdown(controls) + Elements.refresh_button(controls, controls) + Elements.view_button(controls, controls) + Elements.build_button(controls).visible = can_edit + Elements.remove_button(controls, controls).visible = can_edit + + def.data[controls] = { + type_dropdown = type_dropdown, + interface_dropdown = interface_dropdown, + } + Elements.controls.refresh_interfaces(controls) + return controls + end) --[[@as any]] + +--- Get the selected interface type +--- @param controls LuaGuiElement +--- @return ExpScenario_Vlayer.InterfaceType +function Elements.controls.get_selected_type(controls) + return Elements.type_dropdown.get_selected(Elements.controls.data[controls].type_dropdown) +end + +--- Get the selected interface and its type +--- @param controls LuaGuiElement +--- @return ExpScenario_Vlayer.InterfaceType, LuaEntity? +function Elements.controls.get_selected_interface(controls) + local elements = Elements.controls.data[controls] + local interface_type = Elements.type_dropdown.get_selected(elements.type_dropdown) + local interfaces = Vlayer.get_interfaces()[interface_type] + return interface_type, Elements.interface_dropdown.get_selected(elements.interface_dropdown, interfaces) +end + +--- Refresh the listed interfaces to match the selected type +--- @param controls LuaGuiElement +function Elements.controls.refresh_interfaces(controls) + local elements = Elements.controls.data[controls] + local interface_type = Elements.type_dropdown.get_selected(elements.type_dropdown) + Elements.interface_dropdown.refresh(elements.interface_dropdown, Vlayer.get_interfaces()[interface_type]) +end + +--- Refresh the listed interfaces of every online player, used when an interface is built or removed +function Elements.controls.refresh_interfaces_online() + for _, controls in Elements.controls:online_elements() do + Elements.controls.refresh_interfaces(controls) + end +end + +--- Refresh whether a player can see the edit buttons +--- @param player LuaPlayer +function Elements.controls.refresh_permissions(player) + local can_edit = Roles.player_has_permission(player, "exp_scenario.gui.vlayer_edit") + for _, build_button in Elements.build_button:tracked_elements(player) do + build_button.visible = can_edit + end + for _, remove_button in Elements.remove_button:tracked_elements(player) do + remove_button.visible = can_edit + end +end + +--- Container added to the left gui flow +--- @class ExpGui_Vlayer.elements.container: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.container = Gui.define("vlayer/container") + :draw(function(_, parent) + local container = Gui.elements.container(parent, 400) + local stats_table = Elements.stats_table(container) + Elements.stats_table.refresh(stats_table, Elements.stats_table.calculate_display_data()) + Elements.controls(container) + return Gui.elements.container.get_root_element(container) + end) --[[@as any]] + +--- Add the element to the left flow with a toolbar button +Gui.add_left_element(Elements.container, false) +Gui.toolbar.create_button{ + name = "toggle_vlayer", + left_element = Elements.container, + sprite = "entity/solar-panel", + tooltip = { "exp-gui_vlayer.tooltip-main" }, + visible = function(player, element) + return Roles.player_has_permission(player, "exp_scenario.gui.vlayer") + end +} + +--- Replace the one marker entity in a selected area with an interface of the type the player picked +--- @param event EventData.on_player_selected_area +local function on_area_selected(event) + local player = Gui.get_player(event) + local surface = event.surface + local area = expand_area(event.area) + local force = player.force --[[@as LuaForce]] + + local interface_type = "energy" --- @type ExpScenario_Vlayer.InterfaceType + for _, controls in Elements.controls:tracked_elements(player) do + interface_type = Elements.controls.get_selected_type(controls) + end + + -- Platforms have no chests, so a combinator marks the spot once the research allows it + local marker_name = "steel-chest" + if config.power_on_space and surface.platform and interface_type == "energy" then + local research = config.power_on_space_research + if force.technologies[research.name].level < research.level then + player.print({ "exp-gui_vlayer.message-research", research.name, research.level }, Colors.orange_red) + return + end + marker_name = "constant-combinator" + end + + local markers = surface.find_entities_filtered{ area = area, name = marker_name, force = force } + if #markers == 0 then + player.print({ "exp-gui_vlayer.message-no-chest" }, Colors.orange_red) + return + elseif #markers > 1 then + player.print({ "exp-gui_vlayer.message-multiple", type_captions[interface_type] }, Colors.orange_red) + return + end + + local marker = assert(markers[1]) + if marker.name == "steel-chest" and not assert(marker.get_inventory(defines.inventory.chest)).is_empty() then + player.print({ "exp-gui_vlayer.message-chest-not-empty" }, Colors.orange_red) + return + end + + if #Vlayer.get_interfaces()[interface_type] >= config.interface_limit[interface_type] then + player.print({ "exp-gui_vlayer.message-limit", type_captions[interface_type] }, Colors.orange_red) + return + end + + local position = marker.position + marker.destroy() + + if interface_type == "energy" then + if not Vlayer.create_energy_interface(surface, position, player) then + player.print({ "exp-gui_vlayer.message-no-space" }, Colors.orange_red) + return + end + elseif interface_type == "circuit" then + Vlayer.create_circuit_interface(surface, position, player) + elseif interface_type == "storage_input" then + Vlayer.create_input_interface(surface, position, player) + else + Vlayer.create_output_interface(surface, position, player) + end + + game.print{ "exp-gui_vlayer.message-built", player.name, type_captions[interface_type], gps_string(position, surface) } + Elements.controls.refresh_interfaces_online() +end + +SelectArea:on_selection(on_area_selected) + +--- Refresh the edit buttons as the permissions of a player may have changed +--- @param event EventData.ExpRoles.on_player_roles_changed +local function refresh_player_permissions(event) + Elements.controls.refresh_permissions(Gui.get_player(event)) +end + +return { + elements = Elements, + events = { + [Roles.events.on_player_roles_changed] = refresh_player_permissions, + }, + on_nth_tick = { + [config.update_tick_gui] = Elements.stats_table.refresh_online, + } +} diff --git a/exp_scenario/module/gui/warp_list.lua b/exp_scenario/module/gui/warp_list.lua new file mode 100644 index 00000000..f76faed4 --- /dev/null +++ b/exp_scenario/module/gui/warp_list.lua @@ -0,0 +1,1218 @@ +--[[-- Gui - Warp List +Adds a list of warp points which players can travel between, add, and edit +]] + +local Gui = require("modules/exp_gui") +local Roles = require("modules/exp_roles") +local Colors = require("modules/exp_util/include/color") +local config = require("modules.exp_legacy.config.gui.warps") + +local ExpUtil = require("modules/exp_util") +local format_time = ExpUtil.format_time_factory_locale{ format = "short", hours = true, minutes = true } + +local floor = math.floor +local ceil = math.ceil + +--- @class ExpGui_WarpList.elements +local Elements = {} + +local Styles = { + sprite22 = { height = 22, width = 22, padding = -2 }, + sprite32 = { height = 32, width = 32, left_margin = 1 }, +} + +--- Rich text icons shown next to a warp name to describe why it can or can not be used +local status_icons = { + cooldown = "[img=utility/multiplayer_waiting_icon]", + not_available = "[img=utility/set_bar_slot]", + bypass = "[img=utility/side_menu_bonus_icon]", + current = "[img=utility/side_menu_map_icon]", + connected = "[img=utility/logistic_network_panel_white]", + different = "[img=utility/warning_white]", +} + +local update_interval = floor(60 / config.update_smoothing) +local cooldown_ticks = config.cooldown_duration * 60 +local proximity_radius_sq = config.standard_proximity_radius ^ 2 +local spawn_radius_sq = config.spawn_proximity_radius ^ 2 +local minimum_distance_sq = config.minimum_distance ^ 2 + +--- Names of the entities which make up a warp area, used to find them when the area is removed +local area_entity_names = {} --- @type string[] +for _, entity in ipairs(config.entities) do + area_entity_names[#area_entity_names + 1] = entity[1] +end + +--- @class ExpGui_WarpList.Warp +--- @field id number +--- @field force LuaForce +--- @field name string +--- @field icon SignalID +--- @field surface LuaSurface +--- @field position MapPosition.struct The tile the warp is centred on +--- @field last_user LuaPlayer? Nil for warps made by the scenario +--- @field last_edit_tick number +--- @field editing table Names of the players editing the warp keyed by their index +--- @field tag LuaCustomChartTag? +--- @field electric_pole LuaEntity? Pole within the warp area, warps on the same network can be travelled between +--- @field old_tile string? The tile the warp area replaced, nil when no area was made +--- @field new boolean? True until the first edit is confirmed, discarding that edit removes the warp + +--- @class ExpGui_WarpList.PlayerStatus +--- @field in_range ExpGui_WarpList.Warp? The warp the player is standing on +--- @field on_cooldown boolean +--- @field bypass_proximity boolean + +--- Check a permission from the config, which says who an action applies to +--- @param player LuaPlayer +--- @param action "bypass_warp_cooldown" | "bypass_warp_proximity" | "allow_add_warp" | "allow_edit_warp" +--- @return boolean +local function has_config_permission(player, action) + local setting = config[action] + if setting == "all" then + return true + elseif setting == "admin" then + return player.admin + elseif setting == "exp_roles" then + return Roles.player_has_permission(player, config["exp_roles_" .. action]) + end + + return false +end + +--- Check if a player can edit a warp, the spawn warp can never be edited +--- @param player LuaPlayer +--- @param warp ExpGui_WarpList.Warp +--- @return boolean +local function has_permission_edit_warp(player, warp) + if Elements.container.get_spawn_warp(warp.force) == warp then + return false + end + + if config.user_can_edit_own_warps and warp.last_user == player then + return true + end + + return has_config_permission(player, "allow_edit_warp") +end + +--- The sprite path for a warp icon, signal ids and sprite paths name virtual signals differently +--- @param icon SignalID +--- @return SpritePath +local function icon_sprite(icon) + if icon.type == "virtual" then + return "virtual-signal/" .. icon.name + end + return (icon.type or "item") .. "/" .. icon.name +end + +--- Draw a cross over something that stops a warp being added +--- @param surface LuaSurface +--- @param player LuaPlayer +--- @param target LuaEntity | MapPosition +local function draw_blocker(surface, player, target) + rendering.draw_sprite{ + sprite = "utility/rail_path_not_possible", + x_scale = 0.5, + y_scale = 0.5, + target = target, + surface = surface, + players = { player }, + time_to_live = 60, + } +end + +--- Button in the header which adds a warp at the player +--- @class ExpGui_WarpList.elements.add_warp_button: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.add_warp_button = Gui.define("warp_list/add_warp_button") + :track_all_elements() + :draw{ + type = "sprite-button", + sprite = "utility/add", + tooltip = { "exp-gui_warp-list.tooltip-add" }, + style = "shortcut_bar_button", + } + :style(Styles.sprite22) + :on_click(function(_, player) + if player.controller_type ~= defines.controllers.character then return end + local surface = player.physical_surface + local position = player.physical_position + + local water_tiles = surface.find_tiles_filtered{ + collision_mask = "water_tile", + radius = config.standard_proximity_radius + 1, + position = position, + } + if #water_tiles > 0 then + player.print({ "exp-gui_warp-list.add-warning-water", config.standard_proximity_radius + 1 }, Colors.orange_red) + player.play_sound{ path = "utility/wire_pickup" } + for _, tile in pairs(water_tiles) do + draw_blocker(surface, player, tile.position) + end + return + end + + -- A larger radius than the area because the entities a player can place are larger than tiles + local entities = surface.find_entities_filtered{ + radius = config.standard_proximity_radius + 2.5, + position = position, + collision_mask = { "item", "object", "player", "water_tile" }, + } + if #entities > 1 then -- The player's own character is always found + player.print({ "exp-gui_warp-list.add-warning-entities", config.standard_proximity_radius + 2.5 }, Colors.orange_red) + player.play_sound{ path = "utility/wire_pickup" } + local character = player.character + for _, entity in pairs(entities) do + if entity ~= character then + draw_blocker(surface, player, entity) + end + end + return + end + + local force = player.force --[[@as LuaForce]] + local warp = Elements.container.add_warp(force, surface, position, player) + Elements.container.set_editing(warp, player, true) + end) --[[@as any]] + +--- Refresh whether the add button is enabled based on the closest warp +--- @param player LuaPlayer +--- @param blocking ExpGui_WarpList.Warp? The warp too close to add another, nil when a warp can be added +function Elements.add_warp_button.refresh_player_blocking(player, blocking) + local tooltip = blocking and { "exp-gui_warp-list.tooltip-add-too-close", blocking.name } or { "exp-gui_warp-list.tooltip-add" } + for _, add_warp_button in Elements.add_warp_button:tracked_elements(player) do + add_warp_button.enabled = blocking == nil + add_warp_button.tooltip = tooltip + end +end + +--- @class ExpGui_WarpList.elements.warp_button.element_data +--- @field warp ExpGui_WarpList.Warp + +--- Button which travels to the warp, enabled when the player is allowed to use it +--- @class ExpGui_WarpList.elements.warp_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement +Elements.warp_button = Gui.define("warp_list/warp_button") + :draw(function(_, parent, warp) + --- @cast warp ExpGui_WarpList.Warp + return parent.add{ + type = "sprite-button", + sprite = icon_sprite(warp.icon), + style = "slot_button", + } + end) + :style(Styles.sprite32) + :element_data{ + warp = Gui.from_argument(1), + } + :on_click(function(def, player, warp_button) + --- @cast def ExpGui_WarpList.elements.warp_button + Elements.container.teleport_player(player, def.data[warp_button].warp) + end) --[[@as any]] + +--- @class ExpGui_WarpList.elements.icon_picker.element_data +--- @field warp ExpGui_WarpList.Warp + +--- Signal picker shown in place of the warp button while editing +--- @class ExpGui_WarpList.elements.icon_picker: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement +Elements.icon_picker = Gui.define("warp_list/icon_picker") + :draw(function(_, parent, warp) + --- @cast warp ExpGui_WarpList.Warp + return parent.add{ + type = "choose-elem-button", + elem_type = "signal", + signal = warp.icon, + tooltip = { "exp-gui_warp-list.tooltip-icon" }, + } + end) + :style(Styles.sprite32) + :element_data{ + warp = Gui.from_argument(1), + } --[[@as any]] + +--- Get the icon chosen in a picker, the game omits the type for items +--- @param icon_picker LuaGuiElement +--- @return SignalID +function Elements.icon_picker.get_icon(icon_picker) + local icon = icon_picker.elem_value --[[@as SignalID?]] + if not icon then + return Elements.icon_picker.data[icon_picker].warp.icon + end + icon.type = icon.type or "item" + return icon +end + +--- Icon describing whether the warp can be used +--- @class ExpGui_WarpList.elements.status_label: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.status_label = Gui.define("warp_list/status_label") + :draw{ + type = "label", + caption = status_icons.not_available, + } + :style{ + top_padding = 1, -- Keeps the icon in place when the taller textfield is shown + single_line = false, + } --[[@as any]] + +--- @class ExpGui_WarpList.elements.name_label.element_data +--- @field warp ExpGui_WarpList.Warp + +--- Warp name, clicking it opens the map at the warp +--- @class ExpGui_WarpList.elements.name_label: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement +Elements.name_label = Gui.define("warp_list/name_label") + :draw(function(_, parent, warp) + --- @cast warp ExpGui_WarpList.Warp + return parent.add{ + type = "label", + caption = warp.name, + } + end) + :style{ + single_line = true, + left_padding = 2, + right_padding = 2, + horizontally_stretchable = true, + } + :element_data{ + warp = Gui.from_argument(1), + } + :on_click(function(def, player, name_label) + --- @cast def ExpGui_WarpList.elements.name_label + local warp = def.data[name_label].warp + player.set_controller{ type = defines.controllers.remote, position = warp.position, surface = warp.surface } + end) --[[@as any]] + +--- Refresh the name and last edit of the label +--- @param name_label LuaGuiElement +--- @param warp ExpGui_WarpList.Warp +function Elements.name_label.refresh(name_label, warp) + local last_user = warp.last_user and warp.last_user.name or "" + name_label.caption = warp.name + name_label.tooltip = { "exp-gui_warp-list.tooltip-last-edit", last_user, format_time(warp.last_edit_tick) } +end + +--- @class ExpGui_WarpList.elements.name_field.element_data +--- @field warp ExpGui_WarpList.Warp +--- @field icon_picker LuaGuiElement + +--- Textfield shown in place of the name label while editing, confirming it saves the edit +--- @class ExpGui_WarpList.elements.name_field: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp, icon_picker: LuaGuiElement): LuaGuiElement +Elements.name_field = Gui.define("warp_list/name_field") + :draw(function(_, parent, warp) + --- @cast warp ExpGui_WarpList.Warp + return parent.add{ + type = "textfield", + text = warp.name, + clear_and_focus_on_right_click = true, + } + end) + :style{ + minimal_width = 10, + maximal_width = 300, + horizontally_squashable = true, + horizontally_stretchable = true, + height = 22, + padding = -2, + left_margin = 2, + right_margin = 2, + } + :element_data{ + warp = Gui.from_argument(1), + icon_picker = Gui.from_argument(2), + } + :on_confirmed(function(def, player, name_field) + --- @cast def ExpGui_WarpList.elements.name_field + local elements = def.data[name_field] + local icon = Elements.icon_picker.get_icon(elements.icon_picker) + Elements.container.update_warp(elements.warp, name_field.text, icon, player) + end) --[[@as any]] + +--- @class ExpGui_WarpList.elements.confirm_edit_button.element_data +--- @field warp ExpGui_WarpList.Warp +--- @field name_field LuaGuiElement +--- @field icon_picker LuaGuiElement + +--- Button which saves the name and icon being edited +--- @class ExpGui_WarpList.elements.confirm_edit_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp, name_field: LuaGuiElement, icon_picker: LuaGuiElement): LuaGuiElement +Elements.confirm_edit_button = Gui.define("warp_list/confirm_edit_button") + :draw{ + type = "sprite-button", + sprite = "utility/confirm_slot", + tooltip = { "exp-gui_warp-list.tooltip-confirm" }, + style = "shortcut_bar_button_green", + } + :style(Styles.sprite22) + :element_data{ + warp = Gui.from_argument(1), + name_field = Gui.from_argument(2), + icon_picker = Gui.from_argument(3), + } + :on_click(function(def, player, confirm_edit_button) + --- @cast def ExpGui_WarpList.elements.confirm_edit_button + local elements = def.data[confirm_edit_button] + local icon = Elements.icon_picker.get_icon(elements.icon_picker) + Elements.container.update_warp(elements.warp, elements.name_field.text, icon, player) + end) --[[@as any]] + +--- @class ExpGui_WarpList.elements.discard_edit_button.element_data +--- @field warp ExpGui_WarpList.Warp + +--- Button which discards an edit, a warp which was never confirmed is removed +--- @class ExpGui_WarpList.elements.discard_edit_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement +Elements.discard_edit_button = Gui.define("warp_list/discard_edit_button") + :draw{ + type = "sprite-button", + sprite = "utility/close_black", + tooltip = { "exp-gui_warp-list.tooltip-discard" }, + style = "shortcut_bar_button_red", + } + :style(Styles.sprite22) + :element_data{ + warp = Gui.from_argument(1), + } + :on_click(function(def, player, discard_edit_button) + --- @cast def ExpGui_WarpList.elements.discard_edit_button + local warp = def.data[discard_edit_button].warp + if warp.new then + Elements.container.remove_warp(warp) + else + Elements.container.set_editing(warp, player, false) + end + end) --[[@as any]] + +--- @class ExpGui_WarpList.elements.edit_warp_button.element_data +--- @field warp ExpGui_WarpList.Warp + +--- Button which starts editing a warp +--- @class ExpGui_WarpList.elements.edit_warp_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement +Elements.edit_warp_button = Gui.define("warp_list/edit_warp_button") + :draw{ + type = "sprite-button", + sprite = "utility/rename_icon", + tooltip = { "exp-gui_warp-list.tooltip-edit-none" }, + style = "shortcut_bar_button", + } + :style(Styles.sprite22) + :element_data{ + warp = Gui.from_argument(1), + } + :on_click(function(def, player, edit_warp_button) + --- @cast def ExpGui_WarpList.elements.edit_warp_button + Elements.container.set_editing(def.data[edit_warp_button].warp, player, true) + end) --[[@as any]] + +--- Refresh the tooltip which lists who is editing the warp +--- @param edit_warp_button LuaGuiElement +--- @param warp ExpGui_WarpList.Warp +function Elements.edit_warp_button.refresh(edit_warp_button, warp) + if next(warp.editing) then + local player_names = table.get_values(warp.editing) + edit_warp_button.hovered_sprite = "utility/warning_icon" + edit_warp_button.tooltip = { "exp-gui_warp-list.tooltip-edit", table.concat(player_names, ", ") } + else + edit_warp_button.hovered_sprite = "utility/rename_icon" + edit_warp_button.tooltip = { "exp-gui_warp-list.tooltip-edit-none" } + end +end + +--- @class ExpGui_WarpList.elements.remove_warp_button.element_data +--- @field warp ExpGui_WarpList.Warp + +--- Button which removes a warp along with its area and map tag +--- @class ExpGui_WarpList.elements.remove_warp_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement +Elements.remove_warp_button = Gui.define("warp_list/remove_warp_button") + :draw{ + type = "sprite-button", + sprite = "utility/trash", + tooltip = { "exp-gui_warp-list.tooltip-remove" }, + style = "shortcut_bar_button_red", + } + :style(Styles.sprite22) + :element_data{ + warp = Gui.from_argument(1), + } + :on_click(function(def, _, remove_warp_button) + --- @cast def ExpGui_WarpList.elements.remove_warp_button + Elements.container.remove_warp(def.data[remove_warp_button].warp) + end) --[[@as any]] + +--- @class ExpGui_WarpList.elements.warp_table.row +--- @field warp ExpGui_WarpList.Warp +--- @field icon_flow LuaGuiElement +--- @field name_flow LuaGuiElement +--- @field action_flow LuaGuiElement +--- @field warp_button LuaGuiElement +--- @field icon_picker LuaGuiElement +--- @field status_label LuaGuiElement +--- @field name_label LuaGuiElement +--- @field name_field LuaGuiElement +--- @field confirm_edit_button LuaGuiElement +--- @field discard_edit_button LuaGuiElement +--- @field edit_warp_button LuaGuiElement +--- @field remove_warp_button LuaGuiElement + +--- @class ExpGui_WarpList.elements.warp_table.element_data +--- @field rows table Keyed by warp id + +--- Table with a row of icon, name, and actions for each warp of the player's force +--- @class ExpGui_WarpList.elements.warp_table: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.warp_table = Gui.define("warp_list/warp_table") + :track_all_elements() + :element_data{ rows = {} } + :draw(function(_, parent) + local warp_table = Gui.elements.scroll_table(parent, 250, 3, "scroll") + -- Always showing the scrollbar stops the gui changing width as warps are added + assert(warp_table.parent).vertical_scroll_policy = "always" + return warp_table + end) + :style{ + top_cell_padding = 3, + bottom_cell_padding = 3, + } --[[@as any]] + +--- Add a row for a warp +--- @param warp_table LuaGuiElement +--- @param warp ExpGui_WarpList.Warp +--- @return ExpGui_WarpList.elements.warp_table.row +function Elements.warp_table.add_row(warp_table, warp) + local icon_flow = warp_table.add{ type = "flow" } + icon_flow.style.padding = 0 + local warp_button = Elements.warp_button(icon_flow, warp) + local icon_picker = Elements.icon_picker(icon_flow, warp) + + local name_flow = warp_table.add{ type = "flow" } + name_flow.style.padding = 0 + local status_label = Elements.status_label(name_flow) + local name_label = Elements.name_label(name_flow, warp) + local name_field = Elements.name_field(name_flow, warp, icon_picker) + + local action_flow = warp_table.add{ type = "flow" } + action_flow.style.padding = 0 + local confirm_edit_button = Elements.confirm_edit_button(action_flow, warp, name_field, icon_picker) + local discard_edit_button = Elements.discard_edit_button(action_flow, warp) + local edit_warp_button = Elements.edit_warp_button(action_flow, warp) + local remove_warp_button = Elements.remove_warp_button(action_flow, warp) + + local row = { + warp = warp, + icon_flow = icon_flow, + name_flow = name_flow, + action_flow = action_flow, + warp_button = warp_button, + icon_picker = icon_picker, + status_label = status_label, + name_label = name_label, + name_field = name_field, + confirm_edit_button = confirm_edit_button, + discard_edit_button = discard_edit_button, + edit_warp_button = edit_warp_button, + remove_warp_button = remove_warp_button, + } + + Elements.warp_table.data[warp_table].rows[warp.id] = row + return row +end + +--- Remove the row for a warp +--- @param warp_table LuaGuiElement +--- @param warp ExpGui_WarpList.Warp +function Elements.warp_table.remove_row(warp_table, warp) + local rows = Elements.warp_table.data[warp_table].rows + local row = rows[warp.id] + if not row then return end + rows[warp.id] = nil + row.icon_flow.destroy() + row.name_flow.destroy() + row.action_flow.destroy() +end + +--- Refresh whether a warp can be used from where the player is +--- @param row ExpGui_WarpList.elements.warp_table.row +--- @param status ExpGui_WarpList.PlayerStatus +function Elements.warp_table._refresh_row_status(row, status) + local warp = row.warp + local in_range = status.in_range + local position = warp.position + local tooltip, icon, enabled + + if not in_range then + if status.bypass_proximity then + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-bypass", position.x, position.y }, status_icons.bypass, true + else + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-disabled" }, status_icons.not_available, false + end + elseif in_range == warp then + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-same-warp" }, status_icons.current, false + elseif status.on_cooldown then + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-cooldown" }, status_icons.cooldown, false + else + -- Warps are connected when their poles share an electric network, missing poles never match + local network_id = warp.electric_pole and warp.electric_pole.valid and warp.electric_pole.electric_network_id or -1 + local in_range_network_id = in_range.electric_pole and in_range.electric_pole.valid and in_range.electric_pole.electric_network_id or -2 + if network_id == in_range_network_id then + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto", position.x, position.y }, status_icons.connected, true + elseif status.bypass_proximity then + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-bypass-different-network", position.x, position.y }, status_icons.bypass, true + else + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-different-network" }, status_icons.different, false + end + end + + row.warp_button.tooltip = tooltip + row.warp_button.enabled = enabled + row.status_label.tooltip = tooltip + row.status_label.caption = icon + row.name_label.style.font = enabled and "default-semibold" or "default" +end + +--- Refresh a row to match its warp, the player's permissions, and where the player is +--- @param warp_table LuaGuiElement +--- @param row ExpGui_WarpList.elements.warp_table.row +--- @param status ExpGui_WarpList.PlayerStatus +function Elements.warp_table.refresh_row(warp_table, row, status) + local player = Gui.get_player(warp_table) + local warp = row.warp + local can_edit = has_permission_edit_warp(player, warp) + local editing = warp.editing[player.index] ~= nil + + Elements.edit_warp_button.refresh(row.edit_warp_button, warp) + Elements.name_label.refresh(row.name_label, warp) + + row.warp_button.visible = not editing + row.icon_picker.visible = editing + row.name_label.visible = not editing + row.name_field.visible = editing + row.confirm_edit_button.visible = editing + row.discard_edit_button.visible = editing + row.edit_warp_button.visible = can_edit and not editing + row.remove_warp_button.visible = can_edit and not editing + + if editing then + row.name_field.focus() + assert(warp_table.parent).scroll_to_element(row.name_field, "top-third") + else + row.warp_button.sprite = icon_sprite(warp.icon) + row.icon_picker.elem_value = warp.icon + row.name_field.text = warp.name + end + + Elements.warp_table._refresh_row_status(row, status) +end + +--- Rebuild every row in the order of the warps +--- @param warp_table LuaGuiElement +--- @param warps ExpGui_WarpList.Warp[] +--- @param status ExpGui_WarpList.PlayerStatus +function Elements.warp_table.rebuild(warp_table, warps, status) + warp_table.clear() + Elements.warp_table.data[warp_table].rows = {} + for _, warp in ipairs(warps) do + local row = Elements.warp_table.add_row(warp_table, warp) + Elements.warp_table.refresh_row(warp_table, row, status) + end +end + +--- Refresh the row of one warp for the online players of its force +--- @param warp ExpGui_WarpList.Warp +function Elements.warp_table.refresh_row_force(warp) + for player, warp_table in Elements.warp_table:online_elements(warp.force) do + --- @cast player -nil + local row = Elements.warp_table.data[warp_table].rows[warp.id] + if row then + Elements.warp_table.refresh_row(warp_table, row, Elements.container.get_status(player)) + end + end +end + +--- Refresh whether each warp can be used from where the player is +--- @param player LuaPlayer +function Elements.warp_table.refresh_status_player(player) + local status = Elements.container.get_status(player) + for _, warp_table in Elements.warp_table:tracked_elements(player) do + for _, row in pairs(Elements.warp_table.data[warp_table].rows) do + Elements.warp_table._refresh_row_status(row, status) + end + end +end + +--- Rebuild the rows for a player, used when their force or permissions change +--- @param player LuaPlayer +function Elements.warp_table.rebuild_player(player) + local warps = Elements.container.get_warps(player.force --[[@as LuaForce]]) + local status = Elements.container.get_status(player) + for _, warp_table in Elements.warp_table:tracked_elements(player) do + Elements.warp_table.rebuild(warp_table, warps, status) + end +end + +--- Rebuild the rows for the online players of a force, used when the warps change +--- @param force LuaForce +function Elements.warp_table.rebuild_force(force) + local warps = Elements.container.get_warps(force) + for player, warp_table in Elements.warp_table:online_elements(force) do + --- @cast player -nil + Elements.warp_table.rebuild(warp_table, warps, Elements.container.get_status(player)) + end +end + +--- Progress bar which fills as the warp cooldown runs out +--- @class ExpGui_WarpList.elements.cooldown_bar: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.cooldown_bar = Gui.define("warp_list/cooldown_bar") + :track_all_elements() + :draw{ + type = "progressbar", + value = 1, + tooltip = { "exp-gui_warp-list.tooltip-cooldown-none", config.cooldown_duration }, + } + :style{ + horizontally_stretchable = true, + color = Colors.light_blue, + } --[[@as any]] + +--- Refresh the bar to show the ticks left on the cooldown +--- @param cooldown_bar LuaGuiElement +--- @param cooldown number +function Elements.cooldown_bar.refresh(cooldown_bar, cooldown) + if cooldown > 0 then + cooldown_bar.value = 1 - cooldown / cooldown_ticks + cooldown_bar.tooltip = { "exp-gui_warp-list.tooltip-cooldown", ceil(cooldown / 60) } + else + cooldown_bar.value = 1 + cooldown_bar.tooltip = { "exp-gui_warp-list.tooltip-cooldown-none", config.cooldown_duration } + end +end + +--- Refresh the bars of a player +--- @param player LuaPlayer +--- @param cooldown number +function Elements.cooldown_bar.refresh_player(player, cooldown) + for _, cooldown_bar in Elements.cooldown_bar:tracked_elements(player) do + Elements.cooldown_bar.refresh(cooldown_bar, cooldown) + end +end + +--- @class ExpGui_WarpList.elements.container.elements +--- @field frame LuaGuiElement +--- @field add_warp_button LuaGuiElement +--- @field warp_table LuaGuiElement +--- @field cooldown_bar LuaGuiElement + +--- @class ExpGui_WarpList.elements.container.force_data +--- @field warps ExpGui_WarpList.Warp[] The spawn warp first, then by name +--- @field spawn ExpGui_WarpList.Warp? +--- @field next_warp_id number + +--- @class ExpGui_WarpList.elements.container.player_data +--- @field cooldown number Ticks until the player can warp again +--- @field in_range ExpGui_WarpList.Warp? The warp the player is standing on +--- @field blocking ExpGui_WarpList.Warp? The warp too close for another to be added +--- @field keep_open boolean True once the player toggles the list themselves, which stops it hiding when they leave a warp + +--- @class ExpGui_WarpList.elements.container.data +--- @field [LuaGuiElement] ExpGui_WarpList.elements.container.elements +--- @field [LuaForce] ExpGui_WarpList.elements.container.force_data +--- @field [LuaPlayer] ExpGui_WarpList.elements.container.player_data + +--- Container added to the left gui flow +--- @class ExpGui_WarpList.elements.container: ExpElement +--- @field data ExpGui_WarpList.elements.container.data +Elements.container = Gui.define("warp_list/container") + :draw(function(def, parent) + --- @cast def ExpGui_WarpList.elements.container + local player = Gui.get_player(parent) + local allow_add_warp = has_config_permission(player, "allow_add_warp") + local container = Gui.elements.container(parent, allow_add_warp and 268 or 220) + + local header = Gui.elements.header(container, { + caption = { "exp-gui_warp-list.caption-main" }, + tooltip = { + "exp-gui_warp-list.tooltip-sub", + config.cooldown_duration, + config.standard_proximity_radius, + { "exp-gui_warp-list.tooltip-sub-current", status_icons.current }, + { "exp-gui_warp-list.tooltip-sub-connected", status_icons.connected }, + { "exp-gui_warp-list.tooltip-sub-different", status_icons.different }, + { "exp-gui_warp-list.tooltip-sub-cooldown", status_icons.cooldown }, + { "exp-gui_warp-list.tooltip-sub-not-available", status_icons.not_available }, + { "exp-gui_warp-list.tooltip-sub-bypass", status_icons.bypass }, + }, + }) + + local add_warp_button = Elements.add_warp_button(header) + add_warp_button.visible = allow_add_warp + + local warp_table = Elements.warp_table(container) + local force = player.force --[[@as LuaForce]] + Elements.warp_table.rebuild(warp_table, def.get_warps(force), def.get_status(player)) + + local cooldown_bar = Elements.cooldown_bar(container) + Elements.cooldown_bar.refresh(cooldown_bar, def._get_player_data(player).cooldown) + + local root = Gui.elements.container.get_root_element(container) + def.data[root] = { + frame = container, + add_warp_button = add_warp_button, + warp_table = warp_table, + cooldown_bar = cooldown_bar, + } + + return root + end) --[[@as any]] + +--- Get or create the data for a force +--- @param force LuaForce +--- @return ExpGui_WarpList.elements.container.force_data +function Elements.container._get_force_data(force) + local force_data = Elements.container.data[force] + if not force_data then + force_data = { warps = {}, next_warp_id = 1 } + Elements.container.data[force] = force_data + end + return force_data +end + +--- Get or create the data for a player +--- @param player LuaPlayer +--- @return ExpGui_WarpList.elements.container.player_data +function Elements.container._get_player_data(player) + local player_data = Elements.container.data[player] + if not player_data then + player_data = { cooldown = 0, keep_open = false } + Elements.container.data[player] = player_data + end + return player_data +end + +--- Get the warps of a force, the spawn warp first and then by name +--- @param force LuaForce +--- @return ExpGui_WarpList.Warp[] +function Elements.container.get_warps(force) + return Elements.container._get_force_data(force).warps +end + +--- Get the spawn warp of a force +--- @param force LuaForce +--- @return ExpGui_WarpList.Warp? +function Elements.container.get_spawn_warp(force) + return Elements.container._get_force_data(force).spawn +end + +--- Get what decides which warps a player can use +--- @param player LuaPlayer +--- @return ExpGui_WarpList.PlayerStatus +function Elements.container.get_status(player) + local player_data = Elements.container._get_player_data(player) + return { + in_range = player_data.in_range, + on_cooldown = player_data.cooldown > 0, + bypass_proximity = has_config_permission(player, "bypass_warp_proximity"), + } +end + +--- Keep the warps of a force in display order +--- @param force_data ExpGui_WarpList.elements.container.force_data +function Elements.container._sort_warps(force_data) + local spawn = force_data.spawn + table.sort(force_data.warps, function(a, b) + if a == spawn then return true end + if b == spawn then return false end + if a.name == b.name then return a.id < b.id end + return a.name < b.name + end) +end + +--- Add or update the map tag of a warp +--- @param warp ExpGui_WarpList.Warp +function Elements.container._make_tag(warp) + local tag = warp.tag + if tag and tag.valid then + tag.text = "Warp: " .. warp.name + tag.icon = warp.icon + return + end + + local position = warp.position + warp.tag = warp.force.add_chart_tag(warp.surface, { + position = { x = position.x + 0.5, y = position.y + 0.5 }, + text = "Warp: " .. warp.name, + icon = warp.icon, + }) +end + +--- Remove the map tag of a warp +--- @param warp ExpGui_WarpList.Warp +function Elements.container._remove_tag(warp) + local tag = warp.tag + if tag and tag.valid then + tag.destroy() + end + warp.tag = nil +end + +--- Place the tiles and entities which mark a warp on the ground +--- @param warp ExpGui_WarpList.Warp +function Elements.container._make_area(warp) + local surface = warp.surface + local position = warp.position + warp.old_tile = surface.get_tile(position.x, position.y).name + + local tiles = {} + for index, tile in ipairs(config.tiles) do + tiles[index] = { name = tile[1], position = { x = tile[2] + position.x, y = tile[3] + position.y } } + end + surface.set_tiles(tiles) + + for _, entity_config in ipairs(config.entities) do + local entity = assert(surface.create_entity{ + name = entity_config[1], + position = { x = entity_config[2] + position.x, y = entity_config[3] + position.y }, + force = "neutral", + }) + entity.destructible = false + entity.health = 0 + entity.minable_flag = false + entity.rotatable = false + + if entity.type == "electric-pole" then + warp.electric_pole = entity + end + end +end + +--- Restore the ground under a warp and remove its entities +--- @param warp ExpGui_WarpList.Warp +function Elements.container._remove_area(warp) + local old_tile = warp.old_tile + if not old_tile then return end + + local surface = warp.surface + local position = warp.position + local tiles = {} + for index, tile in ipairs(config.tiles) do + tiles[index] = { name = old_tile, position = { x = tile[2] + position.x, y = tile[3] + position.y } } + end + surface.set_tiles(tiles) + + local radius = config.standard_proximity_radius + local area = { + left_top = { x = position.x - radius, y = position.y - radius }, + right_bottom = { x = position.x + radius, y = position.y + radius }, + } + local entities = surface.find_entities_filtered{ force = "neutral", area = area, name = area_entity_names } + for _, entity in pairs(entities) do + if entity.destructible == false then + entity.destructible = true + entity.die(entity.force) + end + end + + -- The area may not be covered by a radar + warp.force.chart(surface, area) +end + +--- Add a warp for a force, when added by a player it is new until they confirm their first edit +--- @param force LuaForce +--- @param surface LuaSurface +--- @param position MapPosition.struct +--- @param player LuaPlayer? +--- @param name string? +--- @return ExpGui_WarpList.Warp +function Elements.container.add_warp(force, surface, position, player, name) + local force_data = Elements.container._get_force_data(force) + local id = force_data.next_warp_id + force_data.next_warp_id = id + 1 + + --- @type ExpGui_WarpList.Warp + local warp = { + id = id, + force = force, + name = name or "New warp", + icon = { type = config.default_icon.type, name = config.default_icon.name }, + surface = surface, + position = { x = floor(position.x), y = floor(position.y) }, + last_user = player, + last_edit_tick = game.tick, + editing = {}, + } + + -- Warps added by a player start in edit mode, discarding that edit removes them again + if player then + warp.new = true + Elements.container._make_area(warp) + end + + force_data.warps[#force_data.warps + 1] = warp + Elements.container._sort_warps(force_data) + Elements.container._make_tag(warp) + + Elements.warp_table.rebuild_force(force) + return warp +end + +--- Remove a warp along with its area and map tag +--- @param warp ExpGui_WarpList.Warp +function Elements.container.remove_warp(warp) + local force_data = Elements.container._get_force_data(warp.force) + table.remove_element(force_data.warps, warp) + if force_data.spawn == warp then + force_data.spawn = nil + end + + Elements.container._remove_tag(warp) + Elements.container._remove_area(warp) + + -- Offline players are corrected by their next proximity update + for _, player in pairs(warp.force.connected_players) do + local player_data = Elements.container._get_player_data(player) + if player_data.in_range == warp then player_data.in_range = nil end + if player_data.blocking == warp then player_data.blocking = nil end + end + + for _, warp_table in Elements.warp_table:online_elements(warp.force) do + Elements.warp_table.remove_row(warp_table, warp) + end +end + +--- Save a new name and icon for a warp, which also ends the player's edit +--- @param warp ExpGui_WarpList.Warp +--- @param name string +--- @param icon SignalID +--- @param player LuaPlayer +function Elements.container.update_warp(warp, name, icon, player) + warp.name = name + warp.icon = icon + warp.last_user = player + warp.last_edit_tick = game.tick + warp.editing[player.index] = nil + warp.new = nil + + Elements.container._sort_warps(Elements.container._get_force_data(warp.force)) + Elements.container._make_tag(warp) + Elements.warp_table.rebuild_force(warp.force) +end + +--- Start or stop a player editing a warp +--- @param warp ExpGui_WarpList.Warp +--- @param player LuaPlayer +--- @param editing boolean +function Elements.container.set_editing(warp, player, editing) + warp.editing[player.index] = editing and player.name or nil + Elements.warp_table.refresh_row_force(warp) +end + +--- Make a warp the spawn of its force, the force respawns there +--- @param warp ExpGui_WarpList.Warp +function Elements.container.set_spawn_warp(warp) + local force_data = Elements.container._get_force_data(warp.force) + force_data.spawn = warp + Elements.container._sort_warps(force_data) + warp.force.set_spawn_position(warp.position, warp.surface) +end + +--- Move a player to a warp and start their cooldown +--- @param player LuaPlayer +--- @param warp ExpGui_WarpList.Warp +function Elements.container.teleport_player(player, warp) + local surface = warp.surface + local position = { x = warp.position.x + 0.5, y = warp.position.y + 0.5 } + + local vehicle = player.vehicle + if vehicle then + local vehicle_position = surface.find_non_colliding_position(vehicle.name, position, 32, 1) + -- Only cars can be teleported between surfaces + if vehicle.type == "car" then + vehicle.teleport(vehicle_position, surface) + elseif surface == vehicle.surface then + if not vehicle.teleport(vehicle_position) then + player.driving = false + player.teleport(surface.find_non_colliding_position("character", position, 32, 1), surface) + end + end + else + player.teleport(surface.find_non_colliding_position("character", position, 32, 1), surface) + end + + local player_data = Elements.container._get_player_data(player) + if not has_config_permission(player, "bypass_warp_cooldown") then + player_data.cooldown = cooldown_ticks + Elements.cooldown_bar.refresh_player(player, cooldown_ticks) + end + player_data.in_range = warp + Elements.warp_table.refresh_status_player(player) +end + +--- Refresh the parts of a player's gui which depend on their permissions +--- @param player LuaPlayer +function Elements.container.refresh_player(player) + local allow_add_warp = has_config_permission(player, "allow_add_warp") + for _, container in Elements.container:tracked_elements(player) do + local elements = Elements.container.data[container] + elements.frame.style.minimal_width = allow_add_warp and 268 or 220 + elements.add_warp_button.visible = allow_add_warp + end + Elements.warp_table.rebuild_player(player) +end + +--- Count down the cooldown of a player +--- @param player LuaPlayer +--- @param player_data ExpGui_WarpList.elements.container.player_data +function Elements.container._update_cooldown(player, player_data) + if player_data.cooldown == 0 then return end + local cooldown = player_data.cooldown - update_interval + if cooldown < 0 then cooldown = 0 end + player_data.cooldown = cooldown + + Elements.cooldown_bar.refresh_player(player, cooldown) + if cooldown == 0 then + Elements.warp_table.refresh_status_player(player) + end +end + +--- Find which warp a player is standing on and whether one can be added where they are +--- @param player LuaPlayer +--- @param player_data ExpGui_WarpList.elements.container.player_data +function Elements.container._update_proximity(player, player_data) + local force_data = Elements.container._get_force_data(player.force --[[@as LuaForce]]) + local surface = player.physical_surface + local position = player.physical_position + local px, py = position.x, position.y + + local closest, closest_distance = nil, nil --- @type ExpGui_WarpList.Warp?, number? + for _, warp in ipairs(force_data.warps) do + if warp.surface == surface then + local dx, dy = px - warp.position.x, py - warp.position.y + local distance = dx * dx + dy * dy + if not closest_distance or distance < closest_distance then + closest, closest_distance = warp, distance + end + end + end + + local in_range = nil --- @type ExpGui_WarpList.Warp? + if closest and closest_distance < (closest == force_data.spawn and spawn_radius_sq or proximity_radius_sq) then + in_range = closest + end + if in_range ~= player_data.in_range then + player_data.in_range = in_range + if not player_data.keep_open then + Gui.toolbar.set_left_element_visible_state(Elements.container, player, in_range ~= nil) + end + Elements.warp_table.refresh_status_player(player) + end + + local blocking = nil --- @type ExpGui_WarpList.Warp? + if closest and closest_distance <= minimum_distance_sq then + blocking = closest + end + if blocking ~= player_data.blocking then + player_data.blocking = blocking + Elements.add_warp_button.refresh_player_blocking(player, blocking) + end +end + +--- Add the element to the left flow with a toolbar button +Gui.add_left_element(Elements.container, false) +Gui.toolbar.create_button{ + name = "toggle_warp_list", + left_element = Elements.container, + sprite = config.default_icon.type .. "/" .. config.default_icon.name, + tooltip = { "exp-gui_warp-list.tooltip-main" }, + visible = function(player, element) + return Roles.player_has_permission(player, "exp_scenario.gui.warp_list") + end +}:on_click(function(def, player) + -- A list the player opened themselves stays open when they leave a warp + Elements.container._get_player_data(player).keep_open = Gui.toolbar.get_button_toggled_state(def, player) +end) + +--- Give a force a spawn warp when its first player is created +--- @param event EventData.on_player_created +local function on_player_created(event) + local player = Gui.get_player(event) + local force = player.force --[[@as LuaForce]] + if Elements.container.get_spawn_warp(force) then return end + + local surface = player.physical_surface + local position = force.get_spawn_position(surface) + local warp = Elements.container.add_warp(force, surface, position, nil, "Spawn") + Elements.container.set_spawn_warp(warp) + + -- Spawn has no warp area, so any pole nearby connects it to the network + local poles = surface.find_entities_filtered{ type = "electric-pole", position = position, radius = 20, limit = 1 } + warp.electric_pole = poles[1] +end + +--- Rebuild the gui for a player as it may be outdated +--- @param event EventData.on_player_joined_game | EventData.on_player_changed_force +local function refresh_player_warps(event) + local player = Gui.get_player(event) + Elements.container.refresh_player(player) + Elements.cooldown_bar.refresh_player(player, Elements.container._get_player_data(player).cooldown) +end + +--- Rebuild the gui for a player as their permissions may have changed +--- @param event EventData.ExpRoles.on_player_roles_changed +local function refresh_player_permissions(event) + Elements.container.refresh_player(Gui.get_player(event)) +end + +--- Restore the map tag of a warp when a player edits or removes it +--- @param event EventData.on_chart_tag_modified | EventData.on_chart_tag_removed +local function on_chart_tag_changed(event) + if not event.player_index then return end + for _, warp in ipairs(Elements.container.get_warps(event.force)) do + local tag = warp.tag + if not tag or not tag.valid or tag == event.tag then + if event.name == defines.events.on_chart_tag_removed then + warp.tag = nil + end + Elements.container._make_tag(warp) + end + end +end + +--- Count down cooldowns and check where each player is +local function update_players() + for _, player in pairs(game.connected_players) do + local player_data = Elements.container._get_player_data(player) + Elements.container._update_cooldown(player, player_data) + Elements.container._update_proximity(player, player_data) + end +end + +local e = defines.events + +return { + elements = Elements, + events = { + [e.on_player_created] = on_player_created, + [e.on_player_joined_game] = refresh_player_warps, + [e.on_player_changed_force] = refresh_player_warps, + [e.on_chart_tag_modified] = on_chart_tag_changed, + [e.on_chart_tag_removed] = on_chart_tag_changed, + [Roles.events.on_player_roles_changed] = refresh_player_permissions, + }, + on_nth_tick = { + [update_interval] = update_players, + } +} diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index 55d72baf..7499ac2c 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -490,6 +490,70 @@ caption-create-footer=Create task caption-edit-footer=Edit task caption-view-footer=Task details +[exp-gui_vlayer] +tooltip-main=Virtual Layer +selection-name=vlayer +caption-stat-solar=[img=entity/solar-panel] Solar Panels +tooltip-stat-solar=Solar panels in use out of those stored +caption-stat-accumulator=[img=entity/accumulator] Accumulators +tooltip-stat-accumulator=Accumulators in use out of those stored +caption-stat-surface-area=[virtual-signal=signal-R] Remaining Surface +tooltip-stat-surface-area=Remaining surface area, insert landfill to increase it +caption-stat-sustained=[virtual-signal=signal-S] Sustained Production +tooltip-stat-sustained=Power production averaged over a day, out of what every stored solar panel could give +caption-stat-production=[virtual-signal=signal-P] Current Production +tooltip-stat-production=Power production right now, out of the production in full daylight +caption-stat-capacity=[virtual-signal=signal-E] Battery Charge +tooltip-stat-capacity=Stored energy out of the accumulator capacity +type-energy=Energy +type-circuit=Circuit +type-storage-input=Storage Input +type-storage-output=Storage Output +caption-refresh=Refresh List +caption-view=View Interface +caption-build=Build Interface +caption-remove=Remove Interface +message-located=The __1__ interface is at __2__ +message-built=__1__ built a __2__ interface at __3__ +message-removed=__1__ removed a __2__ interface at __3__ +message-no-chest=No steel chest was selected +message-chest-not-empty=The steel chest is not empty +message-multiple=Cannot build a __1__ interface, more than one steel chest was selected +message-limit=Cannot build a __1__ interface, the limit has been reached +message-no-space=Cannot build an energy interface here, there is not enough space +message-research=You need to research [technology=__1__] to level __2__ to use the vlayer on a space platform + +[exp-gui_warp-list] +caption-main=Warp List [img=info] +tooltip-main=Warp List +tooltip-sub=Warps can only be used every __1__ seconds and when within __2__ tiles\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ +tooltip-sub-current= - __1__ This is your current warp point; +tooltip-sub-connected= - __1__ You can travel to this warp point; +tooltip-sub-different= - __1__ This warp is on a different network; +tooltip-sub-cooldown= - __1__ You are currently on cooldown; +tooltip-sub-not-available= - __1__ You are not in range of a warp point; +tooltip-sub-bypass= - __1__ Your role allows you to travel here; +tooltip-add=Add new warp +tooltip-add-too-close=Cannot create warp; too close to existing warp point: __1__ +add-warning-water=Cannot create warp; too close to water, please move __1__ tiles away from the water body or landfill it +add-warning-entities=Cannot create warp; too close to other entities, please move __1__ tiles away from the entities or remove them +tooltip-icon=Edit warp icon +tooltip-last-edit=Last edited by __1__ at __2__\nClick to view on map +tooltip-confirm=Save changes +tooltip-discard=Discard changes +tooltip-edit=Currently being edited by: __1__ +tooltip-edit-none=Currently being edited by: Nobody +tooltip-remove=Remove warp +tooltip-cooldown=Please wait __1__ seconds before you can warp +tooltip-cooldown-none=After each warp you will need to wait __1__ seconds before you can warp again +tooltip-goto=Go to x __1__ y __2__ +tooltip-goto-bypass=Go to x __1__ y __2__, bypass mode +tooltip-goto-bypass-different-network=Go to x __1__ y __2__, bypass mode different network +tooltip-goto-same-warp=You are already on this warp +tooltip-goto-different-network=This warp is on a different network, use power poles to connect it +tooltip-goto-cooldown=You are on cooldown, wait for your cooldown to recharge +tooltip-goto-disabled=You are not on a warp point, walk to a warp point + [exp_afk-kick] kick-message=All players were kicked because everyone was AFK. @@ -507,23 +571,23 @@ 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-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-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-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-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-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-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-1= ☕ Boiling the water… ☕ reply-tea-2= ☕ __1__ your tea is done! ☕ reply-mead-1= Filling the drinking horn reply-mead-2= Skål! diff --git a/exp_scenario/module/locale/zh-CN.cfg b/exp_scenario/module/locale/zh-CN.cfg index 31e68f74..19d7d004 100644 --- a/exp_scenario/module/locale/zh-CN.cfg +++ b/exp_scenario/module/locale/zh-CN.cfg @@ -84,7 +84,7 @@ home-get=你的回程傳送點在 __1__ 上, x: __2__ y: __3__ description=把用戶送監而禁止其行動。 arg-player=用戶 arg-reason=原因 -jailed=__1__ 已被 __2__ ,因 __3__ 而被禁止行動。 +jailed=__1__ 已被 __2__ ,因 __3__ 而被禁止行動 already-jailed=__1__ 已被禁止行動。 [exp-commands_unjail] @@ -129,6 +129,8 @@ already-protected=本地區已受保護。 protected-area=本地區現已受保護。 unprotected-area=本地區現不受保護。 repeat-offence=__1__ 在 [gps=__3__,__4__] 拆除了 __2__ +enter-entity-selection = Entered entity selection, select entites to protect, hold shift to remove protection. +enter-area-selection = Entered area selection, select areas to protect, hold shift to remove protection. [exp-commands_tag-protection] description=切換管理員標記模式,新標記只能由管理員編輯。 @@ -153,6 +155,7 @@ fluid-in=你每秒需要 __1__ 來做 [fluid=__2__]。 item-out=結果: __1__ [item=__2__] 每秒。 fluid-out=結果: __1__ [fluid=__2__] 每秒。 machines=你需要 __1__ 部同樣的機器。 +machine-count = And you will need __1__ machines (with the same speed as this one) for this. [exp-commands_repair] selection-name=維修 @@ -235,9 +238,9 @@ response=__1__ 把 __2__ 個火車設置為自動模式。 description-teleport=將一名玩家傳送到另一名玩家。 description-bring=將一名玩家傳送到你身邊。 description-goto=將你傳送到玩家身邊。 -description-spawn=傳送到出生點。 +description-spawn=傳送到出生點 arg-player-teleport=傳送的玩家,若沒有目標,你會被傳送到這。 -arg-player-from=傳送的玩家 +arg-player-from=傳送的玩家。 arg-player-to=傳送的玩家目標 unavailable=沒找到空間進行傳送 same-player=沒辦法傳送到自己 @@ -389,6 +392,7 @@ data-statistics=數據 data-required=需要 data-misc=雜項 data-format=__1____2__ +welcome-title = Welcome to __1__ [exp-gui_research-milestones] tooltip-main=研究介面 @@ -480,16 +484,81 @@ tooltip-edit-none=現沒有被人修改 caption-create-footer=加入工作流程 caption-edit-footer=修改工作流程 caption-view-footer=工作流程細節 +tooltip-confirm = Save changes (minimum of 5 characters long) + +[exp-gui_warp-list] +caption-main=傳送陣清單 [img=info] +tooltip-main=傳送陣清單 +tooltip-sub=傳送陣在 __1__ 秒內只能使用一次。 亦需在 __2__ 格之內使用\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ +tooltip-sub-current= - __1__ 你目前的傳送陣; +tooltip-sub-connected= - __1__ 你可以到本傳送陣; +tooltip-sub-different= - __1__ 該傳送陣未連接; +tooltip-sub-cooldown= - __1__ 傳送冷卻中; +tooltip-sub-not-available= - __1__ 你不在傳送陣範圍; +tooltip-sub-bypass= - __1__ 你可以傳送到這裏; +tooltip-add=新建傳送陣 +tooltip-add-too-close=無法新建傳送陣; 太接近 __1__ 了 +add-warning-water=無法新建傳送陣; 太接近水源了, 請移動 __1__ 格又或者填平該地方 +add-warning-entities=無法新建傳送陣; 太接近其他東西, 請移動 __1__ 格又或者拆除該東西 +tooltip-icon=修改傳送陣圖案 +tooltip-last-edit=最後由 __1__ 在 __2__ 更改\n按這在地圖上看 +tooltip-confirm=儲存更改 +tooltip-discard=放棄更改 +tooltip-edit=現被 __1__ 修改中 +tooltip-edit-none=現沒有被人修改 +tooltip-remove=拆除傳送陣 +tooltip-cooldown=請等 __1__ 秒才再次傳送 +tooltip-cooldown-none=你要等 __1__ 秒才可以再次傳送 +tooltip-goto=到 X __1__ Y __2__ +tooltip-goto-bypass=到 X __1__ Y __2__, 繞行模式 +tooltip-goto-bypass-different-network=到 X __1__ Y __2__, 繞行模式 (不同網絡) +tooltip-goto-same-warp=你已在該傳送陣 +tooltip-goto-different-network=該傳送陣未連接, 你可使用電纜連接 +tooltip-goto-cooldown=傳送冷卻中, 請等候冷卻 +tooltip-goto-disabled=你不在傳送陣, 請移至任意傳送陣 + +[exp-gui_vlayer] +tooltip-main=Vlayer 介面 +selection-name=vlayer +caption-stat-solar=[img=entity/solar-panel] 太陽能板 +tooltip-stat-solar=使用中的太陽能板數量 / 儲存的太陽能板數量 +caption-stat-accumulator=[img=entity/accumulator] 蓄電池 +tooltip-stat-accumulator=使用中的蓄電池數量 / 儲存的蓄電池數量 +caption-stat-surface-area=[virtual-signal=signal-R] 餘下土地 +tooltip-stat-surface-area=餘下土地。 放入堆填來增加 +caption-stat-sustained=[virtual-signal=signal-S] 持續產能 +tooltip-stat-sustained=持續電力產能 +caption-stat-production=[virtual-signal=signal-P] 現時產能 +tooltip-stat-production=現時電力產能 +caption-stat-capacity=[virtual-signal=signal-E] 電池電量 +tooltip-stat-capacity=現時電池電量 / 電池最大容量 +type-energy=電力 +type-circuit=回路 +type-storage-input=放入箱 +type-storage-output=提取箱 +caption-refresh=刷新列表 +caption-view=查看介面 +caption-build=建造介面 +caption-remove=拆除介面 +message-located=__1__ 介面在 __2__ +message-built=__1__ 在 __3__ 建造了 __2__ 介面 +message-removed=__1__ 在 __3__ 拆除了 __2__ 介面 +message-no-chest=沒檢測到鋼箱 +message-chest-not-empty=鋼箱不是空的 +message-multiple=無法建造 __1__ 介面,檢測到多個鋼箱 +message-limit=無法建造 __1__ 介面,超出最大限制 +message-no-space=無法在此建造電力介面,空間不足 +message-research=你要研究 [technology=__1__] 在 __2__ 級 才可使用太空平台 vlayer 功能。 [exp_afk-kick] kick-message=因地圖中沒有活躍玩家,所以所有人都已被請離。 [exp_chat-auto-reply] chat-reply=[Compilatron] __1__ -chat-disallowed=你沒有權限使用這個指令。 +chat-disallowed=You can't use global chat commands reply-online=現在有 __1__ 人在線 reply-players=這張地圖上共有__1__名用戶 -reply-dev=Cooldude2606 只是本場境的開發者 +reply-dev=Cooldude2606 is a dev for this server and makes the scenario and is not a factorio dev. reply-softmod=這裹用了自設情境。 reply-clusterio=Clusterio is a factorio server hosting platform allowing internet enabled mods. reply-blame=責怪 __1__ 吧! @@ -498,23 +567,23 @@ 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-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-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-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-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-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-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-1= ☕ Boiling the water… ☕ reply-tea-2= ☕ __1__ your tea is done! ☕ reply-mead-1= Filling the drinking horn reply-mead-2= Skål! @@ -546,3 +615,22 @@ chat-jailed=__1__ 因被多次拆除受保護物體而被禁止行動。請等 [exp_report-jail] chat-jailed=__1__ 因被多次舉報而被禁止行動。請等候管理員作出下一步處理。 + + +[exp-gui_player-list] +main-tooltip = Player List +open-action-bar = Options +close-action-bar = Close Options +reason-confirm = Confirm Reason +reason-entry = Enter Reason +goto-player = Goto player +bring-player = Bring player +report-player = Report player +jail-player = Jail player +kick-player = Kick player +ban-player = Ban player +afk-time = __1__% of total map time\nLast moved __2__ ago +open-map = __1__ __2__\n__3__\nClick to open map + +[exp_spectate] +follow-label = Following __1__.\nClick here or press escape to stop following. \ No newline at end of file diff --git a/exp_scenario/module/locale/zh-TW.cfg b/exp_scenario/module/locale/zh-TW.cfg index 31e68f74..19d7d004 100644 --- a/exp_scenario/module/locale/zh-TW.cfg +++ b/exp_scenario/module/locale/zh-TW.cfg @@ -84,7 +84,7 @@ home-get=你的回程傳送點在 __1__ 上, x: __2__ y: __3__ description=把用戶送監而禁止其行動。 arg-player=用戶 arg-reason=原因 -jailed=__1__ 已被 __2__ ,因 __3__ 而被禁止行動。 +jailed=__1__ 已被 __2__ ,因 __3__ 而被禁止行動 already-jailed=__1__ 已被禁止行動。 [exp-commands_unjail] @@ -129,6 +129,8 @@ already-protected=本地區已受保護。 protected-area=本地區現已受保護。 unprotected-area=本地區現不受保護。 repeat-offence=__1__ 在 [gps=__3__,__4__] 拆除了 __2__ +enter-entity-selection = Entered entity selection, select entites to protect, hold shift to remove protection. +enter-area-selection = Entered area selection, select areas to protect, hold shift to remove protection. [exp-commands_tag-protection] description=切換管理員標記模式,新標記只能由管理員編輯。 @@ -153,6 +155,7 @@ fluid-in=你每秒需要 __1__ 來做 [fluid=__2__]。 item-out=結果: __1__ [item=__2__] 每秒。 fluid-out=結果: __1__ [fluid=__2__] 每秒。 machines=你需要 __1__ 部同樣的機器。 +machine-count = And you will need __1__ machines (with the same speed as this one) for this. [exp-commands_repair] selection-name=維修 @@ -235,9 +238,9 @@ response=__1__ 把 __2__ 個火車設置為自動模式。 description-teleport=將一名玩家傳送到另一名玩家。 description-bring=將一名玩家傳送到你身邊。 description-goto=將你傳送到玩家身邊。 -description-spawn=傳送到出生點。 +description-spawn=傳送到出生點 arg-player-teleport=傳送的玩家,若沒有目標,你會被傳送到這。 -arg-player-from=傳送的玩家 +arg-player-from=傳送的玩家。 arg-player-to=傳送的玩家目標 unavailable=沒找到空間進行傳送 same-player=沒辦法傳送到自己 @@ -389,6 +392,7 @@ data-statistics=數據 data-required=需要 data-misc=雜項 data-format=__1____2__ +welcome-title = Welcome to __1__ [exp-gui_research-milestones] tooltip-main=研究介面 @@ -480,16 +484,81 @@ tooltip-edit-none=現沒有被人修改 caption-create-footer=加入工作流程 caption-edit-footer=修改工作流程 caption-view-footer=工作流程細節 +tooltip-confirm = Save changes (minimum of 5 characters long) + +[exp-gui_warp-list] +caption-main=傳送陣清單 [img=info] +tooltip-main=傳送陣清單 +tooltip-sub=傳送陣在 __1__ 秒內只能使用一次。 亦需在 __2__ 格之內使用\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ +tooltip-sub-current= - __1__ 你目前的傳送陣; +tooltip-sub-connected= - __1__ 你可以到本傳送陣; +tooltip-sub-different= - __1__ 該傳送陣未連接; +tooltip-sub-cooldown= - __1__ 傳送冷卻中; +tooltip-sub-not-available= - __1__ 你不在傳送陣範圍; +tooltip-sub-bypass= - __1__ 你可以傳送到這裏; +tooltip-add=新建傳送陣 +tooltip-add-too-close=無法新建傳送陣; 太接近 __1__ 了 +add-warning-water=無法新建傳送陣; 太接近水源了, 請移動 __1__ 格又或者填平該地方 +add-warning-entities=無法新建傳送陣; 太接近其他東西, 請移動 __1__ 格又或者拆除該東西 +tooltip-icon=修改傳送陣圖案 +tooltip-last-edit=最後由 __1__ 在 __2__ 更改\n按這在地圖上看 +tooltip-confirm=儲存更改 +tooltip-discard=放棄更改 +tooltip-edit=現被 __1__ 修改中 +tooltip-edit-none=現沒有被人修改 +tooltip-remove=拆除傳送陣 +tooltip-cooldown=請等 __1__ 秒才再次傳送 +tooltip-cooldown-none=你要等 __1__ 秒才可以再次傳送 +tooltip-goto=到 X __1__ Y __2__ +tooltip-goto-bypass=到 X __1__ Y __2__, 繞行模式 +tooltip-goto-bypass-different-network=到 X __1__ Y __2__, 繞行模式 (不同網絡) +tooltip-goto-same-warp=你已在該傳送陣 +tooltip-goto-different-network=該傳送陣未連接, 你可使用電纜連接 +tooltip-goto-cooldown=傳送冷卻中, 請等候冷卻 +tooltip-goto-disabled=你不在傳送陣, 請移至任意傳送陣 + +[exp-gui_vlayer] +tooltip-main=Vlayer 介面 +selection-name=vlayer +caption-stat-solar=[img=entity/solar-panel] 太陽能板 +tooltip-stat-solar=使用中的太陽能板數量 / 儲存的太陽能板數量 +caption-stat-accumulator=[img=entity/accumulator] 蓄電池 +tooltip-stat-accumulator=使用中的蓄電池數量 / 儲存的蓄電池數量 +caption-stat-surface-area=[virtual-signal=signal-R] 餘下土地 +tooltip-stat-surface-area=餘下土地。 放入堆填來增加 +caption-stat-sustained=[virtual-signal=signal-S] 持續產能 +tooltip-stat-sustained=持續電力產能 +caption-stat-production=[virtual-signal=signal-P] 現時產能 +tooltip-stat-production=現時電力產能 +caption-stat-capacity=[virtual-signal=signal-E] 電池電量 +tooltip-stat-capacity=現時電池電量 / 電池最大容量 +type-energy=電力 +type-circuit=回路 +type-storage-input=放入箱 +type-storage-output=提取箱 +caption-refresh=刷新列表 +caption-view=查看介面 +caption-build=建造介面 +caption-remove=拆除介面 +message-located=__1__ 介面在 __2__ +message-built=__1__ 在 __3__ 建造了 __2__ 介面 +message-removed=__1__ 在 __3__ 拆除了 __2__ 介面 +message-no-chest=沒檢測到鋼箱 +message-chest-not-empty=鋼箱不是空的 +message-multiple=無法建造 __1__ 介面,檢測到多個鋼箱 +message-limit=無法建造 __1__ 介面,超出最大限制 +message-no-space=無法在此建造電力介面,空間不足 +message-research=你要研究 [technology=__1__] 在 __2__ 級 才可使用太空平台 vlayer 功能。 [exp_afk-kick] kick-message=因地圖中沒有活躍玩家,所以所有人都已被請離。 [exp_chat-auto-reply] chat-reply=[Compilatron] __1__ -chat-disallowed=你沒有權限使用這個指令。 +chat-disallowed=You can't use global chat commands reply-online=現在有 __1__ 人在線 reply-players=這張地圖上共有__1__名用戶 -reply-dev=Cooldude2606 只是本場境的開發者 +reply-dev=Cooldude2606 is a dev for this server and makes the scenario and is not a factorio dev. reply-softmod=這裹用了自設情境。 reply-clusterio=Clusterio is a factorio server hosting platform allowing internet enabled mods. reply-blame=責怪 __1__ 吧! @@ -498,23 +567,23 @@ 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-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-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-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-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-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-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-1= ☕ Boiling the water… ☕ reply-tea-2= ☕ __1__ your tea is done! ☕ reply-mead-1= Filling the drinking horn reply-mead-2= Skål! @@ -546,3 +615,22 @@ chat-jailed=__1__ 因被多次拆除受保護物體而被禁止行動。請等 [exp_report-jail] chat-jailed=__1__ 因被多次舉報而被禁止行動。請等候管理員作出下一步處理。 + + +[exp-gui_player-list] +main-tooltip = Player List +open-action-bar = Options +close-action-bar = Close Options +reason-confirm = Confirm Reason +reason-entry = Enter Reason +goto-player = Goto player +bring-player = Bring player +report-player = Report player +jail-player = Jail player +kick-player = Kick player +ban-player = Ban player +afk-time = __1__% of total map time\nLast moved __2__ ago +open-map = __1__ __2__\n__3__\nClick to open map + +[exp_spectate] +follow-label = Following __1__.\nClick here or press escape to stop following. \ No newline at end of file diff --git a/exp_scenario/test/controller.test.js b/exp_scenario/test/controller.test.js index d7b96add..097365ab 100644 --- a/exp_scenario/test/controller.test.js +++ b/exp_scenario/test/controller.test.js @@ -42,7 +42,6 @@ async function startPlugin(t2, { withPlugins = true } = {}) { for (const [name, Plugin] of [["exp_roles", RolesPlugin], ["exp_groups", GroupsPlugin]]) { const plugin = new Plugin({ name }, controller, undefined, logger); await plugin.init(); - controller.plugins.set(name, plugin); } } @@ -65,7 +64,7 @@ t.test("class ControllerPlugin", t2 => { t2.test(".handleSeedRequest() creates the roles and reuses them by name", async t3 => { const { plugin, controller } = await startPlugin(t3); - const rolesPlugin = controller.plugins.get("exp_roles"); + const rolesPlugin = RolesPlugin.get(controller); await plugin.handleSeedRequest(); t3.strictSame(controller.roles.size, seedRoles.length, "every seed role exists"); @@ -80,7 +79,7 @@ t.test("class ControllerPlugin", t2 => { t2.test(".handleSeedRequest() creates the groups and resets them by name", async t3 => { const { plugin, controller } = await startPlugin(t3); - const groupsPlugin = controller.plugins.get("exp_groups"); + const groupsPlugin = GroupsPlugin.get(controller); await plugin.handleSeedRequest(); t3.strictSame(groupsPlugin.groups.size, seedGroups.length, "every seed group exists"); @@ -101,7 +100,7 @@ t.test("class ControllerPlugin", t2 => { t2.test(".handleSeedRequest() maps each role onto its group by rank", async t3 => { const { plugin, controller } = await startPlugin(t3); - const groupsPlugin = controller.plugins.get("exp_groups"); + const groupsPlugin = GroupsPlugin.get(controller); await plugin.handleSeedRequest(); const mapped = seedRoles.filter(role => role.group !== undefined); @@ -123,7 +122,7 @@ t.test("class ControllerPlugin", t2 => { t2.test(".handleSeedRequest() reuses mappings and keeps clear of other mappings", async t3 => { const { plugin, controller } = await startPlugin(t3); - const groupsPlugin = controller.plugins.get("exp_groups"); + const groupsPlugin = GroupsPlugin.get(controller); groupsPlugin.roleMappings.set(new RoleMappingRecord(99, new Set([0, 1]), 5, 2, true)); await plugin.handleSeedRequest();