Jump to content

Papyrus Scripting, how to check player location in town or city?


Recommended Posts

Posted (edited)

What I have now is this:

Spoiler
Scriptname BM_LocationCheck extends ReferenceAlias

bool Property isInSettlement auto
location Property WhiterunLocation auto
location Property MarkarthLocation auto
location Property SolitudeLocation auto
location Property RiftenLocation auto
location Property WindhelmLocation auto
location Property DawnstarLocation auto 
location Property FalkreathLocation auto 
location Property MorthalLocation auto
locartion Property WinterholdLocation auto
location Property DLC2RavenRockLocation auto 

Function checkSettlement(location akNewLoc)
	if (akNewLoc == none)
		isInSettlement = False
	else
		if (akNewLoc.HasKeyWord(LocTypeCity) || akNewLoc.HasKeyWord(LocTypeTown))
			isInSettlement = True
		elseIf (WhiterunLocation.IsChild(akNewLoc))
			isInSettlement = True
		elseIf (MarkarthLocation.IsChild(akNewLoc))
			isInSettlement = True
		elseIf (SolitudeLocation.IsChild(akNewLoc))
			isInSettlement = True
		elseIf (RiftenLocation.IsChild(akNewLoc))
			isInSettlement = True
		elseIf (WindhelmLocation.IsChild(akNewLoc))
			isInSettlement = True
		elseIf (DawnstarLocation.IsChild(akNewLoc))
			isInSettlement = True
		elseIf (FalkreathLocation.IsChild(akNewLoc))
			isInSettlement = True
		elseIf (MorthalLocation.IsChild(akNewLoc))
			isInSettlement = True
		elseIf (WinterholdLocation.IsChild(akNewLoc))
			isInSettlement = True
		elseIf (DLC2RavenRockLocation.IsChild(akNewLoc))
			isInSettlement = True
		else
			isInSettlement = False
		endIf
	endIf
EndFunction

 

 

The idea is that a separate script can call checkSettlement(). I've separated the scripts so that it's just easier to read and parse atm. 

1) Will this work, or is there a more efficient method?
2) Can I store locations in an array?

 

Spoiler
Event onInit()
	new settlement = location[10]
	settlement[0] = WhiterunLocation
	settlement[1] = MarkarthLocation
	settlement[2] = SolitudeLocation
	settlement[3] = RiftenLocation
	settlement[4] = WindhelmLocation
	settlement[5] = DawnstarLocation
	settlement[6] = FalkreathLocation
	settlement[7] = MorthalLocation
	settlement[8] = WinterholdLocation
	settlement[9] = DLC2RavenRockLocation
endEvent

Function checkSettlement(location akNewLoc)
	int numCheck = settlement.Length
	while numCheck
		if (settlement[numCheck].IsChild(akNewLoc))
			isInSettlement = True
		else
			isInSettlement = false
		endIf
		numCheck -= 1
	endWhile
endFunction

 

 

Alternatively, can we also do this:

Spoiler
Event onInit()
	new settlement = location[10]
	settlement[0] = WhiterunLocation
	settlement[1] = MarkarthLocation
	settlement[2] = SolitudeLocation
	settlement[3] = RiftenLocation
	settlement[4] = WindhelmLocation
	settlement[5] = DawnstarLocation
	settlement[6] = FalkreathLocation
	settlement[7] = MorthalLocation
	settlement[8] = WinterholdLocation
	settlement[9] = DLC2RavenRockLocation
endEvent

Function checkSettlement(location akNewLoc)
	int numCheck = settlement.Length
	while numCheck
		if playerRef.IsInLocation(settlement[numCheck])
			Debug.Trace("Player is in" + settlement[numCheck])
		endIf
		numCheck -= 1
	endWhile
endFunction

 

 

Edited by Gyra
Posted (edited)

Provided that the separate script that's supposed to use it is also part of your mod, the simplest might be storing locations in a form list. That way, you can add or remove them without needing to edit the script with new properties, or needing to even add and fill out that long list of location properties in the first place. And the body of your function can be reduced to this:

FormList Property Locations Auto

Function checkSettlement( location akNewLoc )
	isInSettlement = Locations.HasForm( akNewLoc )
EndFunction

 

Edited by Taki17
Posted (edited)

So I added the main city and town names via CK to a formlist, one for cities and one for towns. However, the script refuses to read the formlist.

 

I thought it was the akNewLoc location variable I was providing that was problematic. But if I switched it out to an included Location, the code still can’t read the formlist. 
 

I’ve merged the scripts into one script for easier debugging. Calling the function results in a “Can’t use HasForm() on a None object”.

 

Ex.

Location Property WhiterunLocation Auto

FormList Property BM_Cities Auto

onLocationChange(location akOldLoc, location akNewLoc)

isInCity = BM_Cities.HasForm(WhiterunLocation)

 

 

