the Sim Settlements forums!

Register a free account today to become a member! Once signed in, you'll be able to participate on this site by adding your own topics and posts, as well as connect with other members through your own private inbox!

Old Post IDEK's Logistics Station integration

EyeDeck

Active Member
Messages
123
After getting a bunch of requests for this, I finally looked into it today. I've written a feature so that level 2+ Logistics Stations (the level at which the plot gets the ham radio and radio tower) will register themselves as "virtual" Communications Desks.
Code:
; --------------- Salvage Beacons Compat ---------------
; Plugin = SalvageBeacons.esp
; SB_CommunicationsDesk                    0x3D5F
; SB_SalvageBeaconsQuest =                0x80F9
;    ActiveCommStations =                    alias index 81
; PlayerHasWorkedCommStation =             0x2673
; DLCFarHarborHasWorkedCommStation =    0x268D
;    DLC03FarHarbor "The Island" =            0xB0F in DLCCoast.esm
; SB_HasActiveCommStation                 0x2675
string SBstr = "SalvageBeacons.esp" const

Function AddVirtualCommDesk(WorkshopScript thisWorkshop)
    lsTrace("SB Compat: Registering virtual Comm Desk to " + thisWorkshop + "...")
   
    Keyword SB_HasActiveCommStation = Game.GetFormFromFile(0x2675, SBstr) as Keyword
    GlobalVariable PlayerHasWorkedCommStation = Game.GetFormFromFile(0x2673, SBstr) as GlobalVariable
    GlobalVariable DLCFarHarborHasWorkedCommStation = Game.GetFormFromFile(0x268D, SBstr) as GlobalVariable
    Quest SB_SalvageBeaconsQuest = Game.GetFormFromFile(0x80F9, SBstr) as Quest
    RefCollectionAlias ActiveCommStations = SB_SalvageBeaconsQuest.GetAlias(81) as RefCollectionAlias
    Worldspace DLC03FarHarbor = Game.GetFormFromFile(0xB0F, "DLCCoast.esm") as Worldspace
   
    ActiveCommStations.AddRef(thisWorkshop)
    thisWorkshop.AddKeyword(SB_HasActiveCommStation)
   
    if (thisWorkshop.GetWorldspace() != DLC03FarHarbor)
        PlayerHasWorkedCommStation.SetValue(1)
    else
        DLCFarHarborHasWorkedCommStation.SetValue(1)
    endif
   
    lsTrace("...finished registering virtual comm desk.")
EndFunction

Function RemoveVirtualCommDesk(WorkshopScript thisWorkshop)
    lsTrace("SB Compat: Removing virtual Comm Desk from " + thisWorkshop + "...")
    ; first check if any actual comm desks are here - if so just let them handle this and bail out
    ObjectReference[] thisSettlementCommStations = thisWorkshop.FindAllReferencesOfType(Game.GetFormFromFile(0x3D5F, SBstr), 10000)
    if (thisSettlementCommStations.length)
        ScriptObject commStation = thisSettlementCommStations[0].CastAs("SB_commdesk")
        Var[] args = new var[1]
        args[0] = thisWorkshop
        commStation.CallFunctionNoWait("FindActiveCommStation", args)
        lsTrace("...Responsibility passed onto " + commStation + ".")
        return
    endif
    ; it's in SB_commdesk.FindActiveStation(), so it seems appropriate here
    Utility.Wait(5)
   
    Keyword SB_HasActiveCommStation = Game.GetFormFromFile(0x2675, SBstr) as Keyword
    GlobalVariable PlayerHasWorkedCommStation = Game.GetFormFromFile(0x2673, SBstr) as GlobalVariable
    GlobalVariable DLCFarHarborHasWorkedCommStation = Game.GetFormFromFile(0x268D, SBstr) as GlobalVariable
    Quest SB_SalvageBeaconsQuest = Game.GetFormFromFile(0x80F9, SBstr) as Quest
    RefCollectionAlias ActiveCommStations = SB_SalvageBeaconsQuest.GetAlias(81) as RefCollectionAlias
    Worldspace DLC03FarHarbor = Game.GetFormFromFile(0xB0F, "DLCCoast.esm") as Worldspace
   
    ActiveCommStations.RemoveRef(thisWorkshop)
    thisWorkshop.RemoveKeyword(SB_HasActiveCommStation)
   
    bool isFarHarbor = (thisWorkshop.GetWorldspace() == DLC03FarHarbor)
   
    bool otherActiveCommStation = false
    bool otherActiveFarHarborCommStation = false
    if (!isFarHarbor)
        for int i = 0 to ActiveCommStations.GetCount() - 1
            if (ActiveCommStations.GetAt(i).GetWorldspace() != DLC03FarHarbor)
                otherActiveCommStation = true
                break
            endif
        endfor
    else
        for int i = 0 to ActiveCommStations.GetCount() - 1
            if (ActiveCommStations.GetAt(i).GetWorldspace() == DLC03FarHarbor)
                otherActiveFarHarborCommStation = true
                break
            endif
        endfor
    endif
   
    if (!isFarHarbor && !otherActiveCommStation)
        lsTrace("\tCould not find another comm station - setting PlayerHasWorkedCommStation to 0.")
        PlayerHasWorkedCommStation.SetValue(0)
    endif
    if (isFarHarbor && !otherActiveFarHarborCommStation)
        lsTrace("\tCould not find another comm station in Far Harbor - setting DLCFarHarborHasWorkedCommStation to 0.")
        DLCFarHarborHasWorkedCommStation.SetValue(0)
    endif
   
    lsTrace("...finished unregistering virtual comm desk.")
