350 lines
15 KiB
Lua
350 lines
15 KiB
Lua
-------------------------------------------------------------------------------
|
|
-- [Picker Dollies]--
|
|
-------------------------------------------------------------------------------
|
|
local Event = require("__stdlib__/stdlib/event/event").set_protected_mode(true)
|
|
local Area = require("__stdlib__/stdlib/area/area")
|
|
local Position = require("__stdlib__/stdlib/area/position")
|
|
local Direction = require("__stdlib__/stdlib/area/direction")
|
|
local Time = require("__stdlib__/stdlib/utils/defines/time")
|
|
local Player = require("__stdlib__/stdlib/event/player").register_events(true)
|
|
require("interface")
|
|
assert(remote.interfaces[script.mod_name]["dolly_moved_entity_id"])
|
|
Event.generate_event_name("dolly_moved")
|
|
|
|
local blacklist_cheat_types = {
|
|
["character"] = true,
|
|
["unit"] = true,
|
|
["unit-spawner"] = true,
|
|
["car"] = true,
|
|
["spider-vehicle"] = true
|
|
}
|
|
|
|
local blacklist_types = {
|
|
["item-request-proxy"] = true,
|
|
["rocket-silo-rocket"] = true,
|
|
["resource"] = true,
|
|
["construction-robot"] = true,
|
|
["logistic-robot"] = true,
|
|
["rocket"] = true,
|
|
["tile-ghost"] = true,
|
|
["item-entity"] = true,
|
|
["straight-rail"] = true,
|
|
["curved-rail"] = true,
|
|
["locomotive"] = true,
|
|
["cargo-wagon"] = true,
|
|
["artillery-wagon"] = true,
|
|
["fluid-wagon"] = true,
|
|
}
|
|
|
|
local blacklist_names = { ["pumpjack"] = true }
|
|
local oblong_names = { ["arithmetic-combinator"] = true, ["decider-combinator"] = true, ["pump"] = true }
|
|
local copper_wire_types = { ["electric-pole"] = true, ["power-switch"] = true }
|
|
|
|
local input_to_direction = {
|
|
["dolly-move-north"] = defines.direction.north,
|
|
["dolly-move-east"] = defines.direction.east,
|
|
["dolly-move-south"] = defines.direction.south,
|
|
["dolly-move-west"] = defines.direction.west
|
|
}
|
|
|
|
local oblong_diags = {
|
|
[defines.direction.north] = defines.direction.northeast,
|
|
[defines.direction.south] = defines.direction.northeast,
|
|
[defines.direction.west] = defines.direction.southwest,
|
|
[defines.direction.east] = defines.direction.southwest
|
|
}
|
|
|
|
--- @param t table
|
|
--- @return table
|
|
local function table_copy(t)
|
|
local t2 = {}
|
|
for k, v in pairs(t) do t2[k] = v end
|
|
return t2
|
|
end
|
|
|
|
--- @param player LuaPlayer
|
|
--- @param position MapPosition
|
|
--- @param silent? boolean
|
|
local function flying_text(player, text, position, silent)
|
|
player.create_local_flying_text { text = text, position = position }
|
|
if not silent then player.play_sound { path = "utility/cannot_build", position = player.position, volume = 1 } end
|
|
end
|
|
|
|
--- @param entity LuaEntity
|
|
--- @param cheat_mode? boolean
|
|
--- @return boolean
|
|
local function is_blacklisted(entity, cheat_mode)
|
|
local listed = blacklist_types[entity.type] or global.blacklist_names[entity.name]
|
|
if cheat_mode then return listed end
|
|
return listed or blacklist_cheat_types[entity.type]
|
|
end
|
|
|
|
--- @param pdata PickerDollies.pdata
|
|
--- @param entity LuaEntity
|
|
--- @param tick uint
|
|
--- @param save_time uint
|
|
local function save_entity(pdata, entity, tick, save_time)
|
|
if save_time == 0 then return end
|
|
pdata.dolly = entity
|
|
pdata.dolly_tick = tick
|
|
end
|
|
|
|
--- @param player LuaPlayer
|
|
--- @param pdata PickerDollies.pdata
|
|
--- @param tick uint
|
|
--- @param save_time uint
|
|
--- @return LuaEntity|nil
|
|
local function get_saved_entity(player, pdata, tick, save_time)
|
|
if save_time == 0 then return player.selected end
|
|
|
|
if pdata.dolly and (not pdata.dolly.valid or tick > (pdata.dolly_tick + Time.second * save_time)) then pdata.dolly = nil end
|
|
|
|
local selected = player.selected
|
|
if selected then
|
|
if pdata.dolly and blacklist_types[selected.type] then
|
|
return pdata.dolly
|
|
end
|
|
return selected
|
|
end
|
|
return pdata.dolly
|
|
end
|
|
|
|
--- Returns true if the wires can reach
|
|
--- @param entity LuaEntity
|
|
--- @return boolean
|
|
local function can_wires_reach(entity)
|
|
local neighbours = copper_wire_types[entity.type] and entity.neighbours or entity.circuit_connected_entities
|
|
for _, wire_type in pairs(neighbours) do
|
|
for _, neighbour in pairs(wire_type) do
|
|
if not entity.can_wires_reach(neighbour) then return false end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
--- @param event EventData.PickerDollies.CustomInputEvent
|
|
local function move_entity(event)
|
|
---@type LuaPlayer?, PickerDollies.pdata
|
|
local player, pdata = game.get_player(event.player_index), Player.pdata(event.player_index)
|
|
if not player then return end
|
|
|
|
local save_time = event.save_time or player.mod_settings["dolly-save-entity"].value --[[@as uint]]
|
|
local entity = get_saved_entity(player, pdata, event.tick, save_time)
|
|
if entity then
|
|
local cheat_mode = player.cheat_mode
|
|
|
|
if not (cheat_mode or player.can_reach_entity(entity)) then
|
|
return flying_text(player, { "cant-reach" }, entity.position)
|
|
end
|
|
|
|
if is_blacklisted(entity, cheat_mode) then
|
|
local text = { "picker-dollies.cant-be-teleported", entity.localised_name }
|
|
return flying_text(player, text, entity.position)
|
|
end
|
|
|
|
local entity_force = entity.force --[[@as LuaForce]]
|
|
if not (cheat_mode or entity_force == player.force) then
|
|
local text = { "picker-dollies.wrong-force", entity.localised_name }
|
|
return flying_text(player, text, entity.position)
|
|
end
|
|
|
|
local surface = entity.surface
|
|
local start_pos = Position(event.start_pos or entity.position) -- Where we started from in case we have to return it
|
|
if surface.find_entity("rocket-silo-rocket", start_pos) then
|
|
return flying_text(player, { "picker-dollies.rocket-present", entity.localised_name }, start_pos)
|
|
end
|
|
|
|
local prototype = entity.prototype
|
|
local direction = event.direction or input_to_direction[event.input_name] -- Direction to move the source
|
|
local distance = (event.distance or 1) * prototype.building_grid_bit_shift -- Distance to move the source, defaults to 1
|
|
local target_direction = event.target_direction or entity.direction
|
|
local target_pos = start_pos:translate(direction, distance) -- Where we want to go too
|
|
local target_box = Area(entity.selection_box):translate(direction, distance) -- Target selection box location
|
|
local out_of_the_way = start_pos:translate(Direction.opposite_direction(direction), event.tiles_away or 20)
|
|
local final_teleportation = false -- Handling teleportion after an entity has been moved into place and checked again
|
|
|
|
-- Store and clear fluids
|
|
local fluidbox = {} ---@type Fluid
|
|
for i = 1, #entity.fluidbox do fluidbox[i] = entity.fluidbox[i] end
|
|
if entity.get_fluid_count() > 0 then entity.clear_fluid_inside() end
|
|
|
|
-- Try retries times to teleport the entity out of the way.
|
|
---@api can_be_teleported
|
|
local retries = 5
|
|
while not entity.teleport(out_of_the_way) do
|
|
if retries <= 1 then
|
|
return flying_text(player, { "picker-dollies.cant-be-teleported", entity.localised_name }, entity.position)
|
|
end
|
|
retries = retries - 1
|
|
out_of_the_way = out_of_the_way:add { x = retries, y = retries }
|
|
end
|
|
|
|
-- Entity was teleportable and is out of the way, Check to see if it fits in the new spot
|
|
if target_direction then entity.direction = target_direction end -- Rotation for oblong
|
|
save_entity(pdata, entity, event.tick, save_time)
|
|
|
|
--- Update everything after teleporting
|
|
--- @param pos MapPosition
|
|
--- @param raise boolean Teleportation was successfull raise event
|
|
--- @param reason? LocalisedString
|
|
local function teleport_and_update(pos, raise, reason)
|
|
if entity.last_user then entity.last_user = player end
|
|
|
|
-- Final teleport into position. Ignore final_teleportation if we are not raising
|
|
if not (raise and final_teleportation) then
|
|
if event.start_direction then
|
|
entity.direction = event.start_direction
|
|
end
|
|
entity.teleport(pos)
|
|
end
|
|
|
|
-- Insert fluid back here.
|
|
for i = 1, #fluidbox do entity.fluidbox[i] = fluidbox[i] end
|
|
|
|
if not raise then return flying_text(player, reason, pos) end
|
|
|
|
-- At this point the entity should be able to be teleported into a new position.
|
|
-- Hoover up items, Move the proxy, Update any connections, Raise the dolly_moved event.
|
|
|
|
-- Mine or move out of the way any items on the ground
|
|
local items_on_ground = surface.find_entities_filtered { type = "item-entity", area = target_box }
|
|
for _, item_entity in pairs(items_on_ground) do
|
|
if item_entity.valid and not player.mine_entity(item_entity) then
|
|
local item_pos = item_entity.position
|
|
-- local valid_pos = surface.find_non_colliding_position("item-on-ground", item_pos, 0, .20) or item_pos
|
|
item_entity.teleport(item_pos)
|
|
end
|
|
end
|
|
|
|
-- Move the proxy to the correct position
|
|
local proxy = surface.find_entity("item-request-proxy", start_pos)
|
|
if proxy and proxy.valid then proxy.teleport(entity.position) end
|
|
|
|
---@todo Move any rocket-silo-rockets instead of blocking
|
|
|
|
-- Update all connections
|
|
local updateable_entities = surface.find_entities_filtered { area = target_box:expand(32), force = entity_force }
|
|
for _, updateable in pairs(updateable_entities) do updateable.update_connections() end
|
|
|
|
--- @type EventData.PickerDollies.dolly_moved_event
|
|
local event_data = { player_index = player.index, moved_entity = entity, start_pos = start_pos }
|
|
script.raise_event(Event.generate_event_name("dolly_moved"), event_data)
|
|
player.play_sound { path = "utility/rotated_medium" }
|
|
end
|
|
|
|
local can_place_params = {
|
|
name = entity.name == "entity-ghost" and entity.ghost_name or entity.name,
|
|
position = target_pos,
|
|
direction = target_direction,
|
|
force = entity_force,
|
|
build_check_type = defines.build_check_type.blueprint_ghost,
|
|
inner_name = entity.name == "entity-ghost" and entity.ghost_name
|
|
}
|
|
|
|
---@todo Check for ghosts marked for deconstruction
|
|
local allow_collisions = settings.global["dolly-allow-ignore-collisions"].value
|
|
if not (allow_collisions and player.mod_settings["dolly-ignore-collisions"].value) and
|
|
not (surface.can_place_entity(can_place_params) and not surface.find_entity("entity-ghost", target_pos)) then
|
|
return teleport_and_update(start_pos, false, { "picker-dollies.no-room", entity.localised_name })
|
|
end
|
|
|
|
-- Check if all the wires can reach.
|
|
if entity.circuit_connected_entities then
|
|
if not final_teleportation then entity.teleport(target_pos) end
|
|
final_teleportation = true
|
|
if not can_wires_reach(entity) then return teleport_and_update(start_pos, false, { "picker-dollies.wires-maxed" }) end
|
|
end
|
|
|
|
if entity.type == "mining-drill" then
|
|
if not final_teleportation then entity.teleport(target_pos) end
|
|
final_teleportation = true
|
|
local area = target_pos:expand_to_area(prototype.mining_drill_radius) --[[@as BoundingBox]]
|
|
local resource_name = entity.mining_target and entity.mining_target.name or nil
|
|
local count = entity.surface.count_entities_filtered { area = area, type = "resource", name = resource_name }
|
|
if count == 0 then
|
|
return teleport_and_update(start_pos, false, { "picker-dollies.off-ore-patch", entity.localised_name, resource_name })
|
|
end
|
|
end
|
|
|
|
return teleport_and_update(target_pos, true)
|
|
end
|
|
end
|
|
Event.register({ "dolly-move-north", "dolly-move-east", "dolly-move-south", "dolly-move-west" }, move_entity)
|
|
|
|
--- @param event EventData.PickerDollies.CustomInputEvent
|
|
local function try_rotate_oblong_entity(event)
|
|
---@type LuaPlayer?, PickerDollies.pdata
|
|
local player, pdata = game.get_player(event.player_index), Player.pdata(event.player_index)
|
|
if not player or (player and (player.cursor_stack.valid_for_read or player.cursor_ghost)) then return end
|
|
|
|
local save_time = player.mod_settings["dolly-save-entity"].value --[[@as uint]]
|
|
local entity = get_saved_entity(player, pdata, event.tick, save_time)
|
|
if not entity then return end
|
|
if not (global.oblong_names[entity.name] and not is_blacklisted(entity)) then return end
|
|
if not (player.cheat_mode or player.can_reach_entity(entity)) then return end
|
|
|
|
save_entity(pdata, entity, event.tick, save_time)
|
|
event.save_time = save_time
|
|
event.start_pos = entity.position
|
|
event.start_direction = entity.direction -- store the direction for later failed teleportation will need to restore it.
|
|
event.target_direction = Direction.next_direction(entity.direction) --[[@as defines.direction]]
|
|
event.distance = .5
|
|
event.direction = oblong_diags[event.target_direction] -- Set the translation direction to a diagonal.
|
|
move_entity(event)
|
|
end
|
|
Event.register("dolly-rotate-rectangle", try_rotate_oblong_entity)
|
|
|
|
--- @param event EventData.PickerDollies.CustomInputEvent
|
|
local function rotate_saved_dolly(event)
|
|
---@type LuaPlayer?, PickerDollies.pdata
|
|
local player, pdata = game.get_player(event.player_index), Player.pdata(event.player_index) ---@cast player -?
|
|
if player.cursor_stack.valid_for_read or player.cursor_ghost or player.selected then return end
|
|
|
|
local save_time = player.mod_settings["dolly-save-entity"].value --[[@as uint]]
|
|
local entity = get_saved_entity(player, pdata, event.tick, save_time)
|
|
if entity and entity.supports_direction then
|
|
save_entity(pdata, entity, event.tick, save_time)
|
|
entity.rotate { reverse = event.input_name == "dolly-rotate-saved-reverse", by_player = player }
|
|
end
|
|
end
|
|
Event.register({ "dolly-rotate-saved", "dolly-rotate-saved-reverse" }, rotate_saved_dolly)
|
|
|
|
local function on_init()
|
|
global.blacklist_names = table_copy(blacklist_names)
|
|
global.oblong_names = table_copy(oblong_names)
|
|
end
|
|
Event.register(Event.core_events.on_init, on_init)
|
|
|
|
local function on_configuration_changed()
|
|
-- Make sure the blacklists exist.
|
|
global.blacklist_names = global.blacklist_names or table_copy(blacklist_names)
|
|
global.oblong_names = global.oblong_names or table_copy(oblong_names)
|
|
|
|
-- Remove any invalid prototypes from the blacklists.
|
|
for name in pairs(global.blacklist_names) do
|
|
if not game.entity_prototypes[name] then global.blacklist_names[name] = nil end
|
|
end
|
|
for name in pairs(global.oblong_names) do
|
|
if not game.entity_prototypes[name] then global.oblong_names[name] = nil end
|
|
end
|
|
end
|
|
Event.register(Event.core_events.on_configuration_changed, on_configuration_changed)
|
|
|
|
--- @class PickerDollies.global
|
|
--- @field players {[uint]: PickerDollies.pdata}
|
|
--- @field blacklist_names {[string]: true}
|
|
--- @field oblong_names {[string]: true}
|
|
|
|
--- @class PickerDollies.pdata
|
|
--- @field dolly_tick uint
|
|
--- @field dolly LuaEntity|nil
|
|
|
|
--- @class EventData.PickerDollies.CustomInputEvent: EventData.CustomInputEvent
|
|
--- @field direction defines.direction
|
|
--- @field distance number
|
|
--- @field tiles_away uint
|
|
--- @field start_pos MapPosition
|
|
--- @field start_direction defines.direction|nil
|
|
--- @field target_direction defines.direction|nil
|
|
--- @field save_time uint|nil
|