Any ideas? ChatGPT ain’t helping, maybe cuz I’m not asking the right questions.

Edited by Gyra
Posted
46 minutes ago, Gyra said:

Any ideas? ChatGPT ain’t helping, maybe cuz I’m not asking the right questions.

That is unlikely to help you since it's been made profoundly retarded with each successive update. Also because what you have is most likely ain't a coding issue.

 

Have you made sure to fill out the property on the script with the form list you want?

Posted (edited)
19 minutes ago, Taki17 said:

That is unlikely to help you since it's been made profoundly retarded with each successive update. Also because what you have is most likely ain't a coding issue.

 

Have you made sure to fill out the property on the script with the form list you want?

Yeah, pretty sure. Feel free to take a look at the script if you'd like. I've been banging my head against a wall for half-a-day on this.

BM_Player.psc

 

Unless I'm misunderstanding you and missed a step. Sorry, i'm new to all this.

Edited by Gyra
Posted (edited)
1 hour ago, Gyra said:

Unless I'm misunderstanding you and missed a step.

I meant assigning the actual form list you have created in xedit or the Creation kit to the script properties to fill the cites and towns form lists declared in the script.

 

Example is my own mod, however you need to double click the script in your quest to bring up the properties window and fill out the values.

Spoiler

properties.jpg.b19ade5b3ee051d74a9eb6467f896f77.jpg

I have a strong suspicion that you are getting a None object because there is nothing behind the property.

 

Edited by Taki17
Posted (edited)
2 hours ago, Taki17 said:

I have a strong suspicion that you are getting a None object because there is nothing behind the property.

My instinct is that you're right, but is there a way to do this outside CK?

 

I'd assumed writing "FormList Property BM_Cities Auto" was enough to assign the formlist to script properties.

 

I've been staring at a small-ass Steam Deck screen for the past weeks, but that's besides the point. I'm getting a bunch of errors when using CK to read or compile scripts whereas an external third-party PCA has no issue compiling my scripts provided I have the correct source files. Hell, CK tells me the main script is missing a variable found in another script, created by and related to the mod, that CK does see. My guess is that my CK environment isn't setup correctly, so I'd like to avoid it since I've already used CK to create my BM_Cities and BM_Towns formlists and populated them with Locations.

 

I opened that window in your screenshot with CK, but with the mod that I'm editing (BM Licenses), that properties window is completely blank even though there are declared properties in the script that function 100% fine.

Edited by Gyra
Posted
12 minutes ago, Gyra said:

I'd assumed writing "FormList Property BM_Cities Auto" was enough to assign the formlist to script properties.

You'd be right if these were Fallout 3/New Vegas scripts. Alas, they ain't. Think of properties in Skyrim as a sort of handle that's setup to contain a formlist, but this handle can refer to any formlist you assign it to in the script properties.

 

15 minutes ago, Gyra said:

is there a way to do this outside CK?

I guess you could xedit, however it displays data in a bit of a raw-er format. Same example but in xedit:

Spoiler

xeditfl.jpg.2b4747537952424d518fbd9dfc8cd1ec.jpg

 

Though if I were you, I'd look into properly setting up the Creation Kit. Even though xedit is very useful for things like small specific adjustments or batch processing file records, the visual approach of the Creation Kit is sometimes more informative, especially if you are dealing with an aspect that is new to you and don't know what the records do.

Posted (edited)

Ok so I spent another half-a-day moving all source scripts to .\Data\Source\Scripts from .\Data\Scripts\Source (if seen through MO2) and finding that I'm missing FNIS Sexy Move files (which weren't necessary when compiling through PCA). I enabled the setting to allow CK to load multiple masters, and I can't remember what else I did. So FML and fuck Bethesda for using two different script source directories.

 

CK works, compiles my script, and I can add the formlist as a script property.

 

Edit: oh but now CK removed my patch's masters.

Edit 2: adding the formlists to the main plugin, which only has Skyrim.esm as the master, allows it to retain its previous records. Seems using CK to edit a .esp that has another .esp as a master removes the latter as master.

Edited by Gyra
Posted

@Taki17

With your help, I've managed to make the above BM_Player track whether player is in town or city, after some script modifications, and to some extent. The problem now is that location IDs such as WhiterunLocation are both too broad and do not cover interior locations (unlike if one were to use the isInLocation() function). For example, with BM_Cities formlist of locations including WhiterunLocation, isInCity == true when Player is in Plains or Clouds district and when Player is wandering around the stables. isInCity == false when player in inside The Bannered Mare. Another example: isInTown == true when Player is in Riverwood exterior. isInTown == false when Player is in Alvor's house.

 

What I want: Whiterun city outskirts shouldn't be counted as part of the city since Whiterun has walls, but Falkreach city outskirts should be counted as such.

 

