Tentacle Emitter Debugging

Started by planetfall, July 19, 2016, 03:57:07 PM

Previous topic - Next topic

planetfall

Good afternoon all.

I'm running into a bit of difficulty creating a new enemy. It's an emitter that spawns subsidary emitters that spread out in a network. That much I've solved... the problem is the mechanics with killing it. How I'd like it to work is that nullifying an emitter with no subsidaries works normally. If you nullify an emitter that does have subsidaries, it will sacrifice one to stay alive. Unless, of course, all the subsidaries have their own subsidaries... then the nullifier blast will propagate on down until it reaches a terminus, and sacrifice that emitter. I've also got this... mostly working properly.

The trouble arises when nullifying multiple emitters at once. Usually it works relatively sanely. However on occasion, a section becomes split from the main network, i.e. "non-terminus" emitters are sacrificed. I'm not sure what the exact conditions are for this to occur, but it tends to happen when nullifying two distant parts of a long chain. I've made a test map where the emitter will just form a single long chain in a way that wouldn't be sensible in a sane map. "But PF, just make the emitters like they would be in a sane map! Duh that's why it's not working." The problem is that this first happened in such a sane map when nullifying two emitters at once - but it took a long time into the map for it to occur. This test just creates a situation where that is more likely (to the point that it nearly always happens.)

I've done a bit of debugging but the trace log is rather inadequate for something of this complexity, so I managed to throw together a substitute that sends the output from multiple CRPLcores to one log. From what I've managed to gather, there seems to be some record-keeping issues where lists are being modified incorrectly and storing bad information. It may also be some oddity with the exact execution order and when the :destroyed function is called (either causing the above or being its own problem.)

Extra eyes to check my work are always beneficial. Anyone up for a delve into some terrible CRPL? :P
Pretty sure I'm supposed to be banned, someone might want to get on that.

Quote from: GoodMorning on December 01, 2016, 05:58:30 PM"Build a ladder to the moon" is simple as a sentence, but actually doing it is not.

GoodMorning

#1
Count me in.

I think I'd use a manager core, though.

Love the concept.

Edit:
I've had a look, and am rewriting to shift functionality to a manager core. I think you have two problems, processing order and list order.

I'm writing a manager, such that the manager spawns a root, and the root spawns from there, with the manager taking care of the family tree.

I'm also using refwrite and refread to use n+1 lists, the extra list recording all the children. This means that it effectively records 3n+1 UIDs, one each for the "all children" list, the "unit's children" list, and the parent's "children" list.
A narrative is a lightly-marked path to another reality.

GoodMorning

#2
Here's a manager script.

It lacks the functions which interact with the tentacle.

Specifically, you'll need to add logic to this to:

  • Spawn the root.
  • Handle damage to the root.
  • Hide, destroy, &c this Core.
  • Handle adding a new node.

The tentacle script will need to rely on this for spawning, knowing when to die, and it's a good place to put anything central. The point is to keep less information.
Note that the destruction of nodes is recursive, but that it takes place at the next recursion up. This means that the root is not destroyed automatically, unless you choose to make it so.


Note: Applying "Bug = Unintended feature", if you hit the old one with two Nullifiers, you have created a new tentacle, which will spawn as much as you wish.

Edit: I'm looking at the original code, playing "spot the bug". However, it looks like it will need some other scripts to help. I'll write in a tombstone script to log the deaths individually, and hopefully find the error.
A narrative is a lightly-marked path to another reality.

planetfall

Okay, I've found what I'm fairly sure is the solution (tested a bit and it seems to work), rendering this thread rather moot and embarrassing. I'll attach my modified script if you'd like to try and break it.

A drowsily composed semi-technical explanation follows:
Spoiler

The first problem I found was in the RemoveFromHistory function. This entire chunk of code had been moved from the destroyed function, since it turned out that function isn't triggered the INSTANT that core is destroyed via script. Therefore it still had a couple of references to "self" instead of "<-uid," where it was assumed that the destroyer and the destroyee were the same core. This led to the hit cores, rather than the sacrificed cores, being removed from Descendants and Children lists. Interestingly the removal from Children didn't make the hit emitters have a higher branch limit, but it did mean they weren't looking at all possible targets for sacrifices, so if they were hit they may have been destroyed even if there was a valid sacrifice.

The second problem was far more insidious (and more of a 'duh' moment.) Suppose we had this line of connected emitters:

0~1~2~3~4~5~6

Now suppose that through some black magic, we hit 1 and 5 on the same frame. Script execution order is based on UID which is based on the order in which the cores are spawned. So 1 runs first and sacrifices 6. 5 runs afterwards and since it has no children to sacrifice, it dies.

What about 1 and 6? Previously, emitters at a terminus had a NullifierDamageAmt of 0 until they spawned a child (and this was thoroughly managed), so the core was destroyed without going through the script. I don't know the exact internals with the :destroyed function. Either it's called the instant the nullifier fires, or the last will and testament of destroyed cores comes before all other scripts executed on the next frame. I'm inclined to believe the former since :destroyed triggers from pressing X in the editor... but confirmation from V would be nice. In any case, all the lists got updated from 6's destruction, so when 1 executes, it sacrifices 5 and carries on. I've removed the NullifierDamageAmt trick for now, but I may add it back in based on experimentation. In theory it should be less processor intensive. We shall see.

Ah, yes, but if you hit 1, 5 and 6 things get a bit dicier. So 6 is destroyed. 1 runs and searches for terminus emitters and finds 5 - but it considers 5 invalid because it's already been hit. I had included this for cases like so:

0~1a
|
1b

