Jump to content
  • Announcements

    • AndalayBay

      Orphan Attachments   07/31/2018

      I have been doing some housekeeping lately and I've noticed that I had a lot of orphaned attachments. Attachments get orphaned when the PM or post is deleted without removing the attachment first. Deleting a PM or post does not delete the attachment and the file or image remain on the server. I'd like to ask all members to go through their attachments and delete any attachments you don't need anymore or those that have been orphaned. Where can I get a list of my attachments? Click on your display name in the upper right corner of the forums and pick "My Attachments" from the drop-down list. How can I tell an attachment is orphaned? If the PM has been deleted, you'll see a message like this in your attachment list: Unfortunately there is no message if the post has been deleted, so please check your old posts. We do purge old birthday threads every once in a while. Also some hosted projects have been shut down, so you may have orphaned attachments on one of those locations. Thanks!
saebel

OBSE Plugin / Array Processing C++ Expert Assistance Needed

Recommended Posts

Hoping someone with knowledge sees this.

So I'm working on revamping my SDR.dll via OBSE's plugin system.  I'm trying to improve the overall processing time, get rid of memory leaks, etc., to the best of my ability.

Before I get into my problem, I should explain the structure.   There are two "Master" arrays used.

The first one is the Master COV Map, which uses nested maps to store custom object values.  It's structure is as follows:
map ["Parent Mod" (string), "Child Refs" (map["BaseRefID" (UInt32), "COV Groups" (map["GroupID" (UInt32), "COV Data Set" (map["Data Key" (string), "Data Value" (num, string, or ref)])])])]

The second one is the ModGroup ID Array, which is a simple array to assign an ID (the key) to a Mod Name / Group Name pairing.  The Group ID is then used for setting COV values (above) when functions are called to set/get/mod the COVs.  It will look like this:

[0] "SDR.esp|Actor Values"

[1] "SDR.esp|Weapon Values"

[2] "SomeOtherMod.esp|Name of the Group"

I have already completed the core programming needed to set/get/mod values and to add/create maps on the fly as needed.  However, I am concerned about bloat.

I have created some clean up processes that removes data from invalid references and mods that are no longer in the active load order.  However, they are all done through scripts through the SDR.esm.  I would much rather port those features into the SDR.dll to boost performance.

I need to be able to loop through all the Master data and remove any Group ID maps, reference maps, and mod maps that are no longer valid or active - removing entries as I go.  The Group ID array does not need entries removed, because I don't want the key numbers to shift (that can impact data stored in the active save game).  So if a group gets deleted, the entry will be set to "". 

In the SDR.dll, I'm using the OBSE Array Interface system (PluginAPI.h), which has all the functions I need - except one: there is no method to *remove* a key/value pair!!!

If the arrays/maps were just a standard C++ implementation, it would be no problem.  But the OBSE Array Interface has a very whacky construct.  I've tried tapping into ArrayVar.h/.cpp files to see if I can bypass the OBSE Array Interface and use some of the functions deeper under the hood, but I haven't had any luck.  C++ is not my strong suit, and I've come to a dead stop on how to handle this.

Suggestions of any kind would be extremely welcome.

Share this post


Link to post
Share on other sites

This looks like it’s not just C++, but an intimate knowledge of the OBSE code as well. Have you tried contacting shadeMe on GitHub or Nexus? I just checked GitHub and he was active two days ago, but it doesn’t look like there’s any messaging system. Here’s his Nexus profile.

I will poke a couple of folks for you, but as I said, I don’t think it’s just a C++ issue.

Share this post


Link to post
Share on other sites

This may be crude, saebel, as I have no knowledge of plugins, but can't you use native C++ code to delete keys/values?  Then use the OBSE api to talk between the plugin and esp?  Build your array and operate on it within the C++ plugin?  Like I said, I don't know shit about the OBSE api or plugins in general.

Share this post


Link to post
Share on other sites

Tried using Maps or Stringmaps (only difference is data type of the key; map=numeric keys)?

http://obse.silverlock.org/obse_command_doc.html#Array_Variables
Arrays according to the OBSE docs do not allow gaps. If you used ar_Erase on the element of an array, you would necessarily cause an index shift.
"An Array behaves like arrays in most programming languages: the key is an unsigned integer starting at zero, and there are no gaps between elements. (In other words, if an element exists at indexes 1 and 3 then an element necessarily exists at 0 and 2)"