One option I've thought of is to add all possible settlement locations into the formlists, separated into town and city as like it is currently, but filter certain areas with an IF statement. I don't want areas outside walls to count as part of the city, which means including WhiterunLocation, RiftenLocation, SolitudeLocation, and MarkarthLocation (else city streets won't count as city) and creating a condition for when in WhiterunStables etc. Is a large formlist bad practice? The number of forms will exceed 128.

 

The other option is to remain with WhiterunLocation, FalkreathLocation, RiverwoodLocation etc. the parent forms, and make a loop to check each form for isInLocation (to check child forms).

 

Function checkIsInCity(location currLoc)
    int numToCheck = licenses.BM_Cities.GetSize()
    while(numToCheck)
        if licenses.playerRef.GetActorRef().GetAt(numToCheck)
            Debug.Notification("Player is in a city.")
        endIf
    endWhile
endFunction

 

 

What would you say is better? 

Posted (edited)
6 hours ago, Gyra said:
Function checkIsInCity(location currLoc)
    int numToCheck = licenses.BM_Cities.GetSize()
    while(numToCheck)
        if licenses.playerRef.GetActorRef().GetAt(numToCheck)
            Debug.Notification("Player is in a city.")
        endIf
    endWhile
endFunction

Just so that you are aware, this will result in an infinite loop. You need the loop iterator either increased or decreased with each run of the loop, and also specify a proper exit condition that is a comparison with the loop iterator value - which should not exceed the array/list size. Example: while( i  < licenses.BM_Cities.Getsize() )

 

Furthermore, since arrays and form lists start with the 0 index, the actual first index you need to check will be size-1. Example: A form list has 10 elements, getting its size will return 10. However, the last item is stored at the 9th index.

 

The GetAt call on the player will not compile, since that's not getting the player's location, but is used on form lists to fetch an element at a specific index.

 

6 hours ago, Gyra said:

The problem now is that location IDs such as WhiterunLocation are both too broad and do not cover interior locations

I believe you can leverage the parent locations for these city house interiors. Every house and such in Whiterun should has WhiterunLocation as their parent location. For that, there is the IsSameLocation function present in the game, which is aware of parent locations on location forms. It looks something like this:

Bool Function checkIsInCity( Location currLoc )
    ;setup the loop iterator
    int i = 0
    ;loop will go on either until a match is found or we ran out of locations to test
    While( i < licenses.BM_Cities.GetSize() )
        ;assuming currLoc holds the player's current location and BM_Cities holds the city locations:
        ;the LocTypeCity keyword is present on WhiterunLocation
        ;WhiterunLocation is a parent to all locations within Whiterun, like WhiterunBanneredMareLocation
        ;using GetKeyword spares you a keyword property declaration if you type in the exact name of the keyword you want
        If currLoc.IsSameLocation( licenses.BM_Cities.GetAt(i), Keyword.GetKeyword( "LocTypeCity" ) )
            Debug.Notification( "Player is in a city." )
            Return True
        EndIf
        i += 1
    EndWhile
    ;no match found, player is not in a city, return false
    Return False
EndFunction

 

9 hours ago, Gyra said:

adding the formlists to the main plugin, which only has Skyrim.esm as the master, allows it to retain its previous records. Seems using CK to edit a .esp that has another .esp as a master removes the latter as master.

The Creation Kit does not allow esp masters. For that to work, you'll need to temporarily add the ESM flag to your plugins in xedit, and remove it once you are finished working on the mod that has them as masters. Alternatively, move your records around in xedit, since it will allow loading esp masters.

 

Edited by Taki17
Posted
18 hours ago, Taki17 said:

The GetAt call on the player will not compile, since that's not getting the player's location, but is used on form lists to fetch an element at a specific index.

Oops, I must've been dead tired when I sent that code snippet since I didn't even include the IsInLocation() function, which I also totally misinterpreted.

 

You've pretty much done half of my work for me, but I've learnt a lot, so thank you so much.

 

licenses.BM_Cities.GetAt(i) cannot be detected as type Location, so I simply had to add "as location" to prevent parameter type mismatch.

 

Bool Function checkIsInCity(location currLoc)
    int i = 0
    While(i < licenses.BM_Cities.GetSize())
        If currLoc.IsSameLocation(licenses.BM_Cities.GetAt(i) as location, Keyword.GetKeyword("LocTypeCity"))
            Debug.Notification("Player is in a city.")
            Return True
        EndIf
        i += 1
    EndWhile
    Debug.Notification("Player is NOT in a city.")
    Return False
EndFunction

 

I've also realized that the Stables Locations aren't enough to blacklist walled-city outskirts, based on the cell render preview inside CK. I'll just rationalize the governance of licenses in walled-city outskirts as making sense given that some residents don't live within the city walls. I'll look around for filters or alternatives when I have more time to waste.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...