320 lines
11 KiB
Lua
320 lines
11 KiB
Lua
-- RowYourBoat/scripts/main.lua
|
|
local modBaseFormIDs = {
|
|
["boat"] = 0x0015a8,
|
|
["lamp_off"] = 0x007dbc,
|
|
["lamp_on"] = 0x007dbe,
|
|
["seat"] = 0x003ee1,
|
|
["chest"] = 0x005451,
|
|
["ladder"] = 0x006923,
|
|
}
|
|
local objectClasses = {
|
|
["boat"] = "BP_MS08Rowboat_C",
|
|
["lamp_off"] = "BP_ShipLamp01_C",
|
|
["lamp_on"] = "BP_ShipLamp300_C",
|
|
["seat"] = "BP_LCStool01F_C",
|
|
["chest"] = "BP_PCChestClutterLower01_C",
|
|
["ladder"] = "BP_RopeLadder01_C",
|
|
}
|
|
local objectClassToTypes = {}
|
|
for objectType, className in pairs(objectClasses) do
|
|
objectClassToTypes[className] = objectType
|
|
end
|
|
local objectPaths = {
|
|
["boat"] = "/Game/Forms/worldobjects/activator/BP_MS08Rowboat.BP_MS08Rowboat_C",
|
|
["lamp_off"] = "/Game/Forms/worldobjects/activator/BP_ShipLamp01.BP_ShipLamp01_C",
|
|
["lamp_on"] = "/Game/Forms/worldobjects/light/BP_ShipLamp300.BP_ShipLamp300_C",
|
|
["seat"] = "/Game/Forms/worldobjects/furniture/BP_LCStool01F.BP_LCStool01F_C",
|
|
["chest"] = "/Game/Forms/worldobjects/container/BP_PCChestClutterLower01.BP_PCChestClutterLower01_C",
|
|
["ladder"] = "/Game/Forms/worldobjects/door/BP_RopeLadder01.BP_RopeLadder01_C",
|
|
}
|
|
local destroyedObjects = {}
|
|
local primaryObjects = {}
|
|
local initialized = false
|
|
|
|
local function log(message)
|
|
print("[RowYourBoat] " .. message .. "\n")
|
|
end
|
|
|
|
-- Extract base ID (last 6 hex digits) from a full FormID
|
|
local function GetBaseID(formId)
|
|
if not formId then
|
|
return nil
|
|
end
|
|
-- Mask off the load order bytes (first 2 hex digits)
|
|
return formId & 0xFFFFFF
|
|
end
|
|
|
|
-- Get FormID from a boat
|
|
local function GetObjectFormID(object)
|
|
if not object or not object:IsValid() then
|
|
return nil
|
|
end
|
|
|
|
local refComponent = object.TESRefComponent
|
|
if not refComponent or not refComponent:IsValid() then
|
|
log("Object does not have a valid TESRefComponent")
|
|
return nil
|
|
end
|
|
|
|
return tonumber(refComponent.FormIDInstance)
|
|
end
|
|
|
|
-- Check if a object matches a specific base ID
|
|
local function MatchesBaseID(object, baseId, objectType)
|
|
if not baseId then
|
|
return false
|
|
end
|
|
|
|
local formId = GetObjectFormID(object)
|
|
if not formId then
|
|
return false
|
|
end
|
|
|
|
local objectBaseId = GetBaseID(formId)
|
|
log(string.format(string.upper(objectType) .. " Object FormID: %x, BaseID: %x == %x", formId, objectBaseId, baseId))
|
|
return objectBaseId == baseId
|
|
end
|
|
|
|
-- Check if a boat is our mod-added boat
|
|
local function IsModObject(object, objectType)
|
|
return MatchesBaseID(object, modBaseFormIDs[objectType], objectType)
|
|
end
|
|
|
|
-- Check if a actor object is valid and should be considered
|
|
local function IsActorValid(object)
|
|
if not object or not object:IsValid() then
|
|
return false
|
|
end
|
|
|
|
-- Check if we've already destroyed this object
|
|
local objectId = tostring(object)
|
|
if destroyedObjects[objectId] then
|
|
return false
|
|
end
|
|
|
|
-- Skip default objects
|
|
local fullName = object:GetFullName()
|
|
if string.find(fullName, "Default__") then
|
|
return false
|
|
end
|
|
|
|
if object.bHidden == true or object.bActorIsBeingDestroyed == true then
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Find all valid objects of type
|
|
local function FindAllObjects(objectType)
|
|
local objects = {}
|
|
local class = objectClasses[objectType]
|
|
local allObjects = FindAllOf(tostring(class))
|
|
|
|
if allObjects then
|
|
for i, object in pairs(allObjects) do
|
|
if IsActorValid(object) then
|
|
table.insert(objects, object)
|
|
end
|
|
end
|
|
end
|
|
|
|
return objects
|
|
end
|
|
|
|
-- Set or validate the primary object
|
|
local function SetPrimaryObject(objects, objectType)
|
|
local primaryObject = primaryObjects[objectType]
|
|
-- If we already have a primary object and it's still valid, keep it
|
|
if primaryObject and primaryObject:IsValid() and IsActorValid(primaryObject) then
|
|
log(string.upper(objectType) .. " Primary object already set and valid: " .. primaryObject:GetFullName())
|
|
return
|
|
end
|
|
|
|
-- Find the first valid mod object to be our primary
|
|
for _, object in ipairs(objects) do
|
|
primaryObjects[objectType] = object
|
|
log(string.upper(objectType) .. " Set primary mod object: " .. object:GetFullName())
|
|
break
|
|
end
|
|
end
|
|
|
|
local function DestroyObject(object, objectType)
|
|
if not object or not object:IsValid() then
|
|
return
|
|
end
|
|
|
|
local objectId = tostring(object)
|
|
local objectName = object:GetFullName()
|
|
|
|
-- Mark as destroyed first
|
|
destroyedObjects[objectId] = true
|
|
|
|
-- Hide and disable
|
|
local success = pcall(function()
|
|
object:SetActorHiddenInGame(true)
|
|
object:SetActorEnableCollision(false)
|
|
|
|
if object.SetActorTickEnabled then
|
|
object:SetActorTickEnabled(false)
|
|
end
|
|
|
|
object:K2_DestroyActor()
|
|
end)
|
|
|
|
if success then
|
|
log(string.upper(objectType) .. " Destroyed duplicate: " .. objectName)
|
|
else
|
|
log(string.upper(objectType) .. " Failed to destroy object: " .. objectName .. " - " .. tostring(object))
|
|
end
|
|
end
|
|
|
|
-- Clean up duplicate objects for type
|
|
local function CleanupDuplicatesOfType(objectType)
|
|
log(string.upper(objectType) .. " CleanupDuplicatesOfType")
|
|
local objects = FindAllObjects(objectType)
|
|
log(string.upper(objectType) .. " Found " .. #objects .. " objects of type")
|
|
|
|
-- Count and collect mod objects
|
|
local modObjects = {}
|
|
for _, object in ipairs(objects) do
|
|
if IsModObject(object, objectType) then
|
|
table.insert(modObjects, object)
|
|
end
|
|
end
|
|
log(string.upper(objectType) .. " Found " .. #modObjects .. " mod objects")
|
|
|
|
-- Set or validate primary object
|
|
SetPrimaryObject(modObjects, objectType)
|
|
|
|
-- If we have no duplicates, nothing to do
|
|
if #modObjects <= 1 then
|
|
if #modObjects == 0 then
|
|
log(string.upper(objectType) .. " No mod objects found, nulling primary object")
|
|
primaryObjects[objectType] = nil -- Reset if no mod boats exist
|
|
end
|
|
return
|
|
end
|
|
|
|
log(string.upper(objectType) .. " Found " .. #modObjects .. " mod objects, deleting primary one")
|
|
|
|
-- Delete all mod objects except the primary one
|
|
local removedCount = 0
|
|
local primaryModObject = primaryObjects[objectType]
|
|
if primaryModObject and primaryModObject:IsValid() then
|
|
for _, object in ipairs(modObjects) do
|
|
log(string.upper(objectType) .. " Object name: " .. object:GetFullName() .. " primaryObject name: " .. (primaryModObject and primaryModObject:GetFullName()) or "nil")
|
|
if object:GetFullName() == primaryModObject:GetFullName() then
|
|
DestroyObject(object, objectType)
|
|
removedCount = removedCount + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
if removedCount > 0 then
|
|
log(string.upper(objectType) .. " Cleaned up " .. removedCount .. " duplicate object(s)")
|
|
end
|
|
end
|
|
|
|
-- Clean up duplicate objects
|
|
local function CleanupDuplicates()
|
|
for objectType, _ in pairs(objectClasses) do
|
|
CleanupDuplicatesOfType(objectType)
|
|
end
|
|
end
|
|
|
|
local function detectAndDeleteDuplicateObject(object, objectType)
|
|
local attempts = 0
|
|
local maxAttempts = 100 -- 100ms total timeout
|
|
|
|
if not object or not object:IsValid() then
|
|
log(string.upper(objectType) .. " Object is not valid, skipping")
|
|
return false -- Skip invalid objects
|
|
end
|
|
|
|
local fullName = object:GetFullName()
|
|
if string.find(fullName, "Default__") or string.find(fullName, "_Generated_") then
|
|
log(string.upper(objectType) .. " Skipping default or generated object: " .. fullName)
|
|
return false -- Skip default or generated objects which are guaranteed to not be what we are looking for
|
|
end
|
|
|
|
local function waitForFormId()
|
|
local formId = GetObjectFormID(object)
|
|
if formId and formId ~= 0 then
|
|
log(string.upper(objectType) .. " Found object FormID in attempts: " .. attempts)
|
|
if IsModObject(object, objectType) then
|
|
log(string.upper(objectType) .. " New mod object detected: " .. object:GetFullName())
|
|
local primaryObject = primaryObjects[objectType]
|
|
if primaryObject and primaryObject:IsValid() and IsActorValid(primaryObject) then
|
|
log(string.upper(objectType) .. " Delete old primary object: " .. primaryObject:GetFullName())
|
|
DestroyObject(primaryObject, objectType)
|
|
log(string.upper(objectType) .. " Setting new primary object: " .. object:GetFullName())
|
|
primaryObjects[objectType] = object
|
|
else
|
|
log(string.upper(objectType) .. " No current primary object, setting primary: " .. object:GetFullName())
|
|
primaryObjects[objectType] = object
|
|
end
|
|
end
|
|
elseif attempts < maxAttempts and object and object:IsValid() then
|
|
attempts = attempts + 1
|
|
ExecuteWithDelay(1, waitForFormId) -- Check again after 1ms
|
|
else
|
|
log(string.upper(objectType) .. " Failed to get FormID for new object after 100 attempts")
|
|
end
|
|
end
|
|
waitForFormId()
|
|
end
|
|
|
|
NotifyOnNewObject("/Script/Engine.Actor", function(object)
|
|
if not object then
|
|
return
|
|
end
|
|
local objectName
|
|
local success = pcall(function()
|
|
-- Check if object has IsValid method
|
|
if object.IsValid and not object:IsValid() then
|
|
return
|
|
end
|
|
|
|
-- Try to get the name
|
|
if type(object.GetFullName) == "function" then
|
|
objectName = object:GetFullName()
|
|
else
|
|
-- Fallback to string representation
|
|
objectName = tostring(object)
|
|
end
|
|
end)
|
|
|
|
if not success or type(objectName) ~= "string" then
|
|
return
|
|
end
|
|
|
|
local className = string.match(objectName, "^([^%s]+)")
|
|
if not className then
|
|
return
|
|
end
|
|
|
|
local objectType = objectClassToTypes[className]
|
|
if objectType then
|
|
log(string.upper(objectType) .. " Detected spawned object: " .. objectName)
|
|
detectAndDeleteDuplicateObject(object, objectType)
|
|
end
|
|
end)
|
|
|
|
-- -- Manual cleanup hotkey
|
|
-- RegisterKeyBind(Key.K, {ModifierKey.CONTROL}, function()
|
|
-- log("Manual cleanup triggered")
|
|
-- CleanupDuplicates()
|
|
-- end)
|
|
|
|
-- -- Debug key to reset primary boat
|
|
-- RegisterKeyBind(Key.K, {ModifierKey.ALT}, function()
|
|
-- log("Resetting primary boat")
|
|
-- primaryObjects = {}
|
|
-- destroyedObjects = {}
|
|
-- CleanupDuplicates()
|
|
-- end)
|
|
|
|
log("Script loaded!")
|
|
-- log(" CTRL-K = Manual cleanup (with debug info)")
|
|
-- log(" ALT-K = Reset primary boat selection") |