From eb8cd50adab49e296c18c70d537166651265aa41 Mon Sep 17 00:00:00 2001 From: Tyler Hallada Date: Wed, 18 Jun 2025 00:31:15 -0400 Subject: [PATCH] Fix boat duplication/disappearing, reduce collision false-positives Needed to add a UE4SS script to fix the boat duplication issue. Disappearing issue was fixed by disabling and re-enabling when getting back to the boat. Collision false-positives was reduced by ignoring collider position when it is too far off expected X and Y positions. Still not ready for next relase, needs some update migration code. --- ResultScripts/RYBQuestStage3InitCollider.psc | 1 + ResultScripts/RYBQuestStage50DisableRefs.psc | 8 + ResultScripts/RYBQuestStage51EnableRefs.psc | 17 ++ RowYourBoat.esp | Bin 162927 -> 165083 bytes Scripts/RYBQuestScript.psc | 80 ++--- UE4SSScripts/FixRowboatDuplication.lua | 294 +++++++++++++++++++ 6 files changed, 360 insertions(+), 40 deletions(-) create mode 100644 ResultScripts/RYBQuestStage50DisableRefs.psc create mode 100644 ResultScripts/RYBQuestStage51EnableRefs.psc create mode 100644 UE4SSScripts/FixRowboatDuplication.lua diff --git a/ResultScripts/RYBQuestStage3InitCollider.psc b/ResultScripts/RYBQuestStage3InitCollider.psc index 5d2a621..f0be91a 100644 --- a/ResultScripts/RYBQuestStage3InitCollider.psc +++ b/ResultScripts/RYBQuestStage3InitCollider.psc @@ -5,6 +5,7 @@ set RYB.ColliderOffset to 300 set RYB.ColliderOffsetReverse to 350 set RYB.ColliderMoveFreq to 0.05 set RYB.ColliderZ to 1 +set RYB.ColliderPosThreshold to 1.0 RYBColliderRef.SetActorAlpha 0.0 RYBColliderRef.SetActorRefraction 10.0 RYBColliderRef.AddSpell MG14JskarInvis diff --git a/ResultScripts/RYBQuestStage50DisableRefs.psc b/ResultScripts/RYBQuestStage50DisableRefs.psc new file mode 100644 index 0000000..f7eaf94 --- /dev/null +++ b/ResultScripts/RYBQuestStage50DisableRefs.psc @@ -0,0 +1,8 @@ +; RYB Stage 50 Disable Boat and Attachment Refs + +RYBBoatRef.Disable +RYBSeatRef.Disable +RYBChestRef.Disable +RYBLampOnRef.Disable +RYBLampOffRef.Disable +RYBLadderRef.Disable \ No newline at end of file diff --git a/ResultScripts/RYBQuestStage51EnableRefs.psc b/ResultScripts/RYBQuestStage51EnableRefs.psc new file mode 100644 index 0000000..f3c0c5c --- /dev/null +++ b/ResultScripts/RYBQuestStage51EnableRefs.psc @@ -0,0 +1,17 @@ +; RYB Stage 51 Enable Boat and Attachment Refs + +RYBBoatRef.Enable +RYBBoatMapMarker.Enable +RYBSeatRef.Enable +if (RYB.ChestPurchased == 1) + RYBChestRef.Enable +endif +if (RYB.LampPurchased == 1) + if (RYB.LampOn == 1) + RYBLampOnRef.Enable + endif + RYBLampOffRef.Enable +endif +if (RYB.LadderPurchased == 1 && RYB.LadderDeployed == 1) + RYBLadderRef.Enable +endif \ No newline at end of file diff --git a/RowYourBoat.esp b/RowYourBoat.esp index 8f87f663face78b5f09715ec7fd039864867cf34..23a957397e040e629b26de82c3c7c9710dd47334 100644 GIT binary patch delta 6334 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 delta 4131 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 diff --git a/Scripts/RYBQuestScript.psc b/Scripts/RYBQuestScript.psc index 4f17bdc..40cb12e 100644 --- a/Scripts/RYBQuestScript.psc +++ b/Scripts/RYBQuestScript.psc @@ -14,7 +14,10 @@ short Initializing short BoatMoving ; 0 = not moving, 1 = about to move this tick, 2 = in motion short Rowing ; 0 = not rowing (timed), 1 = rowing forward (timed), 2 = rowing backward (timed) short AutoRowing ; 0 = not auto rowing, 1 = auto rowing forward, 2 = auto rowing backward -short Resetting ; 1 = run positioning code once relative to player, 2 = run positioning code once relative to boat +; 1 = run positioning code once relative to player +; 2 = run positioning code once relative to boat +; -1 = re-enable boat and attachments, then run positioning code once relative to player +short Resetting short LockHeading ; 0 = free to turn, 1 = locked to current boat heading short LadderDeployed ; 0 = disabled, 1 = enabled short LampOn @@ -28,6 +31,7 @@ short BoatPurchased short ChestPurchased short LampPurchased short LadderPurchased +short PlayerNearBoat ; 0 = not near boat, 1 = near boat (within RockDistanceThreshold) ; Triggers ; Sort of like functions that any script can call by setting these to 1 which this script will handle and set back to 0 @@ -276,7 +280,9 @@ float LadderZOffset ; pos units from BoatZ upwards to place the ladder (default: ref Collider float ColliderX +float ColliderActualX float ColliderY +float ColliderActualY float ColliderZ float ColliderOffset ; pos units from boat center towards boat direction to place the collider float ColliderOffsetReverse ; pos units from boat center towards the back of the boat to place the collider @@ -285,6 +291,8 @@ float ColliderMoveTimer ; current time left to wait before moving the collider a float CollisionDetectDelay ; how long to wait in seconds at boat startup before checking for collision (default: 2) float CollisionDetectTimer ; current time left to give for moving the boat away at startup before detecting collision short CollisionDetectZThreshold ; Z position that the collider must be above to be considered colliding with something +; how close (in units) the collider must be to the expected position to trigger a collision (default: 1.0) +float ColliderPosThreshold begin GameMode if (Initializing == 0) @@ -301,6 +309,11 @@ begin GameMode SetStage RYB 7 ; init player weight endif + if (Resetting == -1) + SetStage RYB 51 ; Re-enable boat and attachment refs + set Resetting to 1 + endif + set PlayerDistance to BoatRef.GetDistance Player if (PlayerDistance < RockDistanceThreshold) if (RockingEnabled == 1) @@ -308,8 +321,17 @@ begin GameMode else set fQuestDelayTime to MediumUpdateRate ; medium processing rate when nearby but rocking is disabled endif + if (PlayerNearBoat == 0) + ; Fix bug with boat disappearing after returning to the boat's cell by disabling all refs and re-enable in + ; the next frame + SetStage RYB 50 ; Disable boat and attachment refs + set Resetting to -1 ; re-enable boat and attachment refs next frame + endif + set PlayerNearBoat to 1 else set fQuestDelayTime to LowUpdateRate ; low processing rate when far away + set PlayerNearBoat to 0 + ; SetStage RYB 50 ; Disable boat and attachment refs when away endif if (TriggerAutoRow == 1) @@ -508,13 +530,7 @@ begin GameMode set BoatY to PlayerY + (PlayerCos * SummonDistance) set BoatZ to PlayerZ ; Disable all the refs first since they need to be disabled and enabled one frame later to appear correctly - BoatRef.Disable - BoatMarker.Disable - Seat.Disable - Chest.Disable - LampLit.Disable - LampUnlit.Disable - Ladder.Disable + SetStage RYB 50 ; Disable boat and attachment refs BoatRef.MoveTo Player BoatRef.SetPos x, BoatX BoatRef.SetPos y, BoatY @@ -535,13 +551,7 @@ begin GameMode if (BoatZ < (WaterLevelZ + 1)) set BoatZ to WaterLevelZ + 1 endif - BoatRef.Disable - BoatMarker.Disable - Seat.Disable - Chest.Disable - LampLit.Disable - LampUnlit.Disable - Ladder.Disable + SetStage RYB 50 ; Disable boat and attachment refs BoatRef.MoveTo Player BoatRef.SetPos x, BoatX BoatRef.SetPos y, BoatY @@ -554,32 +564,18 @@ begin GameMode ; set SummonTimer to SummonTimerDelay ; this delay maybe not necessary elseif (Summoning == 2) set Summoning to 0 - BoatRef.Enable - BoatMarker.Enable - Seat.Enable - if (ChestPurchased == 1) - Chest.Enable - endif - if (LampPurchased == 1) - if (LampOn == 1) - LampLit.Enable - endif - LampUnlit.Enable - endif - if (LadderPurchased == 1 && LadderDeployed == 1) - Ladder.Enable - endif + SetStage RYB 51 ; Re-enable boat and attachment refs if (RockingEnabled == 0) set fQuestDelayTime to MediumUpdateRate endif set Resetting to 1 endif - if (LampOn == 1 && LampLit.GetDisabled == 1) + if (LampOn == 1 && LampLit.GetDisabled == 1 && Resetting != -1) set Resetting to 1 LampLit.Enable LampUnlit.PlaySound3D SPLFireballFail - elseif (LampOn == 0 && LampLit.GetDisabled == 0) + elseif (LampOn == 0 && LampLit.GetDisabled == 0 && Resetting != -1) set Resetting to 1 LampLit.Disable LampUnlit.PlaySound3D ITMTorchHeldExt @@ -663,10 +659,14 @@ begin GameMode if (BoatMoving == 2 && CollisionDetectTimer > 0) set CollisionDetectTimer to CollisionDetectTimer - SecondsPassed elseif (BoatMoving == 2 && CollisionDetectTimer <= 0 && Collider.GetInSameCell Player && Collider.GetPos z > CollisionDetectZThreshold) - if (RockingEnabled == 0) - set fQuestDelayTime to MediumUpdateRate + set ColliderActualX to Collider.GetPos x + set ColliderActualY to Collider.GetPos y + if (ColliderActualX > ColliderX - ColliderPosThreshold && ColliderActualX < ColliderX + ColliderPosThreshold && ColliderActualY > ColliderY - ColliderPosThreshold && ColliderActualY < ColliderY + ColliderPosThreshold) + if (RockingEnabled == 0) + set fQuestDelayTime to MediumUpdateRate + endif + SetStage RYB 21 ; Collision procedure endif - SetStage RYB 21 ; Collision procedure elseif (BoatMoving == 2 && CollisionDetectTimer <= 0 && Collider.GetDead == 1) ; Moving the collider every frame seems to cause the collider to spontaneously die when near land. However, this ; also seems to have many false positives where it dies in open water. Instead of every frame, the script uses @@ -1119,7 +1119,7 @@ begin GameMode endif ; Boat rocking animation - if (RockingEnabled == 1 && Dragging == 0 && Summoning == 0 && Grounded == 0 && OnLand == 0 && PlayerDistance < RockDistanceThreshold) + if (RockingEnabled == 1 && Dragging == 0 && Summoning == 0 && Grounded == 0 && OnLand == 0 && PlayerNearBoat == 1) ; Update random variation if (RockRandomTimer <= 0) set RockRandomTimer to RockRandomInterval @@ -1206,12 +1206,12 @@ begin GameMode endif endif - if (BoatMoving >= 1 || Dragging >= 1 || Resetting >= 1 || (PlayerDistance < RockDistanceThreshold && (RockZOffset != 0 || RockPitchOffset != 0 || RockRollOffset != 0))) + if (BoatMoving >= 1 || Dragging >= 1 || Resetting >= 1 || (PlayerNearBoat == 1 && (RockZOffset != 0 || RockPitchOffset != 0 || RockRollOffset != 0))) set ang to BoatAngle SetStage RYB 12 ; Calculate sin, cos, & tan for boat angle endif - if (BoatMoving >= 1 || Dragging >= 1 || Resetting >= 1 || (PlayerDistance < RockDistanceThreshold && (RockZOffset != 0 || RockPitchOffset != 0 || RockRollOffset != 0))) + if (BoatMoving >= 1 || Dragging >= 1 || Resetting >= 1 || (PlayerNearBoat == 1 && (RockZOffset != 0 || RockPitchOffset != 0 || RockRollOffset != 0))) ; Transform world pitch/roll to boat's local pitch/roll set BoatPitchAngle to (RockPitchOffset + DragPitchAngle) * cos + RockRollOffset * sin set BoatRollAngle to -(RockPitchOffset + DragPitchAngle) * sin + RockRollOffset * cos @@ -1258,7 +1258,7 @@ begin GameMode Set ColliderMoveTimer to ColliderMoveFreq endif endif - elseif (BoatMoving == 0 && Dragging == 0 && Summoning == 0 && (PlayerDistance < RockDistanceThreshold && (RockZOffset != 0 || RockPitchOffset != 0 || RockRollOffset != 0))) + elseif (BoatMoving == 0 && Dragging == 0 && Summoning == 0 && (PlayerNearBoat == 1 && (RockZOffset != 0 || RockPitchOffset != 0 || RockRollOffset != 0))) ; Apply rocking to stationary boat. set BoatZWithRock to BoatZ + RockZOffset @@ -1271,7 +1271,7 @@ begin GameMode endif ; Update attachments (seat, chest, lamp, ladder) positions and angles both while moving and stationary (if rocking) - if (BoatMoving >= 1 || Dragging >= 1 || Resetting >= 1 || (BoatMoving == 0 && Dragging == 0 && (PlayerDistance < RockDistanceThreshold && (RockZOffset != 0 || RockPitchOffset != 0 || RockRollOffset != 0)))) + if (BoatMoving >= 1 || Dragging >= 1 || Resetting >= 1 || (BoatMoving == 0 && Dragging == 0 && (PlayerNearBoat == 1 && (RockZOffset != 0 || RockPitchOffset != 0 || RockRollOffset != 0)))) SetStage RYB 30 ; Calculate seat position and angle SetStage RYB 40 ; Update seat position and angle diff --git a/UE4SSScripts/FixRowboatDuplication.lua b/UE4SSScripts/FixRowboatDuplication.lua new file mode 100644 index 0000000..4101cf1 --- /dev/null +++ b/UE4SSScripts/FixRowboatDuplication.lua @@ -0,0 +1,294 @@ +-- 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) + local objectName = object:GetFullName() + local className = string.match(objectName, "^([^%s]+)") + 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") \ No newline at end of file