Not fully understand the UpdateAutodetectedContactNames function?
UpdateAutodetectedContactNames .. that's left over related to the prior changes from your
other thread you know the one were you reported the bug, this action is related to that workaround. I point that out less for you lol, and more for anyone else reading this just so they know it's not exactly related to the changes made to scene related to this thread. Also not a function per say, just the name of an action, it's important we keep the two distinctions since there are indeed functions involved. [;)]
So the action name "UpdateAutodetectedContactNames" is part of an event called "RenameAutodetectedContactsEveryMinute", who's trigger is\was to run every one minute, it relates to the AutoDetect(AIS)on action\event such that it's meant to handle certain things after that runs. What does that mean, that means every 1 minute what's inside "UpdateAutodetectedContactNames" will attempt to run and do changes if needed, those changes will only matter to things\units that AutoDetect(AIS)on set's to be autodetectable.
Now let's walk though the code line by line in my narrator voice [:)] in the comments, some this is going to be boring and probably to repetitive[>:] but I don't know what part you might not be following, so I will try to assume little. Due to forum formatting there may be annoying unintended extra space added as I attempt to break up the code sections because if I don't it'll also mangle the formatting. Now go get yourself a beer first

we're gonna be here awhile. So here goes....
Code: Select all
local gKH={}; --this defines a local scoped table (empty at the moment) called gKH.
-- locally scoped variable are removed as soon as this action is over\finished. ie each minute all the code runs everything here we be re-created
-- so to speak. if you removed the 'local' here then all this stuff would 'stick around' in memory but still actually be re-written every 1 minute.
-- I actually original didn't have it local and didn't even have all this code inside the action instead in a startup routine and global in scope.
-- BUT people get easily confused so because it didn't matter much for this example I threw everything into the every minute action and make it local.
-- What's important you remember is local = goes away when my event or the current scope is over. 'no'-local means stay in memory till CMO restarts
-- or till explicitly removed, ie restarting a scene\new scene doesn't get rid of global.
-- You want to use globals where it makes sense to do so (ie for functions or var that need to stick around for re-use), otherwise keep it local.
gKH.SideGuidCache={}; -- this sets up another table called SideGuidCache inside the gKH table.
-- While this may look like a global definition it's not it's inside whatever gKH is which is local.
-- everything inside gKH goes bye-bye when the event action is done running.
-- It will be populated later with [guid]=name data as quick lookup table when we want to convert side-guids to the actual side's name without
-- having to call VP_GetSide or VP_GetSides and running a for-each to look for matching up names.
-- TLDR: It's used to maximize performance, and reduce complexity believe it or not.
Code: Select all
--This creates a function called buildSideGuidCache inside the gKH table. it takes a parameter called sides
--It populates a guid to name lookup table.
function gKH.buildSideGuidCache(sides)
--run though the list of sides submitted as the sides parameter, code assumes it will be a table of sides.
--each side is expected to contain a side wrapper; or at least something that has both .guid and .name fields.
for i=1,#sides do -- for up to the number of sides in the table do the following
--Set the table entry by key in gKH.SideGuidCache['someguidhere'] to equal the sides name property.
gKH.SideGuidCache[sides[i].guid] = sides[i].name;
end --end marker related to the 'for' statement
end --end marker related to the function statement\definition.
So let's move on to the next set of code, it defines a function called processSides stored inside the gKH table.
It take one parameter named unitType. You can see from the code inside it expects this unitType parameter to be a string, and it enforces that restriction. The purpose of the function is basically to run through ALL sides that exist in the scene, grab all units of type 'Ship' from each of those sides and then check each unit for autodetectable and if each unit also has been detected by anyone yet. If so then the it calls a contact renaming function. Think of it as the master and the other functions as helpers. As I mention in comments at the end of the code this is sledge hammer approach to the problem, written fast as an example work around to the issue described in the original thread.
Code: Select all
function gKH.processSides(unitType)
-- The following if statement line says besides everything else first do what is inside (parentheses) before everything.
-- In Lua parens surrounding something indicate order, and processing logic sometimes very much depends on order.
-- For example if I did not have unitType==nil in paraens then type(unitType) would actually run first during execution
-- and if unitType was nil it may throw an error. But because I know the order is right to left and that short circuiting will
-- happen I can avoid that by making sure it checks for nil first and then short circuits. "short circuiting" you say, WTF does that mean.
-- It just means the Lua interpreter is smart, it knows this is an "or" and it's checking for anything being "true" here
-- if the first thing it evaluates is true then nothing else that follows is going to change that so it's actually skipped.
-- for example in the line below if unitType is actually nil then the part about type(unitType) ~="string" is not even processed.
-- it jumps right to the 'then' part which is what we want because we don't want and error generated by type checking 'nil'.
-- Ok back to actually explaining this..
-- If unitType is nil, or if unitType is not a string then print to the lua log a message and then return nil from the function.
-- a 'return' anywhere in your code stops the function right there and then, return nil and just return are the same thing.
if (unitType ==nil) or type(unitType) ~= "string" then print("processSides(): unitType invalid. Aborting."); return; end
Code: Select all
-- The following line creates 2 local variables called retval and sides, these values are set to whatever returns back from
-- a pcall to the CMO API call VP_GetSides, a function that takes no parameters cause it just gets a list of all sides.
-- First you're wondering WTF is a pcall(), and why does KH seem to use it alot in his code? Glad you asked.
-- There are times where you're going to call a function and an error could happen including and 'interactive' that will
-- blow up your script and stop it from executing, VP_GetSides actually it's one of those...so this is a bad example and pure habit
-- So what Lua offers is something called pcall(), it stands for Protected Call. Protected in the sense that you tell it
-- what function to run for you and any parameters and it will do it for you in a special context, it will then return back to you
-- at least two things as return values, first is true|false .. either the call succeeded or it did not.
-- the second is what the actual function returned if it did succeed, OR if it failed it will return the error message if any.
-- SOOO in the following line after it executes successfully then 'retval' will be true, and 'sides' will be a table of sides.
-- but if it fails 'retval' will be false and 'sides' will either be nil, or a string containing some error text.
local retval,sides = pcall(VP_GetSides);
if (retval ==true) and sides ~=nil then -- IF the call was successful and the sides table isn't nil we should proceed.
Code: Select all
gKH.buildSideGuidCache(sides); --CALL already talked about function above, submit 'sides' table as the parameter for it.
-- it's called here once at the start so that if sides are actually added to the scene over time
-- then it's always up to date, and since this is all local there is no saving the data between runs.
local ulist; -- this sets up a variable called ulist to hold as you might guess a list of units. It's worth noting
-- the variable is defined outside the For statement that is coming, that was intentional.
-- HOWEVER there is a non-breaking bug of sorts later in the line 'local ulist = sides[i]:unitsBy(unitType);'
-- the 'local' is not intended to be there but it works anyway for.. reasons.
-- That bug doesn't actually end up mattering much but in some other case it could.
-- the purpose of defining ulist outside of the for was to save tiny amount of performance re-allocating during a for loop.
-- or if you were doing something else where you wanted ulist to be available outside the for loop scope.
-- Below we're going to pretend the line local ulist = sides[i]... actually says ulist = sides[i]...
Code: Select all
-- This for-loop runs though all the sides. for each side that has units of a certain type it will check if the unit
-- is auto detectable, and if it a contact on some side, if it is autodetectable and a contact on someside it will call
-- another function to handled changing the name of that contact to the real name of the unit.
-- remember all this is to work around a little bug where units that were already contacts on a side and get switched to
-- autodetectable don't get their contact names updated to reflect level 4 detection/classification.
-- So let's begin by looping through all sides, the sides table consists of sides[a numberstarting at 1] = {full side wrapper here}
for i=1,#sides do -- for each side do the following
Code: Select all
-- Remember what I said above we're pretending local doesn't exist here but if it did this would say redefine\setup a local var called ulist.
-- Assign the var ulist a value that is returned by unitsBy("SomeType") call on the sides wrapper.
-- a ':' is method\function reference on a object\wrapper I'm gonna save explaining ":" vs "." for another time
-- but while they can be interchangeable note that for CMO purposes the ":" will mean a instance method\function that belongs to a wrapper.
-- and generally operates upon that specific wrapper instance only, SomeUnit:launch() means call launch on that unit as the context.
-- Here we're calling SomeSideWrapper:unitsBy("Ship"), you can see the documentation for :unitsBy() details but it's awesome!
-- It was enhanced at my request to even take further specific filtering which took a load of pain off Lua users if they know to use it.
-- and performance gains too when needing to get a list of units filtered by something fairly specific instead of having to
-- do it all yourself. But here we really do just want ALL ships, so we don't use the optional subtype filtering.
local ulist = sides[i]:unitsBy(unitType);
Code: Select all
if (ulist ~=nil) and #ulist >0 then -- Says if our ulist is not nil and has a count greater than 0, then continue.
--print("processing: " .. sides[i].name .. " unitcount:" .. tostring(#ulist)); --this was for debugging purposes.
local u; -- Create a local variable inside the scope of the for loop above. it will be used inside the for-loop to come.
-- it does not exist outside this for-loop scope, only downward so to speak.
for j=1,#ulist do --ANOTHER for-loop to run though the unit list, which is a list of units guids and names.
-- Now we're back to our friend Mr. pcall(), in a sample that can indeed fail cause a unit could in theory no longer exist.
-- Here we have pcall call ScenEdit_GetUnit for us, and we tell it pass {guid=ulist[j].guid} as the parameter to the function.
-- Notice we're reusing the 'retval' variable since it already exists way above, and is no longer being used for anything.
-- Here 'retval' and 'u' will get assigned true and the unit wrapper object or false and error msg string on failure, respectively.
retval,u = pcall(ScenEdit_GetUnit,{guid=ulist[j].guid})
-- Remember I mentioned about parentheses and controlling order of evaluation here we use it again multiple times
-- from inner to outer. first (retval == true) is evaluated. then (FirstResult and u~=nil) then
-- (SecondResult and u.autodetectable ==true) and finally ThirdResult and u.ascontact ~=nil
-- Such that if retval is false we stop, then if u is nil we stop, then if u.autodetectable is false we stop
-- Then and only then does it bother checking if u.ascontact is nil.
-- It's the long but needed way of saying getting unit succeeded and unit exists and
-- the unit is presently set to autodetectable and it is a contact on at least 1 side then continue.
if (((retval == true) and u~=nil) and u.autodetectable ==true) and u.ascontact ~=nil then
Code: Select all
-- Call the function to do the name change, pass this unit's name as 1st parameter, pass the units ascontact table as 2nd.
gKH.setContactNamesToUnitName(u.name,u.ascontact);
end -- ends the 'if' statement no 'else' needed unless debugging.
retval,u = false,nil; -- multiple assignment, clear both variables before the next iteration set retval to false and u to nil.
-- it's functionally no different then retval=false; u=nil;
end --ends the ulist related for-loop statement.
end --ends the 'if' related to the nil and count checking on, no need for 'else' unless debugging.
ulist = nil; -- sets ulist to nil\nothing we want it nil at the start of every iteration in case the pcall fails.
-- if we did not do it (and 'ulist=sides[i]...' existed without the local in front of it like we are pretending)
-- then 1 success followed by one failure would produce a case where ulist was not nil, but instead still had values
-- of the last iteration of the loop. The fact I left 'local ulist=sides[i]...' in there by mistake,
-- actually prevents that from happening regardless, in this case anyway.
end --ends the sides related for-loop statement
end -- ends the 'if' related to check if sides exist. normally I would have 'else' just above this to print an error if VP_GetSides failed.
end -- closes the function definition.
On to the next one, this sets up a function called setContactNamesToUnitName, it's used above. It takes two parameters, the first is a name, it's what name will be assigned to a sides's contact object (btw I use 'object' and 'wrapper' often interchangeably). The second parameter is table that contains a units .ascontact table. The function basically just handles the details of making the changes to each contact that exists inside the table that is provided.
There is no technical reason all of this code couldn't have technically been included in the process function as yet another for-loop, but pays to break things into smaller chunks of defined functions of work.
Before we begin it's helpful to know that unitwrapper .ascontact returns a table that has a format that looks like the following (stripped to just show 1 entry):
{ [1] = { guid = '4FH7PU-0HMAIQ71NIM27', side = '50PVO4-0HM6IJ7M6SLSG', name = 'SKUNK #16' } }
That is [1] is the numeric key\index, the value is a table, that table contains a .guid key, a .side key and a name key.
They represent a contact guid (relative to the side), the guid of the side the contact guid is from, and the current name that side has assigned to the contact.
Code: Select all
function gKH.setContactNamesToUnitName(uname,ascontact)
-- We've been though this a couple times now but next line basically says
-- IF uname is not nil, and ascontact is not nil and the internal count of ascontact table is greater than 0
-- then proceed. #table name means get the tables internal count.
-- HOWEVER # only works accurately when all 'keys' for a table are ALL sequential numbers\integers.
-- Somewhere on the Lua forum: http://www.matrixgames.com/forums/fb.asp?m=5012321 post#3
-- I have a post that talks about this as well as pairs vs ipairs from a learning\teaching perspective.
-- moving on... actually while we're here talking about parameter sanity checking i should mention we
-- we could have\should have checked if ascontact is actually of type 'table' and uname of type 'string'.
-- I tend to not bother doing that on non-user facing code where the chance of them being the wrong type vs being nil is umm about nil.
if ((uname~=nil) and ascontact ~=nil) and #ascontact > 0 then
Code: Select all
local retval,cw; -- Define two local variables that exist inside the function and are used in the upcoming for.
for i=1,#ascontact do --Loop though the table that was submitted to this function.
-- Here is our good friend pcall() working for us again to wrap the ScenEdit_GetContact call.
-- Why bother you say shouldn't the contact always exist if it was in the list, so it should never fail right?
-- That is true like 99.9% of the time, and 99.999999% if the game is paused. But it's a multithreaded app with lots going on
-- it is possible that a contact\unit whatever doesn't exist a millisecond or two after it previously did.
-- It's mainly just habit, and I consider it good form when you don't want the error to stop execution of chain of functions.
-- Basically I default to using it unless I have reasons not too - and there are reasons, including lazy days, but I digress.
-- Now back to the the call, it calls ScenEdit_GetContact for us and it requires a side name, and the guid we want to grab on that side.
-- The thing is it's one of the few functions that does not take a guid instead of a name, and all we have in the table is the side's guid.
-- If only we had a up to date quick lookup table to convert a side-guid to a side-name without having to call vp_getsides and
-- loop though looking for a match every time this gets call. OH WAIT... we created that already for just this sort of use!
-- We put gKH.SideGuidCache to use and insert the value of entry for the sideguid we do have which will result in the textual name.
retval,cw = pcall(ScenEdit_GetContact,{side=gKH.SideGuidCache[ascontact[i].side],guid=ascontact[i].guid})
Code: Select all
-- Here we do the usual check again to make sure the call was successful and the return value wasn't nil.
-- technically this works fine, but I should have put parens around retval == true ie ideally it should be (retval==true) and cw...
--
if retval ==true and cw ~=nil then
print("changing " .. cw.name .. " to " .. uname .. " on side ".. ascontact[i].side); --this prints the success to the log for the hell of it.
-- ^if we wanted it to print the textual side name and not the guid change ascontact[i].side to cw.fromside.name
cw.name = uname; -- This actually makes the change. cw.name is the contact wrapper field for the descriptive name.
end --ends the retval\cw checking 'if'
end --ends the for-loop related to checking each contact entry in ascontacts table.
end --ends the 'if' at the top that was checking the parameters and the table count.
end -- ends the function definition.
gKH.processSides("Ship"); -- CALLS gKH.processSides function with "Ship" as the unit type to actually run all this code.
Why the functions and not one large chunk of code that just does it all. Cause that's a mess to follow,manage, and maintain. Breaking things it into smaller manageable pieces is almost always the way to go where you can anyway. It also often makes things become somewhat re-usable even if that's not the upfront intent or much of a consideration when you're writing it. It sure makes copy\pasta\change easier in the future.
Suppose you wanted it applied to only Aircraft? Well just change "Ship" to Aircraft, or "Ship" to "Weapons", and if you wanted it to apply to everything you could just call it once for each type of unit. Now that I think about it there is a undocumented feature that shouldn't be relied on but I like that it's there, it's that if you feed it explicitly a nil unittype, not just wrong one, ie :unitsBy(nil), it will return everything. So in theory you could actually process every unit on all sides in the scene if you did gKH.processSides(nil), but our parameter checking actually stops you from doing that, but that's an easy change to remove it or make it convert say "ALL" to nil.
Parting thought about gKH being defined as local... you see originally for you I actually had everything global and in an OnSceneLoaded triggered action. All that was in this action was the last line gKH.processSides("Ship");.
BUT because I didn't want to complicate the sample scene more than technically needed. You see because everything involved in the above doesn't actually need to be global to work, that it's only being called once a minute, and that it is not performance heavy (in the simple scene) at least I copied everything back into this action and just stuck a 'local' in front of gKH. Which is just my own personal namespace preference for my stuff. I mention this because you will almost always see my uses of gKH in the global context. What it actually highlights is the flexibility in Lua if you store your stuff inside a table. Tables in Lua aren't just for 'data' storing some strings or numbers. They can are a building block for all sorts of things you might want to do and can be manipulated to work in all sorts of creative ways. That said you don't have to know beyond the basics for use with Lua in CMO. Eh back to my point which was because I stored everything inside gKH, I was able to change the whole thing to run non-globally by changing 1 line of code from 'gKH={};' to 'local gKH={};' and copy\paste\delete from one action to another. I didn't have to change anything else. Now in retrospect had I known I was going to come back to this same sample again to add stuff that did have to have a SceneLoaded trigger and OnSceneLoaded action and have globals for all the other changes made for THIS thread topic, than I would have left just everything as global and in the startup action I originally had but removed (lol and then added back for this thread's changes). Confused yet? lol
In fact since that exists now in the sample in post #2, you could if you wanted cut everything out of UpdateAutodetectedContactNames
except for the last line, and paste it into the top or the bottom of the "OnSceneLoaded" action, and just change 'local gKH={};' to 'gKH={};'. Everything would work just the time, only technically a little fast because all the code above except for the last line global run at startup and not have to be compiled every 1 minute. Nothing, in theory, should break, in fact I encourage you to try the exercise cause that's really how it should be.
If fact to take it step further you could then if you wanted remove gKH={}; line, and change everywhere there is a gKH to a gParel (since gParel exists in the changes I made for this thread in the OnSceneLoaded action) then everything would be under one namespace\table called gParel. [8D]
Ok that was alot of typing, in fact it was probably way to much omg, hopefully there aren't too many typos. I hope it helped you more than confused you, or if not someone else who happens upon the thread in understanding something they did not before (though they might have to load the scenes to have full context). If there is any line of code, or any concept you still don't understand\follow or want clarity about just keep asking. little by little it'll add up. If I've confused the hell out of you about something (probably because Too much info) tell me.
I'll look at your update scene and see if I can see anything that stands out lua wise.
Edit: fixed some typos..there are probably more but sleep calls.