The map type of array allows gaps, floating point keys, etc. I'm sure all the goodies you want are there without diving into the guts, so try constructing it like

(array_var) ar_Construct arrayType:map

Share this post


Link to post
Share on other sites

This is in a DLL, not game code. It’s written in C++ using the OBSE libraries. @Malonn, that’s a good idea. @saebel, you could do as Malonn suggested. I also saw something in the OBSE array library that suggested they do their own garbage keeping. We got our server fixed up and I got git installed, but I haven’t had a chance to upload the code to the server. Instead I’ve been tearing my hair out all day as I try to get Fail2ban fixed. The server upgrade broke it. :mad:

Share this post


Link to post
Share on other sites
16 hours ago, Malonn said:

This may be crude, saebel, as I have no knowledge of plugins, but can't you use native C++ code to delete keys/values?  Then use the OBSE api to talk between the plugin and esp?  Build your array and operate on it within the C++ plugin?  Like I said, I don't know shit about the OBSE api or plugins in general.

In order to do what you suggested, I would have to do the following:

  1. Copy the OBSE array into a C++ array.
  2. Use native C++ to find and remove the key/value pair.
  3. Copy the native C++ array into a new OBSE array.
  4. Replace the old OBSE array with the new one.

So here's the deal with how they set up the OBSE Array API.  They provided an example of how to do step 3.  But they did not provide an example of how to do step 1.  Considering how many tiers and subarrays we are talking about, I can't believe that coming up with a set of functions to constantly transpose each of the OBSE arrays into C++ and back again would be of any performance benefit.

I suppose it may be possible to keep all the arrays in C++ locally during the game session, and only do the conversion upon game load/save.  But the OBSE API has everything else already covered in terms of storing integers, floats, and strings.

I can already drill down to an OBSE key/value pair using their API.  I just want to be able to remove it.

I guess one other possible method would be to take the existing OBSE array that needs the key/value removed, and generate a duplicate of the array by rolling through and adding the key/values, then skip the key/value I want to remove.  But again, that just seems like extra-processing intensive.

Share this post


Link to post
Share on other sites
15 hours ago, darkrumbleking said:

Tried using Maps or Stringmaps (only difference is data type of the key; map=numeric keys)?

http://obse.silverlock.org/obse_command_doc.html#Array_Variables
Arrays according to the OBSE docs do not allow gaps. If you used ar_Erase on the element of an array, you would necessarily cause an index shift.
"An Array behaves like arrays in most programming languages: the key is an unsigned integer starting at zero, and there are no gaps between elements. (In other words, if an element exists at indexes 1 and 3 then an element necessarily exists at 0 and 2)"

The map type of array allows gaps, floating point keys, etc. I'm sure all the goodies you want are there without diving into the guts, so try constructing it like


(array_var) ar_Construct arrayType:map

I already have solved my problem using scripting and OBSE array functions.  But I would prefer to move all of that into the .dll plugin to improve performance when it comes to loading/save times.  With a small load order and a short adventure time, performance hit is negligible.  I'm concerned about heavily modded games with many hours of play time.  The arrays for those will be really large, and processing very time consuming (I imagine).

Share this post


Link to post
Share on other sites

Understood.  Like I said, I know nothing of the OBSE plugin api, etc.  Nor C++, for that matter.  Maybe AB will find something.

Share this post


Link to post
Share on other sites

To copy an mixed array like that into a C++ usable form might want an std::Variant. This is new stuff, and would take a bit of doing to set up. Is it possible to split the mixed array into smaller ones of different types? Posting some code might help as well. :)

Share this post


Link to post
Share on other sites

There is a lot of code to sort through.  But essentially it comes down to the following:
There is code in OBSE that allows you to erase elements.  But it requires knowing the array ID number.

UInt32 ArrayVarMap::EraseElements(ArrayID id, const ArrayKey& lo, const ArrayKey& hi)
{
	ArrayVar* var = Get(id);
	if (!var || lo.KeyType() != hi.KeyType() || lo.KeyType() != var->KeyType())
		return -1;

	// find first elem to erase
	std::map<ArrayKey, ArrayElement>::iterator iter = var->m_elements.begin();
	while (iter != var->m_elements.end() && iter->first < lo)
		++iter;

	UInt32 numErased = 0;

	// erase. if element is an arrayID, clean up that array
	while (iter != var->m_elements.end() && iter->first <= hi)
	{
		iter->second.Unset();
		iter = var->m_elements.erase(iter);
		numErased++;
	}

	// if array is packed we must shift elements down
	if (var->IsPacked())
		var->Pack();

	return numErased;
}