EndFunction
; --------------- End Salvage Beacons Compat ---------------
This was one of those rare cases where the code I wrote compiled and worked as expected the first time, despite being a heavily reverse-engineered hack.

Anyway, since this functionally eliminates the need to build any of Salvage Beacons' native communications desks at all provided you're using my mod, @kinggath, is it okay if I release this? I intend for this feature to be disabled by default, even if Salvage Beacons is detected, so the user will have to knowingly go into the settings and flip the switch to turn it on.
 
If this does, what I think it does - this just made all my city plans extra awesome.
 
Hm, what happens if you have a logistics station in a mod-added settlement? I haven't reverse-engineered salvage beacons, but I have seen references to vanilla settlements in there, and assumed it's all hardcoded
 
Hm, what happens if you have a logistics station in a mod-added settlement? I haven't reverse-engineered salvage beacons, but I have seen references to vanilla settlements in there, and assumed it's all hardcoded
Most (all?) of the hard-coded stuff looks like it's either to correct the calculation for where the closest comm station is that determines where to dispatch a salvage team from, and how long it should take, or to override where the salvage team returns to.

I can't say it's how I'd have written it; I had to solve a similar problem for my Logistics Stations mod, and I ended up calculating distance based on the location of a settlement's map marker (which is always placed in a worldspace, so x/y coords are accurate), and also applying a coordinate offset from a small database (literally just one entry for Far Harbor and another one for Nuka World, though expandable up to the Papyrus array limit) in case the marker wasn't in the Commonwealth worldspace. I'm picky about my code, though, so the thought of having to hard-code anything at the per-settlement level was unacceptable to me.
 
I agree, hardcoding stuff sucks. I guess what my question boils down to is: do salvage beacons work in mod-added settlements, for which no hardcoded data exists? I never tried that, fearing that it would break my save.
 
Yeah you can release it.

Almost nothing is hard-coded in Salvage Beacons except for references to worldspaces and their entry points which are used to do calculate travel time to NukaWorld’s entrance and to keep Far Harbor isolated as a separate network. It also has a hard-cap on travel time, because occasionally GetDistance returns ridiculous numbers that could result in a month of travel time.

I will be rewriting Salvage Beacons from scratch at some point, but I imagine the keyword on workshops will always be the means of detecting desks as it’s required by the settlement select menu function.
 
Yeah you can release it.
Neat. I don't like interfering with other modders' mods without asking them is all, since there are plenty out there that would be absolutely livid if I did something like this without asking.

Almost nothing is hard-coded in Salvage Beacons except for references to worldspaces and their entry points which are used to do calculate travel time to NukaWorld’s entrance and to keep Far Harbor isolated as a separate network. It also has a hard-cap on travel time, because occasionally GetDistance returns ridiculous numbers that could result in a month of travel time.

