noita_entangled_worlds/quant.ew/files/system/item_sync.lua

562 lines
20 KiB
Lua
Raw Normal View History

2024-07-14 12:19:26 +03:00
-- Synchronizes item pickup and item drop
2024-08-07 17:11:55 +03:00
local inventory_helper = dofile_once("mods/quant.ew/files/core/inventory_helper.lua")
local ctx = dofile_once("mods/quant.ew/files/core/ctx.lua")
local net = dofile_once("mods/quant.ew/files/core/net.lua")
2024-05-13 13:07:28 +03:00
2024-11-18 07:18:35 -05:00
ModLuaFileAppend("data/scripts/items/utility_box.lua", "mods/quant.ew/files/resource/cbs/chest_sync.lua")
ModLuaFileAppend("data/scripts/items/chest_random.lua", "mods/quant.ew/files/resource/cbs/chest_sync.lua")
ModLuaFileAppend("data/scripts/items/chest_random_super.lua", "mods/quant.ew/files/resource/cbs/chest_sync.lua")
2024-06-20 15:53:01 +03:00
dofile_once("data/scripts/lib/coroutines.lua")
local rpc = net.new_rpc_namespace()
2024-05-13 13:07:28 +03:00
local item_sync = {}
local pending_remove = {}
local pickup_handlers = {}
2024-11-17 22:09:57 -05:00
local dead_entities = {}
2024-11-18 07:18:35 -05:00
function rpc.open_chest(gid)
local ent = item_sync.find_by_gid(gid)
2024-11-18 08:26:26 -05:00
local file
for _, com in ipairs(EntityGetComponent(ent, "LuaComponent")) do
local f = ComponentGetValue2(com, "script_item_picked_up")
if f ~= nil then
file = f
break
2024-11-18 07:18:35 -05:00
end
end
2024-11-18 08:26:26 -05:00
if file ~= nil then
EntityAddComponent2(ent, "LuaComponent", {
script_source_file = file,
execute_on_added = true,
call_init_function = true,
})
end
2024-11-18 07:18:35 -05:00
end
util.add_cross_call("ew_chest_opened", function(chest_id)
local gid = item_sync.get_global_item_id(chest_id)
if gid ~= "unknown" then
rpc.open_chest(gid)
end
end)
2024-11-17 22:09:57 -05:00
util.add_cross_call("ew_item_death_notify", function(enemy_id, responsible_id)
local player_data = player_fns.get_player_data_by_local_entity_id(responsible_id)
local responsible
if player_data ~= nil then
responsible = player_data.peer_id
else
responsible = responsible_id
end
table.insert(dead_entities, {item_sync.get_global_item_id(enemy_id), responsible})
end)
2024-05-13 16:19:28 +03:00
function item_sync.ensure_notify_component(ent)
local notify = EntityGetFirstComponentIncludingDisabled(ent, "LuaComponent", "ew_notify_component")
2024-08-30 11:12:10 +03:00
if notify == nil then
EntityAddComponent2(ent, "LuaComponent", {
_tags = "enabled_in_world,enabled_in_hand,enabled_in_inventory,ew_notify_component,ew_remove_on_send",
2024-08-30 11:12:10 +03:00
script_throw_item = "mods/quant.ew/files/resource/cbs/item_notify.lua",
script_item_picked_up = "mods/quant.ew/files/resource/cbs/item_notify.lua",
-- script_kick = "mods/quant.ew/files/resource/cbs/item_notify.lua",
})
end
2024-05-13 16:19:28 +03:00
end
local function mark_in_inventory(my_player)
local items = inventory_helper.get_all_inventory_items(my_player)
for _, ent in pairs(items) do
2024-11-16 20:42:23 -05:00
if not EntityHasTag(ent, "polymorphed_player") then
item_sync.ensure_notify_component(ent)
end
2024-05-13 13:07:28 +03:00
end
end
2024-05-13 15:18:52 +03:00
local function allocate_global_id()
local current = tonumber(GlobalsGetValue("ew_global_item_id", "1"))
2024-08-30 11:12:10 +03:00
GlobalsSetValue("ew_global_item_id", tostring(current + 1))
return ctx.my_player.peer_id .. ":" .. current
2024-05-13 15:18:52 +03:00
end
-- Try to guess if the item is in world.
2024-05-13 13:07:28 +03:00
local function is_item_on_ground(item)
2024-10-06 17:52:37 +03:00
local has_physics = EntityGetComponent(item, "SimplePhysicsComponent") ~= nil or EntityGetComponent(item, "PhysicsBodyComponent") ~= nil
if has_physics then
-- Has at least one of phys components enabled, definitely in world.
return true, "has physics"
end
-- What if it's a new wand?
local spec = EntityGetFirstComponent(item, "SpriteParticleEmitterComponent")
if spec == nil then
-- All disabled, certainly not in world.
return false, "no spec component"
end
-- Ones in starter wands don't have any tags, ukko rock and co do.
if ComponentHasTag(spec, "enabled_in_hand") then
return false, "spec tags enabled_in_hand"
end
if ComponentHasTag(spec, "enabled_in_inventory") then
return false, "spec tags enabled_in_inventory"
end
return true, "other "..spec
2024-05-13 13:07:28 +03:00
end
2024-05-13 15:18:52 +03:00
function item_sync.get_global_item_id(item)
local gid = EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_global_item_id")
if gid == nil then
2024-07-13 15:59:28 +03:00
return "unknown"
2024-05-13 15:18:52 +03:00
end
2024-07-13 15:59:28 +03:00
local ret = ComponentGetValue2(gid, "value_string")
return ret or "unknown"
2024-05-13 15:18:52 +03:00
end
function item_sync.remove_item_with_id(gid)
table.insert(pending_remove, gid)
end
2024-07-18 15:03:07 +03:00
local find_by_gid_cache = {}
function item_sync.find_by_gid(gid)
2024-07-18 15:03:07 +03:00
if find_by_gid_cache[gid] ~= nil and EntityGetIsAlive(find_by_gid_cache[gid]) then
return find_by_gid_cache[gid]
end
--print("find_by_gid: searching")
2024-07-18 15:03:07 +03:00
local global_items = EntityGetWithTag("ew_global_item")
for _, item in ipairs(global_items) do
local i_gid = item_sync.get_global_item_id(item)
2024-07-18 15:03:07 +03:00
find_by_gid_cache[i_gid] = item
if i_gid == gid then
return item
end
end
end
function item_sync.remove_item_with_id_now(gid)
2024-05-13 15:18:52 +03:00
local global_items = EntityGetWithTag("ew_global_item")
for _, item in ipairs(global_items) do
local i_gid = item_sync.get_global_item_id(item)
if i_gid == gid then
--TODO properly note when actually was from a peer picking up a potion maybe
for _, audio in ipairs(EntityGetComponent(item, "AudioComponent") or {}) do
if string.sub(ComponentGetValue2(audio, "event_root"), 1, 10) == "collision/" then
EntitySetComponentIsEnabled(item, audio, false)
end
end
2024-05-13 15:18:52 +03:00
EntityKill(item)
2024-11-16 20:42:23 -05:00
break
2024-05-13 15:18:52 +03:00
end
end
end
2024-05-13 16:19:28 +03:00
function item_sync.host_localize_item(gid, peer_id)
2024-05-13 16:52:29 +03:00
if ctx.item_prevent_localize[gid] then
print("Item localize for " .. gid .. " prevented")
return
2024-05-13 16:52:29 +03:00
end
ctx.item_prevent_localize[gid] = true
if table.contains(pending_remove, gid) then
print("Item localize prevented, already taken")
return
end
local item_ent_id = item_sync.find_by_gid(gid)
if item_ent_id ~= nil then
for _, handler in ipairs(pickup_handlers) do
handler(item_ent_id)
end
end
2024-05-13 15:18:52 +03:00
if peer_id ~= ctx.my_id then
2024-05-13 16:19:28 +03:00
item_sync.remove_item_with_id(gid)
2024-05-13 15:18:52 +03:00
end
rpc.item_localize(peer_id, gid)
2024-05-13 15:18:52 +03:00
end
function item_sync.make_item_global(item, instant, give_authority_to)
EntityAddTag(item, "ew_global_item")
2024-06-20 15:53:01 +03:00
async(function()
if not instant then
wait(1) -- Wait 1 frame so that game sets proper velocity.
end
if not EntityGetIsAlive(item) then
2024-09-09 06:32:47 -04:00
print("Thrown item vanished before we could send it")
return
end
item_sync.ensure_notify_component(item)
2024-08-30 11:12:10 +03:00
local gid_component = EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent",
2024-11-17 22:09:57 -05:00
"ew_global_item_id")
2024-08-20 09:42:47 -04:00
local gid
2024-07-13 15:59:28 +03:00
if gid_component == nil then
gid = allocate_global_id()
if give_authority_to ~= nil then
gid = give_authority_to..":"..gid
end
2024-06-20 15:53:01 +03:00
EntityAddComponent2(item, "VariableStorageComponent", {
_tags = "enabled_in_world,enabled_in_hand,enabled_in_inventory,ew_global_item_id",
2024-07-13 15:59:28 +03:00
value_string = gid,
2024-06-20 15:53:01 +03:00
})
2024-08-20 09:42:47 -04:00
else
gid = ComponentGetValue2(gid_component, "value_string")
2024-06-20 15:53:01 +03:00
end
--local vel = EntityGetFirstComponentIncludingDisabled(item, "VelocityComponent")
--if vel then
-- local vx, vy = ComponentGetValue2(vel, "mVelocity")
--end
2024-11-18 07:18:35 -05:00
2024-06-20 15:53:01 +03:00
local item_data = inventory_helper.serialize_single_item(item)
2024-07-13 15:59:28 +03:00
item_data.gid = gid
2024-11-18 07:18:35 -05:00
2024-11-17 22:09:57 -05:00
local hp, max_hp, has_hp = util.get_ent_health(item)
if has_hp then
util.ensure_component_present(item, "LuaComponent", "ew_item_death_notify", {
script_death = "mods/quant.ew/files/resource/cbs/item_death_notify.lua"
})
end
2024-07-13 15:59:28 +03:00
ctx.item_prevent_localize[gid] = false
rpc.item_globalize(item_data)
2024-06-20 15:53:01 +03:00
end)
2024-05-13 13:07:28 +03:00
end
local function get_global_ent(key)
local val = tonumber(GlobalsGetValue(key, "0"))
GlobalsSetValue(key, "0")
if val ~= 0 then
return val
end
end
2024-05-15 17:05:50 +03:00
local function remove_client_items_from_world()
if GameGetFrameNum() % 5 ~= 3 then
return
end
for _, item in ipairs(EntityGetWithTag("ew_client_item")) do
if is_item_on_ground(item) then
EntityKill(item)
end
end
end
local function is_my_item(gid)
return string.sub(gid, 1, 16) == ctx.my_id
end
2024-11-17 22:09:57 -05:00
rpc.opts_reliable()
function rpc.handle_death_data(death_data)
for _, remote_data in ipairs(death_data) do
local remote_id = remote_data[1]
--[[if confirmed_kills[remote_id] then
GamePrint("Remote id has been killed already..?")
goto continue
end
confirmed_kills[remote_id] = true]]
local responsible_entity = 0
local peer_data = player_fns.peer_get_player_data(remote_data[2], true)
if peer_data ~= nil then
responsible_entity = peer_data.entity
elseif ctx.entity_by_remote_id[remote_data[2]] ~= nil then
responsible_entity = ctx.entity_by_remote_id[remote_data[2]]
end
--if unsynced_enemys[remote_id] ~= nil then
--sync_enemy(unsynced_enemys[remote_id], true)
--end
local enemy_id = item_sync.find_by_gid(remote_id)
if enemy_id ~= nil and EntityGetIsAlive(enemy_id) then
local immortal = EntityGetFirstComponentIncludingDisabled(enemy_id, "LuaComponent", "ew_immortal")
if immortal ~= 0 then
EntityRemoveComponent(enemy_id, immortal)
end
local protection_component_id = GameGetGameEffect(enemy_id, "PROTECTION_ALL")
if protection_component_id ~= 0 then
EntitySetComponentIsEnabled(enemy_id, protection_component_id, false)
end
local damage_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
if damage_component and damage_component ~= 0 then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", false)
end
-- Enable explosion back
local expl_component = EntityGetFirstComponent(enemy_id, "ExplodeOnDamageComponent")
if expl_component ~= nil and expl_component ~= 0 then
ComponentSetValue2(expl_component, "explode_on_death_percent", 1)
end
local current_hp = util.get_ent_health(enemy_id)
local dmg = current_hp
if dmg > 0 then
EntityInflictDamage(enemy_id, dmg+0.1, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity)
end
EntityInflictDamage(enemy_id, 1000000000, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity) -- Just to be sure
EntityKill(enemy_id)
end
::continue::
end
end
2024-09-14 11:07:38 +03:00
local function send_item_positions()
local position_data = {}
for _, item in ipairs(EntityGetWithTag("ew_global_item")) do
local gid = item_sync.get_global_item_id(item)
-- Only send info about items created by us.
2024-09-21 12:39:18 +03:00
if is_my_item(gid) and is_item_on_ground(item) then
local x, y = EntityGetTransform(item)
2024-10-04 19:37:59 -04:00
local costcom = EntityGetFirstComponentIncludingDisabled(item, "ItemCostComponent")
local cost = 0
if costcom ~= nil then
cost = ComponentGetValue2(costcom, "cost")
2024-10-06 10:14:53 -04:00
local mx, my = GameGetCameraPos()
if math.abs(mx - x) < 1024 and math.abs(my - y) < 1024 and EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_try_stealable") then
ComponentSetValue2(costcom, "stealable", true)
end
2024-10-04 19:37:59 -04:00
end
2024-11-17 22:09:57 -05:00
local phys_info = util.get_phys_info(item, true)
if phys_info ~= nil then
position_data[gid] = {x, y, phys_info, cost}
end
end
2024-09-14 11:07:38 +03:00
end
rpc.update_positions(position_data)
2024-11-17 22:09:57 -05:00
if #dead_entities > 0 then
rpc.handle_death_data(dead_entities)
end
dead_entities = {}
2024-09-14 11:07:38 +03:00
end
2024-05-20 01:01:35 +03:00
function item_sync.on_world_update_host()
local my_player = ctx.my_player
2024-05-13 13:07:28 +03:00
if GameGetFrameNum() % 5 == 4 then
mark_in_inventory(my_player)
end
local thrown_item = get_global_ent("ew_thrown")
if thrown_item ~= nil then
item_sync.make_item_global(thrown_item)
2024-05-13 13:07:28 +03:00
end
2024-05-13 15:18:52 +03:00
local picked_item = get_global_ent("ew_picked")
if picked_item ~= nil and EntityHasTag(picked_item, "ew_global_item")
and EntityHasTag(EntityGetRootEntity(picked_item), "ew_peer") then
local gid = item_sync.get_global_item_id(picked_item)
item_sync.host_localize_item(gid, ctx.my_id)
2024-05-13 16:19:28 +03:00
end
2024-05-15 17:05:50 +03:00
remove_client_items_from_world()
2024-05-13 16:19:28 +03:00
end
2024-05-20 01:01:35 +03:00
function item_sync.on_world_update_client()
local my_player = ctx.my_player
2024-05-13 16:52:29 +03:00
if GameGetFrameNum() % 5 == 4 then
mark_in_inventory(my_player)
end
local thrown_item = get_global_ent("ew_thrown")
2024-05-15 17:05:50 +03:00
if thrown_item ~= nil and not EntityHasTag(thrown_item, "ew_client_item") then
item_sync.make_item_global(thrown_item)
2024-05-13 16:52:29 +03:00
end
2024-05-13 16:19:28 +03:00
local picked_item = get_global_ent("ew_picked")
if picked_item ~= nil and EntityHasTag(picked_item, "ew_global_item")
and EntityHasTag(EntityGetRootEntity(picked_item), "ew_peer") then
2024-05-13 16:19:28 +03:00
local gid = item_sync.get_global_item_id(picked_item)
rpc.item_localize_req(gid)
2024-05-13 15:18:52 +03:00
end
2024-05-15 17:05:50 +03:00
remove_client_items_from_world()
2024-05-13 13:07:28 +03:00
end
local function is_safe_to_remove()
return not ctx.is_wand_pickup
end
function item_sync.on_world_update()
-- TODO check that we not removing item we are going to pick now, instead of checking if picker gui is open.
if is_safe_to_remove() then
if #pending_remove > 0 then
local gid = table.remove(pending_remove)
item_sync.remove_item_with_id_now(gid)
end
end
if GameGetFrameNum() % 5 == 2 then
for _, ent in ipairs(EntityGetWithTag("mimic_potion")) do
2024-11-16 20:42:23 -05:00
if ctx.is_host and not EntityHasTag(ent, "polymorphed_player") then
if not EntityHasTag(ent, "ew_global_item") then
item_sync.make_item_global(ent)
end
end
end
end
2024-11-17 22:09:57 -05:00
if GameGetFrameNum() % 5 == 3 then
send_item_positions()
end
end
2024-07-18 15:03:07 +03:00
function item_sync.on_should_send_updates()
if not ctx.is_host then
return
end
local global_items = EntityGetWithTag("ew_global_item")
local item_list = {}
for _, item in ipairs(global_items) do
if is_item_on_ground(item) and not EntityHasTag(item, "mimic_potion") then
2024-07-18 15:03:07 +03:00
local item_data = inventory_helper.serialize_single_item(item)
local gid = item_sync.get_global_item_id(item)
item_data.gid = gid
table.insert(item_list, item_data)
end
end
rpc.initial_items(item_list)
end
2024-08-22 17:53:14 +03:00
function item_sync.on_draw_debug_window(imgui)
local mx, my = DEBUG_GetMouseWorld()
local ent = EntityGetClosestWithTag(mx, my, "ew_global_item")
if ent ~= nil and ent ~= 0 then
if imgui.CollapsingHeader("Item gid") then
local x, y = EntityGetTransform(ent)
GameCreateSpriteForXFrames("mods/quant.ew/files/resource/debug/marker.png", x, y, true, 0, 0, 1, true)
local gid = item_sync.get_global_item_id(ent)
2024-08-30 11:12:10 +03:00
imgui.Text("GID: " .. tostring(gid))
2024-08-22 17:53:14 +03:00
local prevented = ctx.item_prevent_localize[gid]
if prevented then
imgui.Text("Localize prevented")
else
imgui.Text("Localize allowed")
end
2024-10-06 17:52:37 +03:00
local on_ground, reason = is_item_on_ground(ent)
if on_ground then
imgui.Text("On ground: "..reason)
else
imgui.Text("Not on ground: "..reason)
end
2024-08-22 17:53:14 +03:00
end
end
end
local function add_stuff_to_globalized_item(item, gid)
2024-07-18 21:32:52 +03:00
EntityAddTag(item, "ew_global_item")
item_sync.ensure_notify_component(item)
-- GamePrint("Got global item: "..item)
local gid_c = EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_global_item_id")
if gid_c == nil then
EntityAddComponent2(item, "VariableStorageComponent", {
_tags = "ew_global_item_id",
value_string = gid
})
else
ComponentSetValue2(gid_c, "value_string", gid)
end
ctx.item_prevent_localize[gid] = false
end
2024-07-18 15:03:07 +03:00
rpc.opts_reliable()
function rpc.initial_items(item_list)
2024-07-18 15:31:36 +03:00
-- Only run once ever, as it tends to duplicate items otherwise
if GameHasFlagRun("ew_initial_items") then
return
end
GameAddFlagRun("ew_initial_items")
2024-07-18 15:03:07 +03:00
for _, item_data in ipairs(item_list) do
local item = item_sync.find_by_gid(item_data.gid)
if item == nil then
local item_new = inventory_helper.deserialize_single_item(item_data)
add_stuff_to_globalized_item(item_new, item_data.gid)
2024-07-18 15:03:07 +03:00
end
end
end
rpc.opts_reliable()
function rpc.item_globalize(item_data)
if is_safe_to_remove() then
item_sync.remove_item_with_id_now(item_data.gid)
end
2024-11-17 22:20:29 -05:00
if item_sync.find_by_gid(item_data.gid) ~= nil then
return
end
local item = inventory_helper.deserialize_single_item(item_data)
add_stuff_to_globalized_item(item, item_data.gid)
2024-11-02 21:48:32 -04:00
local coms = EntityGetComponent(item, "VariableStorageComponent")
for _, com in ipairs(coms) do
if ComponentGetValue2(com, "name") == "throw_time" then
ComponentSetValue2(com, "value_int", GameGetFrameNum())
end
end
2024-11-17 22:09:57 -05:00
EntityAddComponent2(item, "LuaComponent", {_tags="ew_immortal", script_damage_about_to_be_received = "mods/quant.ew/files/resource/cbs/immortal.lua"})
end
rpc.opts_reliable()
function rpc.item_localize(l_peer_id, item_id)
local item_ent_id = item_sync.find_by_gid(item_id)
if item_ent_id ~= nil then
for _, handler in ipairs(pickup_handlers) do
handler(item_ent_id)
end
end
if l_peer_id ~= ctx.my_id then
item_sync.remove_item_with_id(item_id)
end
end
rpc.opts_reliable()
function rpc.item_localize_req(gid)
if not ctx.is_host then
return
end
item_sync.host_localize_item(gid, ctx.rpc_peer_id)
end
2024-09-14 11:07:38 +03:00
function rpc.update_positions(position_data)
local LIMIT = 128 * 3
local cx, cy = GameGetCameraPos()
2024-09-14 11:07:38 +03:00
for gid, el in pairs(position_data) do
local x, y = el[1], el[2]
2024-11-17 22:09:57 -05:00
local phys_info = el[3]
local price = el[4]
if math.abs(x - cx) < LIMIT and math.abs(y - cy) < LIMIT then
local item = item_sync.find_by_gid(gid)
if item ~= nil then
2024-11-17 22:09:57 -05:00
if not util.set_phys_info(item, phys_info) then
EntitySetTransform(item, x, y)
end
2024-10-04 19:37:59 -04:00
local costcom = EntityGetFirstComponentIncludingDisabled(item, "ItemCostComponent")
if costcom ~= nil then
if price == 0 then
EntitySetComponentsWithTagEnabled(item, "shop_cost", false)
ComponentSetValue2(costcom, "cost", 0)
else
EntitySetComponentsWithTagEnabled(item, "shop_cost", true)
ComponentSetValue2(costcom, "cost", price)
end
2024-10-04 19:37:59 -04:00
end
else
util.log("Requesting again "..gid)
rpc.request_send_again(gid)
end
2024-09-14 11:07:38 +03:00
end
end
end
function rpc.request_send_again(gid)
if not is_my_item(gid) then
return
end
local item = item_sync.find_by_gid(gid)
if item == nil then
util.log("Requested to send item again, but this item wasn't found: "..gid)
return
end
item_sync.make_item_global(item)
end
2024-07-14 12:19:26 +03:00
ctx.cap.item_sync = {
globalize = item_sync.make_item_global,
register_pickup_handler = function(handler)
table.insert(pickup_handlers, handler)
end
2024-07-14 12:19:26 +03:00
}
item_sync.rpc = rpc
2024-09-09 06:32:47 -04:00
return item_sync