There is code that allows you to get the array ID number, but it requires getting the array as a passed argument via a script. 

// From Comands_Array.cpp

static bool Cmd_ar_Erase_Execute(COMMAND_ARGS)
{
	// returns num elems erased or -1 on error
	UInt32 numErased = -1;
	ExpressionEvaluator eval(PASS_COMMAND_ARGS);
	if (eval.ExtractArgs() && eval.Arg(0))
	{
		if (eval.Arg(0)->CanConvertTo(kTokenType_Array))
		{
			ArrayID arrID = eval.Arg(0)->GetArray();
			
			if (eval.Arg(1)) {
				// are we erasing a range or a single element?
				const Slice* slice = eval.Arg(1)->GetSlice();
				if (slice)
				{
					ArrayKey lo, hi;
					slice->GetArrayBounds(lo, hi);
					numErased = g_ArrayMap.EraseElements(arrID, lo, hi);
				}
				else
				{
					if (eval.Arg(1)->CanConvertTo(kTokenType_String))
					{
						ArrayKey toErase(eval.Arg(1)->GetString());
						numErased = g_ArrayMap.EraseElements(arrID, toErase, toErase);
					}
					else if (eval.Arg(1)->CanConvertTo(kTokenType_Number))
					{
						ArrayKey toErase(eval.Arg(1)->GetNumber());
						numErased = g_ArrayMap.EraseElements(arrID, toErase, toErase);
					}
				}
			}
			else {
				// second arg omitted - erase all elements of array
				numErased = g_ArrayMap.EraseAllElements(arrID);
			}
		}
	}

	if (numErased == -1) {
		*result = -1.0;
	}
	else {
		*result = numErased;
	}

	return true;
}

The "GetArray()" function that passes the Array ID is buried in the Script Token section of OBSE.  For example:

// From ScriptTokens.cpp

#if OBLIVION
ArrayID ScriptToken::GetArray() const
{
	if (type == kTokenType_Array)
		return value.arrID;
	else if (type == kTokenType_ArrayVar)
		return value.var->data;
	else
		return 0;
}

// snip

ArrayID ArrayElementToken::GetArray() const
{
	ArrayID out = 0;
	g_ArrayMap.GetElementArray(GetOwningArrayID(), key, &out);
	return out;
}

Within the OBSE plugin API framework, I haven't found any internal function for getting the array ID outside of the scripted method of passing an argument.  However, I *can* get the array ID via a script, using the following code:

scn sdrGetArrayID

array_var aArray
string_var sArrayID
short iArrayID

Begin Function {aArray}
	; calculates and saves the Array ID
	Let sArrayID := sv_Construct "%g" aArray
	Let iArrayID := ToNumber sArrayID
	sv_Destruct sArrayID
	SetFunctionValue iArrayID
End

What my mod does is:
- On game load, either retrieve or create the master arrays that store all the data.
- Using my scripted function, it then determines the array ID of the master IDs and stores them in custom game setting variables
- Those game setting variables can then be accessed by the sdr.dll to grab the master arrays and process them.

*However*, when the subarrays are created and stored inside each parent array, all of that is handled from within the sdr.dll functions.  And because there are no script arguments involved in the lower tiers, I can't figure out a way to extract the arrID.

I have tried code like the following:

ArrayID GetArrayID(OBSEArray* arr) {
	ArrayID out = 0;
	g_ArrayMap.GetElementArray(GetOwningArrayID(), arr, &out);
	return out;
}

But none of my experiments have worked.