When you get around to it, doing the sort of thing I did for ILS might not be a bad idea:
Code:
; ------------ From the main data storage quest ------------
Struct WorldspaceOffset
    Worldspace thisWs
    float x
    float y
    {Offsets to apply to the origin of this worldspace, which get added to settlements in this worldspace's x/y coords}
    float portalAX
    float portalAY
    {Location of map marker relative to this worldspace's coords}
    Worldspace connectedWS
    float portalBX
    float portalBY
    {Location of map marker in the connected worldspace's coords}
EndStruct

bool getMarkerSpinLock
ObjectReference Function GetMarkerFromLocation(Location loc)
    while (getMarkerSpinLock)
        ; this hack uses the story manager to get a reference for us, and as such it obviously can only get one thing at a time,
        ; so make sure the quest is shut down before we try to start it more than once
        lsTrace("A thread is waiting in the GetMarkerFromLocation spin lock.")
        Utility.WaitMenuMode(0.5)
    endwhile
    getMarkerSpinLock = true
    ObjectReference toReturn = None
    if (ILS_FindMapMarkerKeyword.SendStoryEventAndWait(loc))
        toReturn = MapMarkerHackRef.GetReference()
        ILS_MapMarkerHackQuest.Stop()
        if (toReturn != None)
            lsTrace("Successfully resolved location " + loc + "'s map marker ref: " + toReturn)
        else
            lsTrace("Warning: Could not resolve a map marker for location " + loc + ", perhaps modded workshop set up incorrectly?")
        endif
    else
        lsTrace("Warning: Tried to GetMarkerFromLocation, but no quest started!")
    endif
    getMarkerSpinLock = false
    return toReturn
EndFunction

; ------------ From a different update/compatibility script (the one which does stuff on game load) ------------
string sDLC03 = "DLCCoast.esm" const
string sDLC04 = "DLCNukaWorld.esm" const
Function CheckCompat()
    LogisticsParentScript:WorldspaceOffset[] newOffsets = new LogisticsParentScript:WorldspaceOffset[0]
  
    if (Game.IsPluginInstalled(sDLC03))
        AddWSOffsetData(newOffsets, 100000.0, 300000.0, 0x3FEE3, 0x3FEE5, sDLC03) ; DLC03ToCommonwealthMapMarkerREF [REFR:0103FEE3], DLC03ToFarHarborMapMarker [REFR:0103FEE5]
    endif
  
    if (Game.IsPluginInstalled(sDLC04))
        AddWSOffsetData(newOffsets, -300000.0, 20000.0, 0x25517, 0x25515, sDLC04) ; DLC04CommonwealthMapMarker [REFR:02025517], DLC04NukaWorldMapMarker [REFR:02025515]
    endif
  
    LogisticsParent.WorldspaceOffsets = newOffsets
EndFunction

Function AddWSOffsetData(LogisticsParentScript:WorldspaceOffset[] woArr, float x, float y, int portalIDA, int portalIDB, string wsPlugin)
    ObjectReference portalA = Game.GetFormFromFile(portalIDA, wsPlugin) as ObjectReference
    ObjectReference portalB = Game.GetFormFromFile(portalIDB, wsPlugin) as ObjectReference
    LogisticsParentScript:WorldspaceOffset wo = new LogisticsParentScript:WorldspaceOffset
    wo.thisWS = portalA.GetWorldspace()
    wo.x = x
    wo.y = y
    wo.portalAX = portalA.X + x
    wo.portalAY = portalA.Y + y
    wo.connectedWS = portalB.GetWorldspace()
    wo.portalBX = portalB.X
    wo.portalBY = portalB.Y
    woArr.Add(wo)
EndFunction
The basic structure is that a cache of stations with references + location data is kept in a struct array, and when I cache the x/y coordinates of each station, I get the map marker from the relevant location (either by grabbing out of the workbench's script property, or using the function above in case that fails), then get its worldspace, and if it isn't in the Commonwealth, I offset the coordinates with the data in the WorldspaceOffsets array. Then, when I need to do a distance check, I do it directly in Papyrus via Math.sqrt(Math.pow((x1-x2),2) + Math.pow((y1-y2),2) + Math.pow((z1-z2),2)), using the worldspace-corrected coordinate values.

That would also allow you to drop the Far Harbor-specific compatibility stuff, since you could just crank up the distance, e.g. set the offset for that worldspace to (1m, 3m), and let a raw distance check determine whether there are any comm desks in range.

For that mod, though, it's important that all of the code that does that is very performant; it needs to be able to do up to 128^2 distance checks in a very short period of time in order to calculate the minimum spanning tree properly, which obviously means that any sort of delayed function calls (GetDistance etc) would make the calculation take ridiculous amounts of time.

I will be rewriting Salvage Beacons from scratch at some point, but I imagine the keyword on workshops will always be the means of detecting desks as it’s required by the settlement select menu function.
If I have to rewrite or update it later, it's no big deal. All of the research + code writing combined only took me an hour or so in the first place.
 
Last edited:
@Caelaran, Well, I think my local version is in a decent enough state at the moment, so maybe tonight after a bit of extra testing to be sure I haven't done anything stupid.

Edit: For what it's worth I did actually find some code that wasn't working properly because of a compiler bug, of all things. This here is why testing is important.
 
Last edited:
Uh - suggestion? If these are being combined in this way (Which is great) could the combo be updated so instead of delivering to a specific settlement workshop, the delivery went to the nearest logistics locker?
 
@Jonnan It wouldn't be possible to do that without code on Salvage Beacons' side, anyway it would be functionally identical to manually sending the contents to the workbench with the storage designator in it--logistics lockers aren't containers themselves, rather they're just activators that let you open the inventory remotely.
 
@Jonnan It wouldn't be possible to do that without code on Salvage Beacons' side, anyway it would be functionally identical to manually sending the contents to the workbench with the storage designator in it--logistics lockers aren't containers themselves, rather they're just activators that let you open the inventory remotely.
Fair 'nuff - I just thought it would shorten the period to deliver the cache if you could shortcut to the nearest settlement rather than selecting the 'Warehouse' settlement which can be farther.

Admittedly, the logistics station 'collection' options actually rather cover that anyway - everything *is* eventually delivered to the logistics workbench if you have it setup, so this is a bit redundant anyway.
 
Hello Kinggath and EyeDeck,
For clarity: I have both installed through Bethesda's Mod Install and not Nexus Mods.

I have a few short answered questions I am hoping you two can answer quickly about both IDEKs Logistics and Salvage Bacons work. I think I have most of these questions answered already, but I'm just looking for clarity.

Salvage Beacon Questions:
1) Can you deploy more than 1 Beacon out in the wasteland at the same time, or will it delete the prior beacons?
2) Does the beacon also pick up everything in the stash or just junk. (I'm hoping it also takes Armor, Weapons, and Food/Drink to help with settlement donations).

IDEK Logistics with Salvage Beacons turned on.
1) I have 2 Logistic LVL 1 stations (Red Rocket and Sanctuary (Both LVL 0 Settlements) but I get message that I can't deploy a beacon till I have a Comms station. Is this because they are LVL 1 and I have to wait for them to Upgrade to LVL 2?
2) Will the salvage be deposited in the stations storage?
3) Is the Storage automatically shared with the Workbenches, and if Yes is the storage global or Local to the Settlement?
 
  • The ILS/Salvage Beacons integration only kicks in at level 2 when the station gets a radio tower. Or that was the idea anyway, it works with the level 2/3 interior plot too despite it never getting a radio tower (for obvious reasons).
  • Nope, it goes in the workbench. All ILS does is "trick" Salvage Beacons into thinking it has a comm desk in the settlement by manipulating some of SB's internal data stores, so it is functionally identical to a normal comm desk.
  • Ideally you should put ILS's storage thing in a workbench; I tried seeing if there was a way to make an arbitrary container behave as if it were connected to a supply line network, but ultimately concluded that it isn't possible. It might be possible if I were to make certain tweaks to one of the workshop scripts (I don't remember which specific one atm), but that's a mess I don't want to get into.
 
Thank you so much for getting back to me so fast!!
That's great to know that is kicks in at LVL 2.
It's great to have Salvage Beacons tied to the Settlements.
 
As a quick aside, on my last game I kept waiting for a L2 salvage station and at some point realized that the Salvage Beacon integration was toggled off in the settings. Not sure if that's the default and I just hadn't had a new game for awhile but if it's not working you might double check that.

Also, if you have Automatron the HQ you get there is never attacked and is a great place to set your logistics center store. The Castle is only attacked during scripted mass attacks and is a good second option.
 
Top