Quickest way to store and retrieve a reference to a PC?

Continuing on from my last request: I am creating scripting to add and remove players from a list of the players in a local area so that I do not have to poll the entire player list all the time to address players merely in a given area.

I want a way to store a reference to the player object in a list - but I’m not quickly finding it. Is there way to do this easily? I don’t find some sort of UUID reference easily.

The most promising lead I found is some documentation about proving an object is a PC:

// PCs count down from 7fffffff and all other objects count up from 00000000.
// PC objects will start with "7ff" and be 8 characters long.

However I’m not sure, do these references remain the same within one session? Or can they change?

I am not worried about maintaining these lists across resets or crashes. If they don’t change within one session I could use this for lookups I think.

Quickest way to store and retrieve a reference to a PC?

SetLocalObject(GetModule(), "The PC", the_pc);
object the_pc = GetLocalObject(GetModule(), "The PC");

… in a list…

SetLocalObject(GetModule(), IntToString(idx) + "th PC", pc);
object pc = GetLocalObject(GetModule(), IntToString(idx) + "th PC");

Or, if you want to keep the list confined to a single variable:
(didn’t try compiling, sorry for any typos/etc)

void add_pc_to_list(object pc, string listname) {
    json j = GetLocalJson(GetModule, listname);
    j = JsonArrayInsertInplace(j, JsonString(ObjectToSTring(pc));
}
object get_pc_from_list(string listname, int index) {
    json j = GetLocalJson(GetModule, listname);
    return StringToObject(JsonGetString(JsonArrayGet(j, index)));
}

Alternatively…

SqlStep(SqlPrepareQueryObject(GetModule(), 
    r"CREATE TABLE pc_areas (pc_id text, area_id text);
      CREATE INDEX idx_pc_areas on pc_areas(area_id)");

and then just SELECT pc_id FROM pc_areas WHERE area_id=@area; and SqlBindString(q, "@area", ObjectToString(area);

But… Snark incoming:

image

You’ll probably need to hit about 100 concurrent players before something like the above becomes faster than just iterating all players and checking their area. Maybe focus on code that will bring those 100 players, and then if the simplest most obvious solution ever becomes a problem, well, it will be one of those good problems to have.

Code I was given when I approached this previously was hitting the instruction limit during tests around 40 players, though it could be a specific problem with that code. I might never hit 40 outside of testing, but I’d like a little more headroom than that where I can help it.

I can’t imagine what that code is doing, but instruction count (which is only loosely correlated with execution speed) is going to be worse with any of the wrapper methods than just:

for (pc = GetFirstPC(); pc != OBJECT_INVALID; pc = GetNextPC()) {
    if (GetArea(pc) != area) continue;
    // ... code for all PCs in desired area here...
}

That’s about a dozen instructions per PC. Any other solution is likely going to be an order of magnitude worse in the instruction department.

Yeah to be clear speed isn’t an issue here per se its wanting to:

a] minimize how many times I have to go through everyone by storing who is in the local area

and:

b] minimize the number of instructions.

Naturally, having less instructions should make it faster; but that’s a second-order consequence. I probably don’t have to worry about it in truth, but 40 players seemed a little low on the headroom side.

I also was having weird counting issues with something not entirely unlike what you’re suggesting, it seemed to include things that weren’t actually PCs. For instance one dungeon I’m testing has a NPC thats there to represent a dead guard, and they seemed to be included even when I was using the built in function to test if for if they were a PC. It was trying to solve that issue where I found the documentation I cited on the wiki in a provided function which did work.

[edit]: Confessional debugging moment: If it’s going through characters and not specifically player characters that way, that likely explains why it’s blowing the instruction limit; there’s a relatively high number of NPCs and monsters in my module compared to others.

You could build up a json array each time a player transition from one area to another, and then just check the array against the selected area.

If your PCs all have the same tag (including the default ""), you can also just iterate through all of them in a given area using GetNearestObjectByTag(string, object, int) - NWN Lexicon

This is what I was essentially trying to do, I was more or less looking for the way to address them that’s the easiest way to ensure that we’re addressing individual, unique players.

But clippy’s suggestion of using the same tag and then iterating might bear fruit, I’ll play a bit when I get home.

Unless you want to know wjo is in the area, not only if there are Pc in there.

Yep. Iterating the PCs and checking is probably quickest. One thing is that if you are then going to do complicated stuff on each PC you will want to DelayCommand that part to start a new script execution for each. Otherwise, yeah, you used to be able to hit the TMI doing that. Might not be as easy with the adjustable limit now though anyway.

Hmm I didn’t know DelayCommand would create a new execution stack, thats good to know!

Not trying to put any words in your mouth, just clarifying:

What comes up often with nwn builders is the idea that TMI is a hurdle that needs to be overcome rather than a very useful feature. It does a lot more than catch infinite loops. TMI is flawed, but it’s flawed in that it fires too rarely, because it counts instructions and not actual execution time.

When writing real time systems such as a video game it is really important to not stall the whole world (and input/rendering in SP) for a long time. In my experience, hitting TMI has always* meant that my code sucks and it needs to be refactored into something more responsive. Usually that means DelayCommand()s with sufficient spacing, but the goal should always be improved responsiveness and not just tricking the annoying TMI.

* one obvious exception being when there’s 0 players online, such as during module load.

In my experience, if you hit the instruction limit, even the default one, something is wrong. Maybe you’re processing a bunch of stuff in series that really ought to be in parallel. Maybe you have a runaway loop. Maybe you made a recursion monster. Either way its a sign something needs fixed.
My only real complaint is that there isn’t much feedback about hitting it and it could give a little more information as to the cause (or even a stack trace) so as to better help redesign.

We can add a stacktrace to the output. Would need to have the ndb file for it to be legible, but otherwise not an issue.

Edit: logged it as #3897

2 Likes

Agreed. I almost left off the part about the raised limit as it’s still better not to run scripts that long as you say :slight_smile:

Fwiw, recursion is different here as you are limited to something like 32 calls deep. I don’t recall what the failure looks like there at this point, it’s been a while since I tested that. But you will usually hit that limit before TMI with recursion. And executescript() is limited to like 8 deep, this one just silently fails when exceeded.