:(

Share this post


Link to post
Share on other sites

Jimi and I took a look this afternoon, but we’ll have to go through all the code. One idea I had off the top of my head is why can’t you store the array id’s when you populate the arrays?  Either create another dimension to store the id’s or a separate mapping array. We’re having a tough time understanding what you’re trying to do. Could you post a concrete example?

Share this post


Link to post
Share on other sites
Posted (edited)

Okay, hopefully this will help.  See the attached .jpg as a visual aid.

Let's say I want to assign a custom object value to an actor.  The general format (more or less) is along these lines:  callingRef.sdrSetCOVsNum GroupID "ValueKey" Value

For example, I want to set the Player's hearing to type "2": PlayerRef.sdrSetCOVsNum 0 "iCOVsHearingType" 2
In the .dll, the following series of programmed events would take place:

1. Get the name of the Parent Mod of the calling ref using an internal function.  In this case, that would be "Oblivion.esm".
2. Determine the base ref ID of the actor (last six hex digits of the eight), and convert to an integer (I think it's 20 in the case of the player ref).
3. Retrieve the master string map (tier 1) that stores everything by an ID that is captured and stored in a global on game load.
4. Find the Parent Mod key in the string map ("Oblivion.esm"), and retrieve the associated numeric map (tier 2) of base refs.  If it isn't there, create a new key/value pair and populate the value with a new empty map.
5. Find the base ref (20) in the numeric map (tier 2), and retrieve the numeric map of COV Group IDs that are actively being used.  If the base ref ID is missing, create a new key/value pair and populate the value with a new empty map.
6. Find the group ID (tier 3) that was passed in the function ("0" in the example above), and retrieve the data set of key/value pairs that belongs to it (tier 4).  If the group ID is missing, create a new key/value pair and populate the value with a new empty string map.
7. Find the string value key ("iCOVsHearingType") (tier 5) and set the value of it to the passed value of the function (2).  If the key is missing, create a new string key/value pair.

The above example totally works with the current code in the .dll.  And there are functions for getting/setting/modifying numbers, refs, and strings (no arrays for now) that can be called from scripts.

But let's say that I wanted to *remove* a key/value pair of any kind.  For example, let's say a mod is no longer in the active load order.  I would want to delete that key value pair from the Master String Map, because all of that mod's refs would no longer be active, and so all the associated COV data would just be taking up space.  Or let's say that a single reference has been deleted or is invalid (such as a dynamic randomly generated monster), then I would want to be able to remove that base ref ID key/value pair (and thus all the COV data associated with that ref).

But there is no method programmed into the OBSE's Array Interface API that let's you remove a key/value pair.   And every experiment I have tried with my limited knowledge and resources has failed.

I have had to fall back on using scripts in my SDR.esm mod and the OBSE "ar_Erase" function in order to remove inactive mods and deleted/invalid refs.  Those scripts do work.  But I would *rather* do it in the .dll, because I would imagine the performance of rolling through all those records would be significantly higher in the .dll.  Especially if someone has a heavy load order with mods that add lots of NPCs and creatures.

But for the life of me, I can't figure out how to do it.

And while I appreciate the suggestion for tracking the array IDs in another table, there's no method to get the array ID either - at least not as far as I could determine.  I can create the array using the OBSE ArrayVar interface, and I can store the array.  But I have no idea how I am supposed to determine what the ID of the array is once it is created.  I have tried a number of methods (as stated in previous posts), and none of them have worked.  If I could do that, that's half the problem solved right there, since the other array functions used to erase key/value pairs require the array ID.

Flow Chart of Custom Object Value Array Hierarchy.jpg

Edited by saebel

Share this post


Link to post
Share on other sites

Are you sure that all this retained data impacts performance? It looks like it’s going to take a lot of code to clear out these array entries and I wonder if there’s a net gain in the end.

Share this post


Link to post
Share on other sites

SDR adds nearly 60 new actor value categories in order for its advanced detection system to work.  These get added to every NPC and Creature.  That's a lot of extra data.

If the data isn't cleared out for mods and references that are no longer active, there would be save game bloat that will eat up memory.  And considering Oblivion's limitations when it comes to accessing and maintaining memory, I think that would be a bad thing.  There could also be issues with mods that get uninstalled at some point and reinstalled later.  The dynamic references array would be the worst one, since that data would constantly grow, and possible have incorrect data if ref IDs get reused (not sure how that works, or if that happens).

If a mod gets uninstalled, I feel it is proper house keeping to wipe out any and all COVs data that is related to it.  Same goes for any ref that is invalid or gets deleted.   And since I've designed it to be used by other mods so that they can create their own COVs, that would impact what they do as well.

Technically I do have the scripted solution.  I just know from past experience that performance increases exponentially when I can move processes out of the scripts and into the .dll.

If no one spots an easy solution, I will release it with the scripted clean up versions and hope for the best.
 

Share this post


Link to post
Share on other sites
Quote

static bool Cmd_ar_Erase_Execute(COMMAND_ARGS)
{
	// returns num elems erased or -1 on error
	UInt32 numErased = -1;
	ExpressionEvaluator eval(PASS_COMMAND_ARGS);

Weird that it's not evaling COMMAND_ARGS. You might want to replace the 2nd reference with COMMAND_ARGS.

That function just returns a Boolean- but it looks like it wants COMMAND_ARGS in a proper format. How are you passing the arguments exactly?

Share this post


Link to post
Share on other sites
Posted (edited)
1 hour ago, Schtearn said:

Weird that it's not evaling COMMAND_ARGS. You might want to replace the 2nd reference with COMMAND_ARGS.

That function just returns a Boolean- but it looks like it wants COMMAND_ARGS in a proper format. How are you passing the arguments exactly?

The COMMAND_ARGS are passed via the scripted function per the OBSE documentation:

ar_Erase - erases elements from an array. You may provide a single element, in which case only that element will be erased and only if it is present. Or, you may provide a range in slice notation. Any elements greater than or equal to the lower bound and less than or equal to the upper bound of the range will be erased. If the array is of type Array, elements above the erased elements will be shifted down. Returns the number of elements removed. Users of OBSE 0020 or later may omit the second argument; doing so will erase all elements of the array.

(numRemoved:int) ar_Erase target:array index:arrayIndex
(numRemoved:int) ar_Erase target:array range:slice
(numRemoved:int) ar_Erase target:array

Examples:

	array_var arr
	let arr := ar_Construct StringMap
	let arr["another array"] := ar_Construct Array
	ar_Erase arr["another array"] 0  ; erase element 0 if it exists, higher elements will be shifted down by 1
	ar_Erase arr "begin":"end"  ; erase any elements having keys >= "begin" and <= "end"

The command arguments include more than just the passed parameters.  They include the Script Object and a bunch of other data that all gets parsed out.  The first of which is paraminfo, which has an array of arguments - one for each paramenter, with arg(0) being the first one, which is usually the array.  It then uses the Script Token file to get the array ID of that passed array, and from there it can evaluate and erase.

It evaluates fine when I call it from a script.  That's not the problem.

I can't figure out how to call it (or any of the sub-code) from within a .dll function that is looping through nested arrays of data.  Because the arrays I am evaluating are nested, they are not included in the command arguments as parameters, and therefore cannot be treated as script tokens.  If I could convert the array into a script token type that might work, but I have no idea how to do that.

Edited by saebel

Share this post


Link to post
Share on other sites

In the obse example plugin, there is a function:
Cmd_ExamplePlugin_MakeArray_Execute

After creating a nested array in an array (which you're already doing anyways with your subarrays it seems), it prints the new array ID.

Maybe you can use that pattern to get the array ID's for tiers 2-4.

Share this post


Link to post
Share on other sites
47 minutes ago, darkrumbleking said:

In the obse example plugin, there is a function:
Cmd_ExamplePlugin_MakeArray_Execute

After creating a nested array in an array (which you're already doing anyways with your subarrays it seems), it prints the new array ID.

Maybe you can use that pattern to get the array ID's for tiers 2-4.

That's an interesting idea.  But I don't need the IDs when the arrays are created.  I need them later, when I want to delete an key/value pair from the array, and by that point, they are long gone.

I suppose I could create some form of massive reference array that stored all the array IDs that were created and the context under which they were created.  But I fear that would be just as cumbersome and complex as the nested arrays are.  Assuming that worked, and I was able to use the reference table to find the ID and use that to remove the reference, I would still have to address the issue of removing key/value pairs from the reference table... which I would have to get/store the ID of.  And then I am back to the scripted functions again.  I think ultimately that would end up adding even more overhead to the problem without really improving on the current solution I have at the moment.

Share this post


Link to post
Share on other sites
14 minutes ago, saebel said:

That's an interesting idea.  But I don't need the IDs when the arrays are created.  I need them later, when I want to delete an key/value pair from the array, and by that point, they are long gone.

I suppose I could create some form of massive reference array that stored all the array IDs that were created and the context under which they were created.  But I fear that would be just as cumbersome and complex as the nested arrays are.  Assuming that worked, and I was able to use the reference table to find the ID and use that to remove the reference, I would still have to address the issue of removing key/value pairs from the reference table... which I would have to get/store the ID of.  And then I am back to the scripted functions again.  I think ultimately that would end up adding even more overhead to the problem without really improving on the current solution I have at the moment.

That’s what I was suggesting. That’s how databases work. You define a primary key which you then index. The primary key uniquely identifies each row. The index speeds up the process of retrieving the data. I’ll post a specific example from Oblivion XP when I get back to my game machine in a bit.

Share this post


Link to post
Share on other sites

Continuing with my previous post, in database terms, indexes have various structures, but one of the easiest to understand is the B-tree index. In Oblivion terms, the primary key is often the Form ID because that's always unique, so you would key your array on form ID. Here's the definition of a B-tree index. If you had an index on names and you were looking for a name starting with S, you would immediately skip to the lower half of the tree. In your case, if you include something that you would always know as part of the array, then you can track down the required element quite quickly. I have two examples. The first is from my book stacking script for Imperial Furniture Renovated. My book stacking script sorts books according to size and stacks the largest books first. Unfortunately this is base OBSE script, but you might be able to adapt the process to your situation.

	let itemidx := 0
	let j := 0
	
	let items := player.GetItems 21

	; sort books by size, largest first. Doing this because upper shelves are larger.
	while (itemidx < ar_Size items)
		let item := items[itemidx]
		if (isBook item) && (isScroll item == 0) && (IsQuestItem item == 0)
			if (IsModLoaded "DLCSpellTomes.esp")
				let formID := GetFormIDString item
				let formID := formID[2:7]
				let index := ar_hasKey spellTomes formID
				
				if (index)
					let size := spellTomes[formID]
				else
					let size := GetEditorSize item
				endif
			else
				let size := GetEditorSize item
			endif

			if (sortedBooks)
				let i:= 0
				let inserted := 0
				while (i < ar_Size sortedBooks)
					let book := sortedBooks[i]
					if (GetEditorSize book < size)
						let numStack := player.GetItemCount item
						let j := 0
						while (j < numStack)
							ar_insert sortedBooks i item
							let j += 1
							let i += 1
						loop
						let inserted := 1
						break
					endif
					let i += 1
				loop
				if (inserted == 0)
					let numStack := player.GetItemCount item
					let j := 0
					while (j < numStack)
						let sortedBooks[i] := item
						let j += 1
						let i += 1
					loop
				endif
			else
				let sortedBooks[0] := item
			endif
		endif

		let itemidx += 1
	loop

Sorry, if I put that in a spoiler tag, it breaks.

The second example is the old Oblivion XP kill script. It used to scan cells for all actors and store all the references in an array. It had a special section to detect summoned creatures and it would insert those at the top of the array so that the player would get points for killing the summoned creature as well as the summoner when they killed the summoner. Otherwise they only got points for the summoned creature when they killed the sorcerer.

		while ( actor )
			if ( actor.getDead == 0 ) && ( actor != player ) && ( actor.isEssential == 0 ) && ( actor.getObjectType == 35 || actor.getObjectType == 36 )
				;store reference in array unless it's exceptional
				let tempShort := 0
				let tempString := actor.getName
				if eval ( tempString == "Scattered remains" || tempString == "Bloody organ" )
					let tempShort := 1
				endif
				if eval ( tempString == "Crushed skull" || tempString == "Frozen remains" )
					let tempShort := 1
				endif

				if eval ( sv_Find "mannequin" tempString ) > 0
					let tempShort := 1
				endif

				;if ( tempShort )
					;PrintToConsole "ObXP: Exceptional actor encountered (%z), skipping." tempString
				;else
				if ( tempShort == 0 )
					; give summoners precedence when processing the kills by putting them first
					; in the actor array.  Use actorAttrib array as a holding tank until you determine
					; whether current actor has a summons or not.
					let actorAttrib := ar_Construct Array

					;NPC reference
					let actorAttrib[0] := actor

					;companion in combat with actor?
					let actorAttrib[1] := 0

					;number of party member in combat with actor
					let actorAttrib[2] := 1

					;party level
					let actorAttrib[3] := player.getLevel

					; does current actor have a summoned creature?
					; if so, store summons as an attribute of actor - will need multiple elements for each summons

					let k := 4
					ForEach each <- (actor.GetActiveEffectCodes)
						let index := each["value"]
						if (MagicEffectUsesCreatureC index)
							let effectKey := each["key"]
							let actorAttrib[k] := actor.GetNthActiveEffectSummonRef effectKey						
							let k := k + 1
						endif
					Loop

					; If the current actor has a summons, insert the reference into the top of the array,
					; otherwise append to the end.
					let arraySize := ar_Size actorAttrib
					if ( arraySize > 4 )
						ar_Insert arrayNPC 0 actorAttrib
					else
						let arrayNPC[i] := actorAttrib
					endif

					let i := i + 1
				endif
			endif
			let actor := getNextRef
		Loop

The point of all this is to show that if you include the Form ID as one of the array elements, or another unique identifier, you can use that to find the element you need. Furthermore, you process multi-dimensional, or nested arrays with nested loops. One nasty complication for you is that the first two digits of the Form ID won't refer to the same mod if the player removes a mod. I think you'll need a reference array to store the load order with the mod name.

Share this post


Link to post
Share on other sites

Yeah sorry, Anda's idea is better- but I'm actually attempting to locate where COMMAND_ARGS is defined- i.e. what's the structure of it.

Is arg(0) and arg(1) separated with a comma or space- or something else?

Quote

ExpressionEvaluator eval(PASS_COMMAND_ARGS);

And what's PASS_COMMAND_ARGS? Looking at it in context of the function above it's just a blank variable.

Or is PASS_COMMAND_ARGS some kind of macro?

Share this post


Link to post
Share on other sites

It doesn’t matter what PASS_COMMAND_ARGS is - you can call it whatever you like. You are passing in arguments, so you can call them what you like. I agree that they should have given them a name and stuck with it. I suspect that what they meant is the first reference, COMMAND_ARGS, are the arguments you supply, so PASS_COMMAND_ARGS refers to the arguments you passed in. I do find OBSE’s documentation very annoying in this regard. I think they should be much clearer when it comes to references versus base objects.

Share this post


Link to post
Share on other sites
Posted (edited)

I appreciate the examples and ideas, but I don't see how the script slices can help.  Part of that is because although I understand the purpose of keys and how they are used for creating data bases and relationships (I make a living working with FileMaker), I don't really understand what you are doing with the scripts in terms of how I can apply the techniques to my problem of removing a key/value pair within the .dll.

I already solved the problem of using formIDs as index keys with a GetBaseRef function that strips the mod index from the Ref ld and storing the base RefIds in a child numerical map of their parent mod.  So load order becomes irrelevant.

As I mentioned before, I already have scripted solutions that work.  I just want to port those into the .dll to boost performance, but there is no method I can find that allows me to tap into the equivalent of ar_Erase.

I don't want to have to create an entire set of arrays to track the IDs of the arrays created.  It's cumbersome and problematic.  The array ID data *is* already there and being tracked by OBSE.  I just can't figure out how to get to it using OBSE's internal .dll structures, classes, and methods.

I'm not clear about the difference between COMMAND_ARGS and PASS_COMMAND_ARGS beyond that they are structures that are created from predefined information as well as parameters that are passed when an OBSE function is called from a script.  You have to evaluate it in order to extract the passed parameters and get things like the script object that sent the command, the calling ref (if any), etc., so that you can process it and return a result.  It's a little murky.

Edited by saebel

Share this post


Link to post
Share on other sites

One possible idea that may be the next best thing to ar_Erase is to create a "cloned array", but skip cloning the key/value pairs that are supposed to be dropped, and then replace the original array with the cloned array.  The original array will no longer be referenced, so OBSE will remove it when it handles it's garbage collection routines.

So for example, let's say I have StringMap1 with three key/value pairs ([modA:data],[modB:data],[modC:data]).

I then create a new OBSE StringMap1clone.

I create a loop that cycles through StringMap1.  It grabs the key of each value pair, and tests to see if the mod is in the active load order.  If it is, it copies the key/value pair over to the clone.  If it isn't, then it is skipped.

In this example, let's assume ModB is no longer active.  When the loop finishes, the clone would only have the ModA and ModC key/value pairs.  The original array would then be replaced by the clone.  Something like that should be able to work for the lower tier items when removing invalid/deleted refs, data groups, or specific data keys from the data sets.

I'm not sure about replacing the master array though.  That could be problematic.  But it occurs to me that the array ID of the master array is stored in a game setting global.  So potentially I could swap out that ID with the new one created by the clone (which should be capturable).  I might also be able to use that ID to tap into the EraseElements function.  I haven't tried that yet though.

Share this post


Link to post
Share on other sites

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

×