if you nullify 0 and 1a on the same frame, the damage to 0 should be converted to a sacrifice, and it shouldn't be wasted on the core that was going to be destroyed anyway. So it would skip over 1a and apply it to 1b.

However in the case of the chain above, the core doesn't have another terminus to sacrifice, so it dies despite having descendants. I had a specific case "if descendants exist but none can be sacrificed, destroy self." This is really weird, though... why did I have any sort of command to destroy the core if it showed as having descendants? Well, I was dead certain that if it had descendants but none that were shown as valid sacrifices, they MUST have all been damaged and just not executed yet. An attempt to compensate for the information arriving one frame late, as it were. Well... why? I could easily just remove that destruction command and have it check on the next frame when all the lists would be resolved for any cores destroyed. So far, no problems.

So, hooray.
[close]

As for your script... I should give it more of a look, but it's quite late right now and I'm slowed down by all the refreads and refwrites even if I get the gist of what it's trying to do (I'd have used lists instead of refreading concat(varname,RecursionDepth) but that's just me personally.) I'm also not sure if the game would take kindly to recursion within a loop. I know do loops are limited to a maximum of 3 nested, but the wiki says nothing about while loops... so there may be a similar restriction or not - experimenting to find the nuances of a given command isn't my cup of tea.
Pretty sure I'm supposed to be banned, someone might want to get on that.

Quote from: GoodMorning on December 01, 2016, 05:58:30 PM"Build a ladder to the moon" is simple as a sentence, but actually doing it is not.

GoodMorning

#4
I've been ripping the script apart, several times.
I think the fault was the

<-unit CONST_NULLIFIERDAMAGEAMT GetUnitAttribute eq0 if

and

<-unit CONST_HEALTH GetUnitAttribute 100 eq if


I've come up with a slightly asynchronous but reliable light(er)weight version, by arranging for damage to be passed down the chain, and untaken damage to bubble back up.

This lets the Ancestors list be thrown out, and the core try to pass the damage on to various sub-tentacles.
If it can't, it dies, and throws the damage back at the parent core.

A side effect is that multiple Nullifiers the end of a tentacle (on the same frame) can allow you to kill a greater distance back. This is because the damage is passed.

The Text attribute shows the damage thrown further down the chain, after the "/". Hitting it with multiple Nullifiers at different points in the chain can now do very significant damage.

I also attach a "Tombstone" script, useful for logging destruction variables.

Ignore the manager script, this should outdo it.

Edit: The Tombstones count as Nullifier targets. A one-line change, but it could be of strategic value in testing.
A narrative is a lightly-marked path to another reality.

knucracker

As a note, there is a "Debug" CRPL command.  It will peek (that's peek, not pop) at the stack and log a line to the game's output log.  It outputs this:
Type " " IntVal " " FloatVal " " StringVal

In other words whatever is on the top of the stack will be looked at and the int,float, and string interpretation will be printed.

If you do this in CRPL:
Debug ("Hello World") pop

The output in the game's output log should look something like this:
"STRING 0 0 Hello World"

Note the use of "pop" in the above example since Debug just looks at the stack, it doesn't pop from it.

For Particle Fleet, I may make a command that works like the trace commands, but sends the output to it's own file in the game's settings directory.  That way you could tail that file and have a nice clean rolling log while debugging without in-game gui constraints.  For CW3 you can do that but on the game's log file and the output isn't super tidy.... but it is still useful.

GoodMorning

So that would be output_log.txt under CW3_Data. Good to know for bulk debugging, especially regarding death-activated events.
I look forward to trying out PRPL.

And again, many thanks for the game.
A narrative is a lightly-marked path to another reality.

knucracker

Yep, exactly.
For PRPL I've just added "print" statements that mirror the trace statements.  They do the same as trace except they append to a log file in the game's settings directory.

GoodMorning

Considering this script somewhat further...
I think that tentacles might become "established" over time, growing faster and emitting more. Therefore, there is good reason to take out a higher-level node, despite the possibility of it regrowing somewhere less pleasant.
A narrative is a lightly-marked path to another reality.

GoodMorning

#9
Here is the intuitive "next step" in the networked Creeper process: Spore nodes.

When a new node is spawned, it has a chance (depending on the rest of the network) to become a Spore.
When planting the root, set the desired maximum Spore proportion.

After a Spore node spawns, it will start firing after three times it's normal interval. (In case one spawns early.)

This has the NotPersist removed from the map, and the parent/child setup instead of the ancestor/descendant setup, sacrificing same-frame destruction for efficiency... (Adding a graphical tweak so that tentacles flare as damage is passed will make this a feature rather than a bug artifact.)

Edit:
Another node type: Power.
Looks like a CRPL Core, supercharges it's children. This doesn't get inherited/propagated further.
Supercharged nodes get a PZ, this is destroyed when they are.
Emitters make a multiple of their normal output.
Spores come tougher, fatter and more often.
Tune by altering $PowerFactor

Also, I tuned the specialising criteria to make the specials appear later.

In the older version, something was destroying the Core without the "kill" conditional being activated, but only when it was destroyed by passed damage.
This logic now resides in :destroyed; but the original cause is difficult to pin down (it may be fixed entirely).
The damage now passes back up the chain correctly, allowing a limb to be injured by overkill on the tip. This will help against isolated branches, rather than trunks, although it helps the paring rate either way.

I haven't tested the modifications combined with the brainy form.

As a bonus, there are now more options for tracing. The tentacles can have the trace logs turned on and off by keys, and the Tombstones can be removed while paused.

What do you think?
A narrative is a lightly-marked path to another reality.