#Starlancer AIFix v3.13.2 | EnemyEscape v3.0.0

1 messages ยท Page 5 of 1

faint copper
#

it's just more prone to errors

tulip vault
#

which is just a .clear at the start of the round?

faint copper
#

idk, up to you guys shrug

#

I would personally avoid that kind of thing if I possibly can, because it's a lot easier to mess it up than to make something fit into the component model Unity uses

#

the only other reason to do that is if you actually are doing this kind of thing in performance-critical code, but this should only run a few times when the AI is near the doors

#

basically it would just be a component where you can check the distance to the entrance yourself on a similar interval to the one the AI uses, then teleport the AI when you want to

#

no patches needed hopefully

#

except for instantiating it at Start()

gleaming robin
#

I may have completely misunderstood the direction you were guiding me in initially, but here's what I've got so far

gleaming robin
faint copper
#

that's about right, but that function doesn't need to exist, you can just do (Time.time - [...].timeAtTeleport) < x to check it, and [...].timeAtTeleport = Time.time; to set it

#

also you have the operands backwards for getting the delta

tulip vault
#

getcomponent in update is cursed

faint copper
#

wait maybe I misread

gleaming robin
faint copper
#

oopsie

tulip vault
faint copper
gleaming robin
faint copper
#

GetComponent<>() and Dictionary.TryGetValue() are probably not that different in performance if there are so few components as the AI tend to have

#

I don't know if it cleans up the table so that the buckets aren't occupied

tulip vault
#

the trick is not using tryget

#

fair tho

faint copper
#

whut?

#

what makes you think that TryGetValue() is any slower than []?

tulip vault
#

idk i just figured it was

#

ngl

faint copper
#

they're both functions

tulip vault
#

does more ig

faint copper
#

they both do essentially the same thing

#

[] just throws an error if there is no item instead of returning false

gleaming robin
#

Dictionaries are another thing I haven't dabbled in lol
but if I move the patch inside the component, then the timer can just be set and read completely internally?

faint copper
#

yeah

#

accessing the timestamp from inside and outside is the same when it's public or internal though

gleaming robin
#

yee, but no GetComponent needing to be ran

faint copper
#

so the main thing is that you don't have to associate your timer to the EnemyAI manually

#

exactly

#

you can cache the EnemyAI in your component and act on it without any extra cost

gleaming robin
#

Does this method have to be a coroutine in order to implement a Wait?

#

Or is that the wrong way to go about this, I want to call on it every 3 seconds

faint copper
#

same way you check whether you can teleport is how you can determine whether to run your interval function

#

good ol Time.time

#

you could make it a coroutine, but that has to go through an event loop and stuff, idk if it's better or worse for this type of thing

#

that would be pretty difficult to profile since both events are handled in the runtime

gleaming robin
faint copper
#

to which method?

gleaming robin
#

For example I'm calling it TeleportMethod()

#

moving everything into the StarlancerTimer component (which I'll rename)

faint copper
#

you shouldn't be calling any methods from outside your component, just use the void Update() Unity event to do everything

gleaming robin
#

ahh I see, my question still stands of should I be passing EnemyAI __instance to that? Or how do I access the game object that the component belongs to?

faint copper
#
public class TheComponent : MonoBehaviour {
  private const float UpdateInterval = 0.2f;

  private EnemyAI enemy;
  private float lastTeleportCheck = float.NegativeInfinity;
  private float lastTeleportTime = float.NegativeInfinity;
  private void Awake() {
    enemy = GetComponent<EnemyAI>();
  }
  private void Update() {
    if ((Time.time - lastTeleportCheck) < UpdateInterval)
      return;

    lastTeleportCheck = Time.time;
    [do magic]
  }
}
gleaming robin
#

it's so simple when I see it laid out, and my brain is like "I already knew how to do that why couldn't I think of ittttt"

faint copper
#

yeah so basically you just hook into EnemyAI.Awake() and add this component, then it does everything else

#

you could also make subclasses and override some virtual methods if you want to make specific conditions for different enemy types (like current behavior state etc)

gleaming robin
#

I'll be borrowing those variable names btw ty

faint copper
#

I will say though, subtracting negative infinity might result in NaN, so I'm not sure if that condition will actually trigger at first (I always forget the comparison rules for NaN)

#

np lol

gleaming robin
#

Well I certainly wouldn't know lol

#

Should I do all the setup in Awake rather than Start?

faint copper
#

it doesn't really matter here

gleaming robin
#

Of my component, I mean

#

kk

faint copper
#

either will work, they'll essentially run at the same point for your purposes

#

Awake() gets called immediately inside AddComponent() iirc, but either one will run before Update()

gleaming robin
#

okie dokie

gleaming robin
# faint copper I will say though, subtracting negative infinity might result in NaN, so I'm not...

In comparison operations, positive infinity is larger than all values except itself and NaN, and negative infinity is smaller than all values except itself and NaN. NaN is unordered: it is not equal to, greater than, or less than anything, including itself. x == x is false if the value of x is NaN. You can use this to test whether a value is NaN or not, but the recommended way to test for NaN is with the isnan function (see Floating-Point Number Classification Functions). In addition, <, >, <=, and >= will raise an exception when applied to NaNs.

#

So I think it'll be okay?

faint copper
#

honestly I think I was overthinking it by thinking that it was necessary to use infinity

#

I'm pretty sure Time.time starts at 0, so by the time you're in-game, it'll be quite a bit larger than 0

#

those can just be assigned 0

gleaming robin
gleaming robin
faint copper
#

yeah sounds good to me

#

it is in C#, yeah

#

I have ptsd from the undefined behavior of uninitialized variables in C++

gleaming robin
#

lmao

tulip vault
gleaming robin
faint copper
#

oh wait, 1.0f - float.NegativeInfinity is float.PositiveInfinity, no NaN! that's cool

faint copper
#

anything that's a class is nullable by default and default(class X) is always null

#

not exactly sure why strings are reference types, but they probably had a good reason for that (I hope)

faint copper
#

you said you wanted a cooldown on the actual teleport, right? that's what that's for

gleaming robin
#

oh right

faint copper
#

the lastTeleportCheck is just to make it not run this code every single frame

gleaming robin
#

riiiight

#

It might be bad syntax, but I'm curious, does this set both variables to Time.time ?
lastTeleportTime = lastTeleportCheck = Time.time;

faint copper
#

yeah

#

it's not a magic special syntax, though

#

the expression x = (expression y) evaluates to the evaluated value of (expression y)

#

when you chain those, you're really saying
x = (y = (z = (w = 1)));

#

so by extension, you can do

Thing x;
if ((x = GetThing()) != null) {}
gleaming robin
#

How are you getting it to show up like actual code in discord?

faint copper
#

'''cs but with `

#

I can't put that into code tags it seems lol

gleaming robin
#

hrm

faint copper
#

you have to put a newline after the triple apostrophe and its language

rocky fable
#

@gleaming robin Working on a big update for AI Fix are you?

#

I saw you ping me earlier

#

๐Ÿ˜†

faint copper
#
bool thing;
if (thing) {}
gleaming robin
#
private void Update()
{
    if (((Time.time - lastTeleportCheck) > UpdateInterval) && (Time.time - lastTeleportTime) > CooldownTime)
    {
        if (enemy.isOutside)
        {
            for (int i = 0; i < outsideTeleports.Length; i++)
            {
                if ((outsideTeleports[i].transform.position - enemy.transform.position).sqrMagnitude < 1 && UnityEngine.Random.Range(0, 100) < 100) //Replace with variable chance to enter facility
                {
                    enemy.transform.position = insideTeleports[i].entrancePoint.transform.position;
                    enemy.SetEnemyOutside(false);
                    int nodeIndex = UnityEngine.Random.Range(0, enemy.allAINodes.Length - 1);
                    enemy.favoriteSpot = enemy.allAINodes[nodeIndex].transform;
                    lastTeleportTime = Time.time;
                    lastTeleportCheck = Time.time;
                    logger.LogInfo($"{enemy.gameObject.name} teleported inside; Switching to interior AI. Setting Favorite Spot to {enemy.favoriteSpot}.");
                }
            }
        }
        else
        {
            for (int i = 0; i < insideTeleports.Length; i++)
            {
                if ((insideTeleports[i].transform.position - enemy.transform.position).sqrMagnitude < 1 && UnityEngine.Random.Range(0, 100) < 100)
                {
                    enemy.transform.position = outsideTeleports[i].entrancePoint.transform.position;
                    enemy.SetEnemyOutside(true);
                    int nodeIndex = UnityEngine.Random.Range(0, enemy.allAINodes.Length - 1);
                    enemy.favoriteSpot = enemy.allAINodes[nodeIndex].transform;
                    lastTeleportTime = Time.time;
                    lastTeleportCheck = Time.time;
                    logger.LogInfo($"{enemy.gameObject.name} teleported outside; Switching to exterior AI. Setting Favorite Spot to {enemy.favoriteSpot}.");
                }
            }
        }
    }   
}```
faint copper
#

don't forget to close it!

#

there you go

gleaming robin
#

yup lol

gleaming robin
faint copper
#

I'm surprised it let you send, it normally interprets Enter as a newline until you close it

gleaming robin
#

it didn't, I was clicking send

rocky fable
faint copper
#

I don't have a send button lmao

gleaming robin
faint copper
#

hopefully the only one if LethalEscape isn't maintained and you can come to feature parity where it matters, honestly

#

the fact that SetEnemyOutside() isn't called in LethalEscape will inevitably become a problem for compatibility for me and possibly other mods

rocky fable
#

Cus LethalEscape is kind of buggy

#

c;

#

We need the Starlancer version Starlancer is the goat

gleaming robin
#

I am nothing without the community that helped teach me lol

#

How does that look btw @faint copper ?

faint copper
#

your Time fields are both doing the same thing

#

you need to set lastTeleportCheck immediately upon discovering that (Time.time - lastTeleportCheck) > UpdateInterval

#

and also set it even if !((Time.time - lastTeleportTime) > CooldownTime)

#

as it is currently, they're both just acting as a teleport cooldown

gleaming robin
#

yep yep

faint copper
#

and all of the code will otherwise run every single frame

#

also use the early return so you don't have to indent so far!

#

(like #1206494982521753620 message)

#

skipping code via control flow statements like return, continue, break will save you a lot of tabs

gleaming robin
#

yee, though I don't manually indent lol, VS auto-indents for ifs, fors, etc.

faint copper
#

oh sure, but it still pushes your code closer and closer to that scroll bar

gleaming robin
#

yee

gleaming robin
faint copper
#

yeppers, that looks about right to me

#

the cooldown check can also be an early return

gleaming robin
#

yee, also I didn't actually do a calculation there lol

faint copper
#

two tabs down, one to go

#

oh yeah

#

I missed that lol

#

I'm not in the code review mentality

gleaming robin
#

I can make it sneaky by putting return on the same line as the checks as well lol

#
private void Update()
{
    if ((Time.time - lastTeleportCheck) < UpdateInterval) {return;}

    lastTeleportCheck = Time.time;

    if ((Time.time - lastTeleportTime) < CooldownTime) {return;}    

    if (enemy.isOutside)
    {
        for (int i = 0; i < outsideTeleports.Length; i++)
        {
            if ((outsideTeleports[i].transform.position - enemy.transform.position).sqrMagnitude < 1 && UnityEngine.Random.Range(0, 100) < 100) //Replace with variable
            {
                enemy.transform.position = insideTeleports[i].entrancePoint.transform.position;
                enemy.SetEnemyOutside(false);
                int nodeIndex = UnityEngine.Random.Range(0, enemy.allAINodes.Length - 1);
                enemy.favoriteSpot = enemy.allAINodes[nodeIndex].transform;
                lastTeleportTime = Time.time;
                lastTeleportCheck = Time.time;
                logger.LogInfo($"{enemy.gameObject.name} teleported inside; Switching to interior AI. Setting Favorite Spot to {enemy.favoriteSpot}.");
            }
        }
    }
    else
    {
        for (int i = 0; i < insideTeleports.Length; i++)
        {
            if ((insideTeleports[i].transform.position - enemy.transform.position).sqrMagnitude < 1 && UnityEngine.Random.Range(0, 100) < 100)
            {
                enemy.transform.position = outsideTeleports[i].entrancePoint.transform.position;
                enemy.SetEnemyOutside(true);
                int nodeIndex = UnityEngine.Random.Range(0, enemy.allAINodes.Length - 1);
                enemy.favoriteSpot = enemy.allAINodes[nodeIndex].transform;
                lastTeleportTime = Time.time;
                lastTeleportCheck = Time.time;
                logger.LogInfo($"{enemy.gameObject.name} teleported outside; Switching to exterior AI. Setting Favorite Spot to {enemy.favoriteSpot}.");
            }
        }
    }
}   ```
tulip vault
#

i might be misunderstanding the code but you could put returns here to avoid iterating through teleports when you've already found one you like right?

gleaming robin
#

you're 100% correct lol

#

it wouldn't really matter since there's usually <4 teleports, but you're right

#

@rocky fable you wanna test it while I go do chores?

rocky fable
gleaming robin
#

Good enough lol

#

lemme just test that the game will load rq actually

#

test on hold

#

@rocky fable still some bugs, but I also have an easy method of testing, so I'll let you relax :P

#

The blob is the one of the best candidates for the test lol

rocky fable
faint copper
#

@gleaming robin would you want the cooldown to start even if the random chance fails? it the enemy stays near the exit, your current code reroll regularly and eventually exit, not sure if you intend that

#

also, while I'm here nitpicking, it's not necessary to set lastTeleportCheck in the loops

gleaming robin
#

I'm gonna move the random variable up so the rest of the function depends on it being true instead of rolling it in the for loop

#

Use some more of that early returning

gleaming robin
#

Okay, so the method is being called, but the enemy's position isn't changing

#

oh wait I might know what the issue is

#

yuuup okay time for another test

#

nope, not the issue

#

Something is forcing them to stay at the facility height for some reason

golden basin
#

What're you testing?

gleaming robin
#

Enemy escaping

#

The method is being called, and the various AI settings I want to change are taking effect, but the transform fails to take

golden basin
#

Is their local position not changing?

gleaming robin
#

Correct

golden basin
#

But the exact same code works for like, masked?

gleaming robin
#

No I'm using different code

#

I'm adding a component to enemies at Start() that runs Update() every x seconds

#

My logs show that it gets to the code for being teleported, but the enemy doesn't actually go outside, they stay at the dungeon root

golden basin
#

Well from what I know the navmesh gets pretty weird when you try changing the position of the enemy, usually doesn't let you know and whatnot

#

Can I see the code?

gleaming robin
#
private void Update()
{
    if ((Time.time - lastTeleportCheck) < UpdateInterval) {return;}
    
    lastTeleportCheck = Time.time;

    if ((Time.time - lastTeleportTime) < CooldownTime) {return;}
    
    if (UnityEngine.Random.Range(0, 100) > 99.99) { return; }

    if (enemy.isOutside) 
    {
        for (int i = 0; i < outsideTeleports.Length; i++)
        {
            if ((outsideTeleports[i].transform.position - enemy.transform.position).sqrMagnitude < 5) //Replace with 1
            {
                enemy.gameObject.transform.position = insideTeleports[i].entrancePoint.transform.position;
                enemy.SetEnemyOutside(false);
                int nodeIndex = UnityEngine.Random.Range(0, enemy.allAINodes.Length - 1);
                enemy.favoriteSpot = enemy.allAINodes[nodeIndex].transform;
                lastTeleportTime = Time.time;
                logger.LogInfo($"{enemy.gameObject.name} teleported inside; Switching to interior AI. Setting Favorite Spot to {enemy.favoriteSpot}.");
                return;
            }
        }
    }
    else
    {
        logger.LogInfo($"Enemy is inside, checking teleport positions.");
        for (int i = 0; i < insideTeleports.Length; i++)
        {
            logger.LogInfo($"Check #{i}.");
            if ((insideTeleports[i].transform.position - enemy.transform.position).sqrMagnitude < 5)
            {
                enemy.gameObject.transform.position = outsideTeleports[0].entrancePoint.transform.position;
                enemy.SetEnemyOutside(true);
                int nodeIndex = UnityEngine.Random.Range(0, enemy.allAINodes.Length - 1);
                
                enemy.favoriteSpot = enemy.allAINodes[nodeIndex].transform;
                lastTeleportTime = Time.time;
                logger.LogInfo($"{enemy.gameObject.name} teleported outside; Switching to exterior AI. Setting Favorite Spot to {enemy.favoriteSpot}.");
                return;
            }
        }
    }
}```
#

Maybe I need to set the transform after altering the AI properties

golden basin
#

Could try that

gleaming robin
#

Nope, even if I try to use UnityExplorer I can't alter their position

golden basin
#

have you tried instead doing it through the rigidbody?

gleaming robin
#

I have a dumb idea, I'm gonna try disabling them, moving them, and then reenabling them

golden basin
#

enemy.gameObject.GetComponent<Rigidbody>().MovePosition(insideTeleports[i].entrancePoint.transform.position); something like that

gleaming robin
#

oh, I'll try that

golden basin
#

i could be getting my variables a lil wrong since i havent looked at it explicitly, but something like that

gleaming robin
#

this is more likely to work I bet lol

#

NRE, gotta use GetComponentInChildren lol

golden basin
#

yeah thats what i was afraid of, finding the rigidbody lol

#

if (UnityEngine.Random.Range(0, 100) > 99.99) { return; } also wut

gleaming robin
#

just a way of guarantee-ing it lol

#

trust in the process

golden basin
#

that's definitely not gona cause desync issues in the future

#

:3

gleaming robin
#

well its on the component, shouldn't that automatically sync it?

golden basin
#

i dont believe so? you'll probably need to make your own random, i just generally dont trust having a system.random or unityengine.random that i didnt make from the start lol

gleaming robin
#

still no luck on the moveposition

golden basin
#

damn

#

dunno then, i might mess with the script's execution order but prolly not

gleaming robin
#

I'll keep at it and report my findings

rocky fable
gleaming robin
#

lmaooooo

#

I'm shocked you aren't using my mod

golden basin
#

clearly you make bad mods

rocky fable
rocky fable
#

Starlancer's mod was an essential before Zeekerss fixed their damn spawns

golden basin
#

i like my theory more

rocky fable
#

Nah

#

I love Starlancer's mods too much for that to ever be the case

#

Man makes goated quality mods

#

Although the bugs with Warehouse were funny

#

I can't wait for LLL to hopefully be stable in the next few days or so depending on how experimental testing goes, don't take this as a given though

tulip vault
gleaming robin
rocky fable
#

Depending on how the experimental testing keeps going

#

LOL

#

I very well know it could be longer

tulip vault
#

ye but the people reading it do not

gleaming robin
#

WHY WON'T THE GAME LET ME MOVE THE ENEMYYY

golden basin
#

try disabling the agent or just doing whtaever :p

gleaming robin
#

yea gonna

#

๐Ÿ‘€ ๐Ÿ‘€ ๐Ÿ‘€

golden basin
#

there we go

#

i did tell you that the navmesh wouldnt let you :p

gleaming robin
#

you tried to stray me from the path

#

oh I just straight up disabled the gameobject lol

golden basin
golden basin
gleaming robin
#

I mean

#

it did lol

golden basin
#

i just dunno how that would affect the AI

gleaming robin
#

it tried to kill me, so seemed fine

golden basin
#

coilhead is like the last one i'd test for its AI changing ๐Ÿ˜ญ

#

more worried about custom enemies tho lol

gleaming robin
#

well I still need to iterate on this a bit anyways, but I'll try out Jester next :P

gleaming robin
# gleaming robin

Also tried a blob, but it didn't take too well to the transform. I think I might know why, but I'll keep working on it tomorrow. Still, at least I have a proof-of-concept

gleaming robin
golden basin
#

you'd have to change the navmesh but it's prolly possible

gleaming robin
#

You're 100% right about it being a navmesh issue, but Unity did miraculously consider this situation, so now I know what to use instead

golden basin
#

lol nice

#

and ye, i know its navmesh cuz i've tried to mess with my enemy's position before

gleaming robin
#

Ty for your help Xu

golden basin
#

its weird, if u repeatedly change the position of an enemy in like Update() the position will change

#

or atleast the "body"

#

but not if you only do it once

gleaming robin
#

Good ol engine quirks

faint copper
#

it might help if you find the closest navmesh node

#

i.e. RoundManager.Instance.GetNavMeshPosition(targetPos, RoundManager.Instance.navHit, 2.7f) is used by vanilla to find the closest valid position on the navmesh within 2.7 meters

#

if it fails, it'll return targetPos, and if that happens setting the position will likely fail

#

but if Warp() works then you're probably good anyway

gleaming robin
#

I'll keep it in mind!

gleaming robin
#

@golden basin @faint copper

#

gonna try implementing the GetNavMeshPosition so the blob won't be in the floor to start

#

Have blobs and coilheads always spammed the log with "Setting Destination[...]" messages?

golden basin
#

Don't think so?

gleaming robin
twilit drift
#

oh, that

gleaming robin
#

Also from what I can tell this blob weirdness is due to their fluid simulation or something preventing the whole mesh from warping, so the mesh is stuck in the ground until it arbitrarily frees itself

#

Ugghh I don't wanna make a config for this ๐Ÿ˜ญ

gleaming robin
gleaming robin
#

Well for the rest of the enemies it seems like a success! This is without StarlancerAIFix present. I'm gonna end up making AIFix a hard dependency though, since certain enemy behavior can still mess up when outside, like the Springman slide and Jester not properly attacking

tired ore
#

you're a saint

gleaming robin
#

Initial release will probably just not support blobs until I can figure out a good way to shift it in its entirety

tired ore
#

(i doubt there's enough people on the whole earth who would want to let blobs out haha)

#

btw can they also go inside?

gleaming robin
#

lemme just make sure, but it's the same code inverted so should be able to

tired ore
#

๐Ÿ‘€

twilit drift
#

the genius strikes again ๐Ÿ”ฅ

tired ore
#

actually huge

twilit drift
#

is that one day's work?

gleaming robin
#

ye

twilit drift
#

you're fucking goated

gleaming robin
#

technically 6-ish hours work I guess?

#

idr when I started last night

#

but also credit to Zaggy, Batby and Xu for their advice

faint copper
#

weird that the blob doesn't go all together, I thought that those colliders were all children so they would move with it

gleaming robin
#

I'll screenshot its weirdness

faint copper
#

you can just abuse GetComponentInChildren though

#

yeah please do

gleaming robin
#

okay well it seems to be inconsistent actually

#

but here, managed to catch it stuck in its weirdness. Eventually it's like that point got unstuck and it just schlorped into place

#

but that's also proof that it went back inside

faint copper
#

oh, this is it entering?

gleaming robin
gleaming robin
tired ore
#

oh god

faint copper
#

oh god

tired ore
#

does anyone remember that one mod that would make the transition between inside and outside seamless?

faint copper
#

what I would do is collect all those rigid bodies when teleporting blobs and just set all their positions to the navmesh point

tired ore
#

yeye that one

faint copper
#

the blob will start as one tiny blob and then expand

faint copper
#

you can make a virtual function on your component that does the teleporting and then implement a subclass that overrides it for blobs only to use

turbid wave
faint copper
#

not really a good idea if they have trouble with being inside the collision

#

then they may spawn partially in the wall and get stretched out as they move

turbid wave
faint copper
#

they'll expand very quickly if they are all in one point and it'll look more like they entered through the door I think

gleaming robin
#

just gonna test in->out rq since that's necessary for the AI conditions

#

hm, not the solution

#

I'mma try hooking into the BlobAI component

#
if (enemy.enemyType.name == "Blob")
{
    foreach (Rigidbody bone in enemy.gameObject.GetComponent<BlobAI>().SlimeBones)
    bone.transform.position = outsideTeleports[i].entrancePoint.transform.position;
}
#

so this seems to work a little better? I think what it might be doing is still attempting to move to my position inside when it teleports, bc it seems like sitting inside allows it to reorient faster

#

so lemme try resetting its AI on teleport

#

๐Ÿ‘€

#

3 for 3 on the new code

#

let's gooooooooo all I had to do was
enemy.DoAIInterval(); and it works perfectly, no blob-specific code needed

#

@faint copper

faint copper
#

oh?

#

wait, so it was only messed up for less than 200ms after a teleport?

#

I'm not sure why DoAIInterval fixes the bones thonk

gleaming robin
#

no clue, but it does lol

faint copper
#

also I would tend to say you should just do

if (enemy is BlobAI blob) {
  foreach (var bone in blob.SlimeBones)
    bone.transform.position = teleportPosition;
}

also, remember to cache your reused transform positions!

gleaming robin
#

that's the blob DAII

faint copper
#

oh I see

gleaming robin
#

I just call DoAIInterval instead

faint copper
#

strange...

gleaming robin
faint copper
#

definitely doesn't hurt to immediately update the AI target though

gleaming robin
#

I'm also gonna set it to use the transform of the telepoint instead of the entrance teleport itself, as I feel like that could be buggy with the way SpawnSyncObjects works

#

I'm so hype this is working lmao

#

it's such a beautifully simple way of doing it

faint copper
#

yeah, it really doesn't take much, it's very nice

#

might be nice to make that RNG be deterministic though btw

gleaming robin
#
[HarmonyPatch(typeof(EnemyAI), "Start")]
[HarmonyPostfix]

private static void EscapeSetup(EnemyAI __instance)
{
    if (__instance.gameObject.GetComponent<StarlancerEscapeComponent>() == null)
    {
        StarlancerEscapeComponent EscapeComponent = __instance.gameObject.AddComponent<StarlancerEscapeComponent>();
        logger.LogInfo($"Adding EscapeComponent to {__instance.gameObject.name}. It may now roam freely.");
        UnityEngine.Random.InitState(StartOfRound.Instance.randomMapSeed);
    }
}```
faint copper
#

you could store a Random instance inspired by the vanilla EnemyAI search coroutine's

#

wait

#

don't be doing that

gleaming robin
#

no?

faint copper
#

that's global to everything

#

it won't just affect your class, it'll affect anyone else that uses it

#

I didn't even know that was a thing, I hate it

gleaming robin
#

lmao

faint copper
#

making RNG static is kinda gross

gleaming robin
#

So if I wanted to implement that vanilla code for this, I'd make a new system random on component awake and then use that for my rng stuff?

faint copper
#

exactly

gleaming robin
#

I'm not sure exactly what making it deterministic in this case entails

faint copper
#

and base it on either directly on the seed, or based on the location of the enemy

#

it means that the map seed when landing exactly dictates the behavior

gleaming robin
#

base it on either directly on the seed
that's what I was doing before yes? The only issue with it is that it would apply globally?

faint copper
#

so if all player input were the same on the same seed, the enemies would do the exact same thing again

gleaming robin
#

kk

faint copper
#

also it turns out that Zeekers uses the static RNG functions, so by setting the global seed you were changing those things

#

I don't really like that vanilla makes use of that but since it does, it's best not to touch it

gleaming robin
#

So once I have the variable of the Random, how do I use it exactly? Intellisense isn't being super helpful here

#

like say I wanna pick a value from 0 to 100

#

with UnityEngine.Random it's just Range(0, 100)

#

oh is that what Next is for?

#

okay nvm that's the ticket ^^;

faint copper
#

ah, yeah

#

or if you really want a float, you can do Random.NextSingle() * 100

gleaming robin
#

kk

gleaming robin
#

Making enemies escape: Simple
Making the escape chance configurable per enemy: Hell

twilit drift
#

I released a small fix removing the possibility of bumping off steep angles at least for now, and fixing the masked enemy. Also brought Adamance scrap spawn amounts to be still slightly better than March but not on par with the ice moons.
new LC update

gleaming robin
#

Nice

gleaming robin
#

@faint copper if I put this in the Awake(), would this actually work for disabling the component and thus removing the Update() calls?

if (chanceToEscape <= 0) { enemy.gameObject.GetComponent<StarlancerEscapeComponent>().enabled = false; }```
#

Or should I destroy it instead?

gleaming robin
#

it almost didn't occur to me that I don't want to roll the random chance if it's not even close enough to a teleport

faint copper
#

if you're not using it, though, you may as well remove it or not add it in the first place

gleaming robin
#

Mmk. I'll see if I can reconfigure to not add it, I was just not finding immediate results with that so I pivoted :P

faint copper
#

don't forget you can check if it's working with UnityExplorer

gleaming robin
#

ye

#
if (EnemyEscapeConfigDictionary[__instance.enemyType.name].Value > 0 && __instance.gameObject.GetComponent<StarlancerEscapeComponent>() == null)```
#

There we go, that should prevent it from adding the component

gleaming robin
#

@faint copper My understanding was that assigning a value to a dictionary entry would make a key if it didn't already exist, but I'm getting this error. I have a preset list of vanilla enemies and I'm trying to use this code to loop through and generate the config entries

static readonly string insideEnemyList = "Blob, Bunker Spider, Butler, Butler Bees, Centipede, Crawler, Flowerman, Hoarding bug, Jester, Nutcracker, Puffer, Spring";
static readonly string outsideEnemyList = "Baboon hawk, Earth Leviathan, ForestGiant, Manticoil, MouthDog, RadMech, Red Locust Bees";
static readonly string[] vanillaEnemies = (insideEnemyList + "," + outsideEnemyList).Split(',');

internal static Dictionary<string, ConfigEntry<float>> EnemyEscapeConfigDictionary = new Dictionary<string, ConfigEntry<float>>();


//In Awake();
foreach (string enemy in vanillaEnemies)
{
    float probability = configEscapeChanceDefault.Value;
    if (EnemyEscapeConfigDictionary[enemy] == null)
    {
        EnemyEscapeConfigDictionary[enemy] = EnemyEscapeConfig.Bind("StarlancerEnemyEscape", $"{enemy}EscapeChance", probability, new ConfigDescription($"Chance for {enemy} to go into or out of the facility.", new AcceptableValueRange<float>(0, 100)));
    }
}```
Do you see any issues??
faint copper
#

the error would be happening when you EnemyEscapeConfigDictionary[enemy] == null, you need to use a different method to check if the entry exists

gleaming robin
#

ah okay

faint copper
#

the indexing operator doesn't return default when no entry is found, it throws an error

gleaming robin
#

if (!EnemyEscapeConfigDictionary.ContainsKey(enemy))

faint copper
#

yep

gleaming robin
#

noice

faint copper
#

and when you need to use the value immediately, you can do

if (!dict.TryGetValue(key, out var thing))
  dict[key] = (thing = [your default value here]);
gleaming robin
#

So that code creates a key and assigns a default value to the entry if it fails to get the key?

faint copper
#

yeah

gleaming robin
#

Sort of shorthanding my current code?

faint copper
#

if the value exists, then TryGetValue will assign it to thing, if not, you will create the key, assign it to the key and thing at the same time

gleaming robin
#

ohh

faint copper
#

well, you don't immediately need to use the value you're assigning, so no

#

Contains is fine if you don't need tou se your bound config entry

#

one thing I'll say, I'm pretty sure there's a point at which the game knows about every enemy in the game, or at least every enemy that modders are allowing to spawn through the vanilla spawn system

#

if you use that list instead to bind your configs, most modded enemies will show up

#

you'll have to do some digging in ILSpy to find that, but if you search for references to EnemyAI in RoundManager and StartOfRound you'll probably see it in there

#

most likely RoundManager

#

@tulip vault might know exactly where to look and when to hook into that

tulip vault
#

wassup

gleaming robin
#

Is there a way to grab the name of every loaded enemy?

tulip vault
#

Usecase?

faint copper
#

#1206494982521753620 message

#

(the code, not the message, I should specify)

tulip vault
#

SelectableLevel.Enemies - Inside
SelectableLevel.OutsideEnemies & SelectableLevel.DaytimeEnemies - Outside

#

Dunno how youโ€™d do butler bees

faint copper
#

oh damn, so there's no list of all the enemy types? for some reason I thought there was

#

in that case, perhaps it would be good to populate from the SelectableLevel and then lazily bind the rest as enemies spawn?

gleaming robin
#

I think it's fine to just leave the option available to manually input extra enemies

#

That way I'm not messing with other peoples custom enemies too

faint copper
#

but if the default is 0 then you won't be, no?

gleaming robin
gleaming robin
faint copper
#

I'm guessing it's pretty likely that you'll get questions about what the internal names for custom enemies are if you don't make it automatic

faint copper
#

same thing with the circuit bees

tulip vault
#

could just check the highest point of the dungeon and check if the enemy is above or below that when it spawns?

faint copper
#

or actually, I guess it's the nest that doesn't spawn naturally, nvm

faint copper
gleaming robin
#

Correct, this is all for EnemyEscape, not AIFix

tulip vault
#

yeah i know, i meant more for the inside enemy list and outside enemy list

#

ohhh config

#

uh

#

iterate through StartOfRound.levels on ig like RoundManager.Start() ye

faint copper
#

ohh ok I see

#

I assume that it should be lowest priority so that any modded enemies get registered first?

#

and binding config options late into the game won't cause issues, I assume

#

I only ever bind them at Plugin.Awake() so I'm not sure, but it would be good to bind any config entries that are missing when such enemies spawn in so that they can be configured as well

tulip vault
#

do it too early and you'll miss them

faint copper
#

right, lowest priority runs last doesn't it?

#

or am I backwards?

tulip vault
#

i wish i knew

#

docs suck

#

in the longterm you'd have to reference LLL for this to work fully though

faint copper
tulip vault
#

yup

faint copper
#

why does it need a reference though?

#

to hook into some event in LLL or something?

tulip vault
#

upcoming LLL lets enemies dynamically inject into enemy pools depending on dynamic contexts

faint copper
#

ah I see

#

I wasn't sure when that would happen

tulip vault
#

some might not be in the levels when audio knight checks for them

#

because eg. it's not stormy weather rn

faint copper
#

right

#

that's why I think it would be good to lazily bind in both a low-priority RoundManager.Start() postfix and on EnemyAI.Awake()

#

shouldn't require specific LLL dependency if that's how it's done

tulip vault
#

i dont think you understand

#

you can't check for them in roundmanager.start if their not in the pools yet

gleaming robin
#

I could still have a conditional on EnemyAI.Awake() for binding a config if none exists, it's just... messier than being able to pre-emptively cycle through a list on game start and generate the configs right away

#

I want as few dependencies as possible, ideally just BepInEx and my AIFix

faint copper
faint copper
#

one single function call in EnemyAI.Awake() and you have your code nicely split up in your config manager

tulip vault
#

would you want onnetworkspawn instead or is that not relevant here

faint copper
#

Start() should be called on all clients if that's what you mean

gleaming robin
#

But I can just put in the readme (that no one will read) that any modded enemies will populate the configuration upon first spawn

faint copper
#

at least you have the option to say "load into a level and see" instead of having to direct them somewhere else

#

that should find prefabs, so if you run that after all assets are loaded, it should find every EnemyAI instance in the game, unless someone loads an asset after the game starts

tulip vault
#

gross

rocky fable
tulip vault
#

you'd have to ensure thats running after lethallevelloader stuff

rocky fable
#

Is till dunno how that happened

tulip vault
#

otherwise your gonna get the duplicates from levels

faint copper
#

enemies would be deduplicated by name (or type?) anyway

#

as far as knowing when to run that scan, yeah, it is a little ugly

tulip vault
#

still gonna be in lobby

faint copper
#

you mean you do it asynchronously and join any tasks upon opening a lobby?

tulip vault
#

no i mean to reliably find those objects you'd need to be in a lobby

faint copper
#

hmm? why?

#

it would include loaded assets

#

nothing has to be known by the game systems

tulip vault
#

on main menu they should be loaded in memory because they happen to exist in the networkmanager prefab list but idk feels little sketchy

faint copper
#

the assets would be loaded in regardless of whether they're referenced, wouldn't they?

tulip vault
#

no

faint copper
#

or does Unity lazily load assets from within a package?

tulip vault
#

ye

faint copper
#

oh hmm

tulip vault
#

thats why a lot of stuff can only be accessed when you enter a lobby

#

the only real exceptions is the stuff that happens to be referenced in stuff in the networkmanager

gleaming robin
faint copper
#

it seems like the best way to ensure that at least most enemies are found as early as possible

#

fresh load with Scopophobia

#

(it has to finish loading the main menu, though)

gleaming robin
#

So if I did wanna do that, when would I call it?

faint copper
#

I don't know too much about the menu preroll animation but maybe Batby would know?

tulip vault
#

to be 100% safe

#

first update call of gamenetworkmanager.update

gleaming robin
#

Sorry chef, not trying to pull you away from cooking too much lol

faint copper
#

not low priority GameNetworkManager.Start()?

tulip vault
#

otherwise gamenetworkmanager.start postfix as late as possible

faint copper
#

ah yeah

#

I would tend towards low priority Start() since then it doesn't have to run the code every frame

#

no need to add any extra overhead for this

gleaming robin
#

when you talk about priority, is there some property to delay code and allow other stuff to go before it in a method?

#

Or are you just referring to Start() as low priority

faint copper
#

[HarmonyPriority(Priority.[...])]

gleaming robin
#

Ahh okay

faint copper
#

it defines where it falls in the order of the hooks that Harmony generates for you

#

I'm pretty sure that lower priorities run later

#

ah yeah it does

#

found my old code in OpenBodyCams

rocky fable
#

๐Ÿ˜ฆ

gleaming robin
faint copper
#

sure yeah lol

gleaming robin
#
[HarmonyPatch(typeof(GameNetworkManager), "Start")]
[HarmonyPostfix]
[HarmonyPriority(0)]

private static void BindRegisteredEnemies()
{
    EnemyAI[] enemyAIs = Resources.FindObjectsOfTypeAll<EnemyAI>();
    foreach (EnemyAI enemy in enemyAIs)
        if (!EnemyEscapeConfigDictionary.ContainsKey(enemy.enemyType.name))
        {
            EnemyEscapeConfigDictionary[enemy.enemyType.name] = EnemyEscapeConfig.Bind("StarlancerEnemyEscape", $"{enemy.enemyType.name}EscapeChance", configEscapeChanceDefault.Value, new ConfigDescription($"Chance for {enemy.enemyType.name} to go into or out of the facility.", new AcceptableValueRange<float>(0, 100)));
        }
}```
faint copper
#

I would tend to say you should use Type as a key instead of the name

#

in case anyone creates enemies with the same name

#

not a huge deal though

#

I'd also separate the meat of that loop into a separate function and call it from EnemyAI.Awake() as well

#

that way if the enemies somehow aren't loaded immediately you'll still get them in the config eventually

gleaming robin
#

both fair points, but then I have to go and rename the preset lists lmao

faint copper
#

preset lists?

gleaming robin
#
static readonly string outsideEnemyList = "Baboon hawk,Earth Leviathan,ForestGiant,Manticoil,MouthDog,RadMech,Red Locust Bees";```
#

not actually a big deal

#

just an annoyance :P

faint copper
#

what are you using those for currently?

#

(also, why are those not string[] thonk )

gleaming robin
#

So players can pick from "all, list, inside, outside" as presets
and bc I was following a piece of someone else's code and didn't think about it :P

faint copper
#

oh I see

#

it'll be much easier to use those if you make them into arrays

gleaming robin
#

yea

faint copper
#

and arguably even more so if you do Types

#

only problem is you'd have to differentiate your config entries for duplicates

#
private static Dictionary<string, int> DuplicateEnemyNames = [];

public static float RegisterEnemy(EnemyAI enemy) {
  var enemyType = enemy.GetType();
  if (!EscapeChances.TryGetValue(enemyType, out var escapeChance)) {
    var configNameBuilder = new StringBuilder(enemy.enemyType);
    if (DuplicateEnemyNames.TryGetValue(enemy.enemyType.name, out var currentValue)) {
      configNameBuilder.Append(" ");
      configNameBuilder.Append(currentValue);
      DuplicateEnemyNames[enemy.enemyType.name] = currentValue + 1;
    }
    else
    {
      DuplicateEnemyNames[enemy.enemyType.name] = 1;
    }
    configNameBuilder.Append(" Escape Chance");
    EscapeChances[enemyType] = Config.Bind(..., configNameBuilder.ToString(), escapeChance = defaultEscapeChance, ...);
  }
  return escapeChance;
}

void EnemyAI_Awake(EnemyAI __instance) {
  var escapeChance = ConfigManager.RegisterEnemy(__instance);
  if (escapeChance > 0) {
    [do component]
  }
}
#

oops I just wrote a bunch of code on discord

#

I think something is wrong with me

golden basin
#

yeah smoething is 100% wrong with you

faint copper
#

it's true

gleaming robin
faint copper
#

that was for if you switch to using EnemyAI subclass type

#

but also I just realized that maybe some will reuse the class for different AI prefab instances with different behaviors so maybe that's not the best idea

gleaming robin
#

I think it'd be better (read: Less stress for me) to just do it via enemyType.name. If multiple enemy mods come out with the same name, they can easily just append their author name to its enemyType.name like StarlancerForestGiant or w/e

faint copper
#

yeah, agreed

gleaming robin
#

Which they should do anyways for ease of identification in the ecosystem

tulip vault
gleaming robin
gleaming robin
#

bc you're right, all of the custom content mods are shifting to LLL as it is so I might as well make things simple ig

tulip vault
#

thats only in the experimental rn if your still keen

#

can do vanilla for now tbh won't be hard to redirect

gleaming robin
#

okay, so pretty much like we've already discussed will be a solid foundation?

tulip vault
#

ye more or less

gleaming robin
# faint copper it'll be much easier to use those if you make them into arrays

If I switch them from a string to a string[], how do I keep the configuration method simple for the user? Right now in its list-style the user would just have to follow the format presented for adding enemies, but wouldn't I need to implement some back-and-forth conversions from array to not-array to not make it complicated to modify for the end-user? Or am I just missing a piece of knowledge here

faint copper
#

well I guess it depends on how your config is set up

#

are you planning on having a list of enemies in your config for some reason? I thought you were just binding options for each enemy type

gleaming robin
#

That was another thought in my head was to forego any manual addition of enemies entirely and just do the grab method

#

Which if it's what I'll migrate to with LLL then I might as well cement the methodology

faint copper
#

I mean there's no reason to add them manually

gleaming robin
#

tru tru

faint copper
#

you already have it generating config bindings for every enemy in the game, at least if you did the thing in EnemyAI.Awake() like I suggested

#

the whole point is to remove the need for it to have two layers to it in the config

gleaming robin
#

Yep, I just love/hate deleting code I've spent a decent chunk of time on :P

faint copper
#

also I wonder if it's necessary to hook into LLL? maybe as an additional layer to allow custom enemies to be registered in the main menu reliably it could make sense, but why depend on LLL when you don't know whether there will even be custom enemies?

faint copper
#

the more okay you are with deleting bad code the better your code will be

#

I used to be that way too, but since I started to let go of that things have been much easier

#

I'll write whole libraries and then realize I have to throw out half the code because it just doesn't work well enough, even if it was something small it's still worth tossing if you know something you didn't before

#

coding is learning

#

any code you're writing without fully understanding the problem, you can prepare for the worst case scenario where you'll need to start over and tehn it won't feel nearly so bad

gleaming robin
#

Yee, all valid.
Hooking into LLL would present a surefire way to grab everything at once, no need to wait for spawn-binding, so the config could be fully tweaked sooner from the user's end
But you're right that if someone isn't using any custom enemies (or moons/interiors with custom enemies) that there'd be no need to

faint copper
#

Hooking into LLL would present a surefire way to grab everything at once
I'm not sure how that would work any differently than the Resources.FindAllObjectsOfTypeAll(), that function just needs to be called at the right point in the loading sequence and then it'll catch everything without any dependency

gleaming robin
#

ยฏ_(ใƒ„)_/ยฏ
I also am not sure ๐Ÿคญ

faint copper
#

if there's a point at which LLL can guarantee that all the EnemyAI prefabs have been loaded, then there should also be a point in the vanilla code where that would be the case

#

so making a dependency, whether it be soft or hard, seems pointless to me

gleaming robin
#

Regardless, the good news is that my manual config assignment is now only two things ๐Ÿคญ

configManualEscapeChance = Config.Bind("EnemyEscapeSettings", "ManualEscapeChance", false, new ConfigDescription("Assign escape values manually?"));
configEscapeChanceDefault = Config.Bind<float>("EnemyEscapeSettings", "EscapeChanceDefault", 10, new ConfigDescription("The default chance for enemies to go into or out of the facility.", new AcceptableValueRange<float>(0,100)));```
#

actually I'll just remove the "assign values manually" toggle and just let people go ham

faint copper
#

yeah I was gonna say, I'm not sure what that one is for

#

and what about having a Dictionary of certain enemies that should have a default non-zero escape chance so that you can have it do something you consider to be fairly balanced out of the box?

#

i.e.
Dictionary<string, float> DefaultEscapeChances and Config.Bind(..., DefaultEscapeChances.GetValueOrDefault(enemy.enemyType.name, 0), ...)

#

that way, if you feel like it's nice to have say only the blob and yippee leave by default, you can do

Dictionary<string, float> DefaultEscapeChances = {
  { "Blob", 0.25f },
  { "Hoarding bug", 0.25f },
};
#

and every other enemy will have zero chance

#

you could even make selectable presets if you had a Dictionary<string, Dictionary<string, float>> if you really wanna go nuts

gleaming robin
#

lmao

#

How does GetValueOrDefault() work in this case?

gleaming robin
#

ohh default float is 0

faint copper
faint copper
#

nice nice

#

the Microsoft docs are actually good unlike the Unity ones so it's definitely worth checking them if you wonder whether you can do something fancy shmancy

gleaming robin
#

bc if so then I'm heavily annoyed bc .net 8.0 in VS doesn't like me for some reason

faint copper
#

you should use .NET Standard 2.1

#

what version are you using now?

gleaming robin
#

Framework 4.8
idr why that specific one exactly

faint copper
#

that would make sense

#

.NET Standard 2.1 is what the game targets though, and it supports a whole lot of nice syntactic sugar that you won't get on Framework 4.8

gleaming robin
#

mmk, can I just retarget it in the csproj?

faint copper
#

and you'll also be able to use .pdb files to get line numbers if you target Standard 2.1

#

probably? I actually converted my projects over by manually editing the .csproj

#

can't hurt to try just switching it in the UI though, you should be able to undo if it blows up

#

especially if you're on version control

gleaming robin
#

all it shows me is framework versions lol

faint copper
#

oh, you found a way?

#

nice

faint copper
gleaming robin
faint copper
#

oh I see I see

#

in case you want to reference what I did to mine

#

looks like git didn't want to generate a very pretty diff for you though...

gleaming robin
#

that's certainly a lot of words, characters, and numerals

#

I guess a configurable "default chance" is actually redundant

#

if I'm gonna have a preset for vanilla anyways, and people will wanna tweak custom enemies themselves

faint copper
#

what you could do is have an invalid value as a placeholder for using the preset values too

#

so like
normal values range from 0.0 to 1.0, and anything < 0 is considered invalid and will take the preset values

#

that way the presets won't require you to erase your config

gleaming robin
#

I'm not understanding you. Each Config.Bind() I generate has an acceptablevaluerange property (currently 0 to 100 just cuz), so how would an invalid value get assigned?
When you say "erase your config" do you mean the presets would have to regenerate the config, or are you referring to replacing code?

#

ohhhhhhhhhhhhhhhhhhh

#

I hardly config stuff to much degree, I didn't realize it would let you put in something incorrect lmao

#

So what I could do is set the default value as -1, and if the value is not in the (0, 100) range, reassign the default preset?

faint copper
#

I'm not sure if it lets you load it if it's outside the acceptable range, but you can certainly change your acceptable range to allow -1

gleaming robin
#

That would make sense lol

golden basin
#

what would happen if you made an offmeshlink between the entrance of an interior to the outside?

#

instead of doing any sort of moving and whatnot

gleaming robin
#

I guess it would work? But it wouldn't be configurable per enemy, it'd be global

#

but in theory sure, you could plop OMLs into a scene at runtime connecting EntranceTeleports of matching IDs

golden basin
#

i vote for all enemy rights ๐Ÿ˜ญ

gleaming robin
faint copper
#

I think an OffMeshLink might look a little weird since they're designed to interpolate their agent across them, but I may be wrong

gleaming robin
#

I believe you are correct

gleaming robin
#

Was looking at the wiki about Butler Bees

Fortunately, though, they cannot leave the facility.

spice latch
#

oh no

gleaming robin
#

wesley have you seen what I've been working on?

spice latch
#

no?

#

maybe?

gleaming robin
#

StarlancerEnemyEscape

#

A fun little add-on mod for the AIFix

spice latch
#

oh dear

#

perfect secret dependency for my moons

gleaming robin
#

lmaooo

#

if only it could be

#

this all sorta happened as a result of Zaggy wanting to be able to hook into SetEnemyOutside for things, so really the blame lies on them

faint copper
#

D:

#

I would never encourage the bees to leave

spice latch
gleaming robin
#

I'd like some input on balancing a default preset.

#

I feel like it's at an okay spot here, especially since most of the time enemies don't wander right by an entrance/exit

#

but if anything feels like it should be increased (or further reduced) lemme know

#

I'm gearing up for another test

#

so hopefully I'll be releasing this soon

golden basin
gleaming robin
#

% chance to use an EntranceTeleport when close enough to it

#

It checks every 1s, and every time it actually rolls the dice to use it, it initiates a 10s cooldown

sullen inlet
#

Why no centipedes? To not rely on PathfindingLagFix by default or something?

gleaming robin
#

Lore :P

sullen inlet
#

Oh wait

#

Right

gleaming robin
#

EnemyEscape will rely on AIFix tho

gleaming robin
#

Around you, there are a blob and a dog, they are in the wrong spots

#

Also I'll just assume no input on the balance means it's good 'nuff

sullen inlet
#

Yeah I see no issue with it

#

maybe bump up Baboon Hawks since they have the whole pack and pair thing going on but that could be argued either way

golden basin
gleaming robin
#

I can't wait to see someone lead a coilhead outside when their friends don't know about it

#

if (enemy.enemyType.name == "Spring" && enemy.agent.speed == 0f) { return; }

#

easy coilhead condition

twilit drift
#

oh no ๐Ÿ’€

golden basin
gleaming robin
#

So make sure it doesn't have a target at all, is what you're suggesting?

golden basin
#

yeah

gleaming robin
#

Cuz I was just conditionalizing it for not being looked at

golden basin
#

no cuz sometimes people deliberately move the coilhead around

#

but yknow maybe it would be a funny feature

#

lol

#

up to you

gleaming robin
golden basin
#

ahh icic

gleaming robin
#

although

#

it'd be even funnier if they think they made it out safely only for the coilhead to come screaming through the door behind them

#

so I will check state

#

if (enemy.enemyType.name == "Spring" && enemy.currentBehaviourStateIndex != 0) { return; }

#

I thought about checking for if 1, but this allows for the (albeit slim) possibility for someone to mod extra behaviour states in

golden basin
#

๐Ÿค”

#

i hate wrapping my head around behaviour logic lol

gleaming robin
#

"if it's a coilhead and it's not idle, don't do things"

#

Zaggy taught me the value of early returns to prevent running unnecessary code lol

gleaming robin
gleaming robin
tired ore
#

huh, I thought the ceiling fan in facility had a killbox

gleaming robin
#

it was still in its spawn animation when it teleported, but idk :P

faint copper
#

@gleaming robin perhaps the EscapePreset should be an enum? that way, you can allow people to select a default with no escape chance for anything or your default preset, and add new ones later

faint copper
gleaming robin
#

dangit Zaggy, I was gearing up for release :P

#

I haven't messed with Enums yet though, so this would be as good a time as any to learn ; w ;

faint copper
#

it's not too complicated

#

they work out of the box with config

#

it's better to make it an enum now than later, because if you change the type of a config option it'll break compatibility with old configs

#

basically, you'll just want to define all the presets within the enum and bind the config to the type of the enum instead of bool

#

then when you load the config option, you'll map each enum value to a preset with a switch statement

#

i.e. if you have an enum ConfigPreset { Disabled, NiceDefaults } make a function

Dictionary<string, float> GetPresetValues(ConfigPreset preset) {
  switch (preset) {
    case ConfigPreset.Disabled:
      return DisabledPresetValues;
    case ConfigPreset.NiceDefaults:
      return NiceDefaultsValues;
  }
}
gleaming robin
#

I see I see

#

I'll come up with a quick thing in a few

gleaming robin
gleaming robin
faint copper
#

if you allow the actual per-enemy values to override the preset, then I don't see the point in an option to enable the presets

#

but if you do need that, just make that another of the enum options

#

no need to add an extra config option when its state nullifies the state of the enum

#

(the point of my suggestion to add a "disabled" config was to have a default where every chance value is zero, so that people can create their own options from scratch)

gleaming robin
#

Would it be able to update the config outside of the game??

faint copper
#

uh

gleaming robin
faint copper
#

I mean, the point is for the presets to be a valid state of the config rather than something that changes all your options

gleaming robin
#

at least in my understanding

faint copper
#
  • Load all config options from the file
  • For any options where the value == -1, replace that internally-used value (not the value in the config) with the value from the selected preset
  • ???
  • Profit
gleaming robin
#

Ahh

faint copper
#

That way, people can start from a preset, modify whatever values they see fit, and then they have whatever specific behavior they want

#

like say they think "wow this preset is almost perfect, but this one enemy going outside is stupid and I hate it" then they can just select that preset, and change the one value from -1 to 0

#

ez

tulip vault
#

audio knight be skilling up like crazy

faint copper
#

the critical part is that the config is never modified by the presets

#

only the values that are used within your scripts

gleaming robin
#

Something about this is hurting my brain lol
Is this method meant to prevent the user from being able to actually specify a value in the config?

faint copper
#

not prevent them from being able to thonk

#

prevent them from having to

gleaming robin
#

Okay, right, that's where I had gotten to before trying to implement the Enum (which I still want to do bc I like multiple presets)

faint copper
#

if you have

Preset=SaneDefaults // where Coilhead escape chance is 10
Coilhead escape chance=0

then it takes all the values from SaneDefaults, except that it will never allow the coilhead specifically to ever escape instead

#

(all other omitted options would be set to the default, -1)

gleaming robin
#

I'm sorry, this must be frustrating for you bc I'm clearly just missing something lmao
So what you're suggesting is defaulting the manual values to -1 on the first run so that presets take precedence by default, yes?

faint copper
#

indeed

#

the default value passed to Bind() would be -1

gleaming robin
#

Would I just want to list the preset values in the readme for the player to actually know what each preset is?

faint copper
#

yeah, probably

#

if you want, you could generate the comments from your code

gleaming robin
#

In this manner?
Preset "Default" = [enemyName:escapeChance], [enemyName:escapeChance], etc.

#

y'know what, that's all aesthetic stuff, I have an actual issue lol

#

I have the Preset method

internal static Dictionary<string, float> GetPresetValues(ConfigPreset preset)
{
    switch (preset)
    {
        case ConfigPreset.Disabled:
            {
                return DisabledPresetValues;
            }
        case ConfigPreset.NiceDefaults:
            {
                return DefaultEscapeChances;
            }
    }
    return DefaultEscapeChances;
}```
#

I bind it

configEscapePreset = Config.Bind<Enum>("EnemyEscape", "EscapePreset", ConfigPreset.NiceDefaults, new ConfigDescription("Which preset to use. If an enemy has a value manually set below, it will take priority."));```
#

but I cannot for the life of me figure out how to actually access that value

faint copper
#

Bind<ConfigPreset>, not Bind<Enum>

#

I would think anyway

#

and to access the value do you mean access the configEscapePreset.value, or access the preset values?

#

if you mean access the preset values, just call the function with the value of the config option

#

and you can wrap the currently-selected preset in another function like

static Dictionary<string, float> GetCurrentlySelectedPreset()
{
  return GetPresetValues(configEscapePreset.Value);
}
gleaming robin
faint copper
#

looks fine to me

gleaming robin
#

oh dammit

#

I see what I did

faint copper
#

although it's better to put the options on multiple lines, I just formatted it that way in Discord because I'm lazy

#

I don't want to imagine what some of my enum declarations would look like if they had to be formatted like that

gleaming robin
#

I had the declaration of the ConfigEntry set to Enum which is why it was yelling at me ^^;
public static ConfigEntry<Enum> configEscapePreset;

faint copper
#

oh yeah, that'll do it

gleaming robin
#

So for custom enemies, I think the best way to go about registering them would be to default to 0 on first run and let the user adjust it later. Since they won't be included in any preset, I can dedicate a config section to mod enemies and restrict their value from 0-100 with a separate conditional binding on GameNetworkManager.Start()

gleaming robin
#

Okay, I think I'm nearing completion. Hoping to get it released in the next hour

#

I've had the mechanics done since this morning, most of my coding today has been on this config ; w ;

#

Opinions wanted: Are there any other enemies besides the Coilhead that should only teleport if they're in their "passive" state (currentBehaviourStateIndex == 0)?

faint copper
gleaming robin
#

I'd make it global, but some enemies like the Crawler don't immediately revert to state 0 upon losing their target, and I'd like for enemies to have a chance to sort of follow the player out

faint copper
#

and that opens you up to including mod enemies in the presets as well

tulip vault
#

question

#

does this go both ways

gleaming robin
#

entry and exit you mean?

tulip vault
#

well enemies can exit the dungeon

#

can enemies enter the dungeon

gleaming robin
#

yea if they happen to get close enough to the entrance teleport

tulip vault
#

do you have a size limit on that lol

gleaming robin
#

Size limit on what specifically?

tulip vault
#

i mean like

#

what if a forest keeper walks up to a fire exit

gleaming robin
#

well my default value (which is attempted to be balanced) has them at 0

tulip vault
#

I see

gleaming robin
#

Custom enemies will also default to 0 as well, so the out-of-the-box state of the mod is fairly reasonable

#

@tulip vault do you think it would cause any problems to instantiate an empty game object at each EntrancePoint tagged AINode/OutsideAINode at runtime so that enemies have a chance to actually path to where they could teleport?

#

If I hook into RoundManager.FinishedGeneratingLevel() I could maybe slide the instantiation into a prefix and then let the original code run, which would ensure that the game finds them for its own node arrays

#

Or better yet, I could just postfix it and add them to the arrays myself

#

Even better, rather than instantiating anything, I can just change the tag of the EntrancePoint

tulip vault
#

change the tag?

gleaming robin
#

yeah, just change whatever the EntrancePoint (usually telepoint in vanilla scenes) is from Untagged to AINode/OutsideAINode

#

It occurs to me that I might wanna instantiate an empty child object tagged SpawnDenialPoint and add it to the RoundManager's spawnDenialPoint array, otherwise a forest giant might spawn in front of the door

#
foreach (EntranceTeleport entranceTeleport in entranceTeleports)
{
    if (entranceTeleport.isEntranceToBuilding)
    {
        entranceTeleport.entrancePoint.tag = "OutsideAINode";
        var spawnDenial = new GameObject();
        spawnDenial.tag = "SpawnDenialPoint";
        spawnDenial.transform.position = entranceTeleport.entrancePoint.transform.position;
        RoundManager.Instance.outsideAINodes.AddItem(entranceTeleport.entrancePoint.gameObject);
        RoundManager.Instance.spawnDenialPoints.AddItem(spawnDenial);
    }
    else
    {
        entranceTeleport.entrancePoint.tag = "AINode";
        RoundManager.Instance.insideAINodes.AddItem(entranceTeleport.entrancePoint.gameObject);
    }
}```
#

oops, forgot to add them to the arrays

#

that's better

gleaming robin
tulip vault
#

could i possibly suggest making a new gameobject for the ainode tag rather than setting the preexisting one?

#

and explicitly naming both new objects to indicite its from your mod

gleaming robin
#

sure

tulip vault
#

for context on that first request, even though its probably never going to matter and your def in a position to make that change, it is technically a destructive edit to the basegame, so if you don't need to do it it's nicer not to

gleaming robin
#

gotcha

gleaming robin
#

hm, just used a blob to test and it kinda got funky again, I really though Warp took care of that

#

I'll add it to the readme as a note

gleaming robin
#

I was hooked into FinishGeneratingNewLevelClientRPC and kept getting duplicates, but hooking into SetLevelObjectVariables instead made it work perfectly โœจ

#

Starlancer AIFix v3.5.0 | EnemyEscape v1.0.0

#

Go nuts, I'm going to bed

#

(Also, big thank you to @faint copper and @tulip vault for all their help!)

faint copper
#

that way you never have dupes

#

but it's good if you found somewhere that runs on all clients and the host where you can set those up

#

(definitely make sure of the clients part, though, run it in LAN and verify that those get created)

gleaming robin
golden basin
faint copper
#

you have to start the game twice in LAN

golden basin
#

yeah

faint copper
#

then join yourself

gleaming robin
#

ohhh that makes more sense

golden basin
#

you could also do multiplayer but much easier to do lan

gleaming robin
#

I'll verify in the morning (or some kind soul can test and let me know)
But it should be good, since FinishGeneratingNewLevelClientRPC() calls SetLevelObjectVariables()

spiral ermine
#

HOLY FUCKING SHIT

#

lethal escape but it works?

twilit drift
flint vector
tired ore
#

It's written a lot better than LEsc also

#

Let's gooooo

warm nexus
#

peak

spiral ermine
#

Oh yea itโ€™s obviously a bit buggy rn

#

In a test with @wary bough we discovered that hoarding bugs when aggroโ€™d inside the facility will follow you outside perfectly fine, but once they get outside they tend to freeze and donโ€™t move in place until like 2-10 seconds of rethinking their life, then they start acting normal again

#

But when trying to lure them back inside using aggro

#

Almost never worked

#

Now my main question about how this mod works is the config itself

#

Letโ€™s say I have a thumper

#

And I set the chance for it to path outside is 30%

#

Does this mean that without aggro on any player, a thumper has a 30% to get basically a pathing command to go outside/inside the facility

#

Or

#

For example it starts charging at a player who then leaves the facility

#

Does that thumper have a 30% of following the player outside

#

If what I said made any sense to anyone

gleaming robin
#

@spiral ermine heard on the bug bugginess
the percent chance is purely for it to actually use the teleport, so in your case if it gets close enough to the teleport during it's charge after you leave, it has a 30% chance of using the teleport IF it's not on cooldown

spiral ermine
#

Whatโ€™s the actual cooldown / system used

#

On how enemies escape or enter

gleaming robin
#

Cooldown is a 10 second timer currently, but I can adjust that
The system is that, every second while not on cooldown, the enemy checks some logic and checks the distance between each teleport point and themselves, if they're close enough, they roll the dice and try to leave

#

I think the issue you were seeing with the lootbug is a design flaw in my code actually

#

I currently have it go on full cooldown if it fails to leave, and that's probably too heavy-handed

#

Here's my current thinking:
Reduce cooldown to 3 seconds, but make it so an enemy only teleports if it doesn't have a target.
I still have to investigate the lootbug behavior tho

spiral ermine
#

How I thought it worked, and maybe should work

#

Is every [Insert Check Cooldown] the enemy will check if within [Insert Configurable Distance] of an entrance or exit point have a [Configurable Chance] to travel to that location and teleport

#

Then

#

A separate check for if enemy has the ability to aggro onto a player
If enemy is aggroโ€™d or is following the player within the last [Configurable Time in Seconds] have a separate [Configurable Chance] to use the nearest teleport

#

Professional idiot speaking here

#

If anything Iโ€™m spitting is absolutely horse water let me know

#

But with the nonsense I just spouted letโ€™s say The Hoarding Bug has a 40% chance if itโ€™s at least 20? ||(I really donโ€™t fucking know the scale system of lethal company now that I think about it)|| units away from either a fire exit or main entrance it will then path towards that teleport and use

#

But then letโ€™s say if you the Brilliant player decides to steal the hoarding bugs favorite key, and it aggros on you.
The hoarding bug then has a 80% chance to follow you out of the exit / entrance you used if it saw you within the past 5 seconds

reef salmon
#

finally, good lethal escape

#

so, how does this work? does it teleport enemies outside when you go outside like lethal escape? or do enemies actually properly go to the doors, like masked?

#

and can enemies go back inside?

gleaming robin
gleaming robin
#

lemme see if I can replicate the hoarding bug issue

#

Okay I think I fixed the hoarding bug, lemme see about implementing a chance to path to the node

spice kindle
#

I suppose the enemyescape doesnt work with modded enemies yet?

gleaming robin
#

It'll automatically detect any enemy that's registered in the game when you get into the main menu and add it to the config, from there if you have LethalConfig you can set the chance in game, else you can quit and modify the config in r2 (or manually if that's your style)

#

I just don't offer any presets for modded enemies :3

spice kindle
#

i am getting this error, which i got for spectateenemies too once, and its because of "dont touch me" enemy

#

this is the full error

gleaming robin
#

Alright, gonna test adding this to my update interval

if ((Time.time - lastPathToTeleport) > PathCooldownTime) //Attempt to path to a nearby entrance.
{
    if (random.Next(0, 100) < chanceToPath)
    {
        foreach (EntranceTeleport teleport in entranceTeleports)
        {
            if (Vector3.Distance(teleport.transform.position, enemy.transform.position) < PathRange)
            {
                agent.SetDestination(teleport.transform.position);
                lastPathToTeleport = Time.time;

                break;
            }
        }
    }
}

//Upon successfully telporting
enemy.SwitchToBehaviourState(0);
enemy.StartSearch(enemy.transform.position);```
spice kindle
#

not really sure what the lethal things gremling enery error is

gleaming robin
spice kindle
gleaming robin
#

ah I missed it, ty

spice kindle
#

i think its because of DTM enemy at least

tired ore
gleaming robin
#

I'll attempt to implement the following in the config bindings next update, should hopefully fix that

Bind[...](enemy.enemyType.enemyName.Replace("[^0-9A-Za-z _-]", ""))```
turbid wave
#

Because pathing to the exit would be needed before they are able to exit. A coilhead would be naturally unable to path while held by looking

gleaming robin
gleaming robin
spiral ermine
#

Itโ€™s ok man down the testing I did it works fine with everything else like butlers

#

Actually I do have another question

turbid wave
#

If this pathing logic ends up working out. Maybe we can get another set of such configs for returning

spiral ermine
#

Because I had 3 major reasons I didnโ€™t use lethal escape

#
  1. Once and enemy escapes, it cannot reenter which you fixed
turbid wave
#

I'd like to set a high range and chance to return, for enemies that would be at a disadvantage outside

#

Or like lootbugs, they would know that there is no loot outside and return if able

spiral ermine
#
  1. Didnโ€™t escape to the actual door itself and no fire exit support which you have done perfectly
#
  1. My concern on how the power is calculated
#

If a bracken is spawned inside, it takes 3 power level from the inside enemy power level

#

If that bracken leaves, does it still count towards that inside power level?

#

Or does it count towards that outside power level

gleaming robin
gleaming robin
spiral ermine
#

So hypothetically

#

Letโ€™s say we are on assurance

#

And I have every single enemy spawned until the power level caps inside

#

If I then lure all the creatures outside or they all escape

#

And then go down from the platform

#

I do not believe they can path back up that ladder

#

Meaning

gleaming robin
spiral ermine
#

Inside is now free

gleaming robin
#

Sure, but outside is now hell

spiral ermine
turbid wave
spiral ermine
turbid wave
#

And don't know the way back

spiral ermine
#

Thatโ€™s if they can even access the facilities doors once outside

turbid wave
#

The pathing should be able to determine that

spiral ermine
#

I do not believe any vanilla enemy can path up ladders on experimentation other than masks

gleaming robin
#

As Piotrenewicz is suggesting, if I figure out how to reliably give it a chance to directly set its path to the entrance, I could increase the outside range

spiral ermine
#

I know this for fact

#

Because Iโ€™m making it the fuck up

#

But like Iโ€™m at least 80% sure Iโ€™m correct

#

Hoarding bugs, spiders, thumpers, snare fleas

#

Cannot use ladders

#

Jesters 100% canโ€™t use ladders

#

Slimes ๐Ÿ’€

#

Coilheads cant use the ladders

#

I havenโ€™t seen butlers interact with ladders yet

#

Iโ€™ll test that

turbid wave
#

And when it's unable to path to the closest entrance, it should have a way to consider the second closest, ignoring the first

spiral ermine
#

Your honor heโ€™s capping!!!!

turbid wave
#

That would allow the emergency exits to bypass the ladder problem

turbid wave
spiral ermine
#

Me awaiting Knight to provide the source that destroys my baseless information

#

Oh yea on the other half of my entrance discussion

#

Outside enemies would normally never be able to enter the facility because of how the vanilla maps are

gleaming robin
#

Enemies that can climb according to their NavMeshAgent:
Baboon Hawks
Butler (and bees)
Centipedes
Docile Locust Bees
Manticoils
Ghost girl
Bracken
Flower Snake
Hoarding Bugs
Jester
Masked
Nutcracker (that surprised me)
Spore Lizards
RadMech (dear god)
Circuit Bees

spiral ermine
#

Hm

#

I think the issue is

#

They have the ability

#

But the maps were never intended for it

#

If possible I want you to go to experimentation

#

Spawn a nutcracker

#

Aggro it

#

Set the chance to like 100 just for measure

#

Go outside

#

It will follow you outside

#

Go down the ladders

#

It can path down the ladder

#

But I donโ€™t think it will be able to path up the ladder

turbid wave
#

They probably climb ladders the same way they jump over gaps. Just interpolate through without proper animation

gleaming robin
#

@turbid wave

gleaming robin
turbid wave
gleaming robin
turbid wave
gleaming robin
#

ยฏ_(ใƒ„)_/ยฏ

gleaming robin
#

I'll test it later, I'm COOKIN rn

#

or maybe I'll test it with this next escape test

#

if I'm feeling nice

#

I don't feel like recording a vid tho, so you'll have to take my word for whatever the result is

#

@spiral ermine both of these silly lads came up that ladder

turbid wave
#

IMO the inside and outside range should be configurable per enemy. Cooldown is fine to be internal.

gleaming robin
#

We'll see

#

I make no promises ๐Ÿคญ

#

Okay I see the issue that I made with my set-pathing attempt, I was calling on the navmesh agent when EnemyAI already has a function for this

spiral ermine
#

Holy fucking shit

gleaming robin
#

Also may have just figured out the weirdness with some enemies on teleporting outside

spiral ermine
#

Only thing I have to figure out is the power balancing

gleaming robin
#

Which I'm close to

gleaming robin
#

hrmmmm the puffer is really fighting me on this pathing business

gilded zealot
#

Starlancer this happend mid game / Enemy Escape

#

didn't get any change in the way the plugin works

#

but apeared

gleaming robin
#

What custom enemies do you have?

#

@gilded zealot

gleaming robin
gilded zealot
#

only lethal casino and it happend after it

gleaming robin
#

@tall merlin do you have anything with an EnemyAI component in your mod?

reef salmon
#

from what i understand, it just being a random chance to teleport if an enemy is near a door, couldn't enemies get warped outside mid-chase if the player went near the door? and wouldn't enemies be unlikely to chase players outside?

#

also, is it possible to configure the range to the door?

gleaming robin
#

range config is a maybe

reef salmon
#

hmm

#

so it goes

gleaming robin
#

well do you mean range as in for pathing or teleport?

reef salmon
#

teleport but eh

gleaming robin
#

nah, no config for that

reef salmon
#

also why can radmechs go in

gleaming robin
#

anything can go in, but the default for radmechs is 0

reef salmon
#

dearie me

#

will enemies that always track players like bracken navigate outside if nobody is inside?

gleaming robin
#

As of right now no, but it's something I'll look into implementing

tired ore
#

enemy escape seems to be killing my game if i have dont touch me installed

#

straight up annihilates it

#

clients dont see custom scraps, custom enemies and cant die
and it only started happening after i installed enemy escape

#

@edgy topaz pls just get rid of this apostrophe i really like the mod but it killed my game 3 times already ๐Ÿ˜ญ

tired ore
#

thats the second mod affected by the apostrophe

#

i was just eyeing this thread all day cause enemy escape was the only mod i added into my pack and it just outright broke everything

gleaming robin
#

Sorry, I'm just trying to get some good stuff implemented

tired ore
#

oh nws lol

gleaming robin
#

OKAY I finally have enemies pathing outside properly (except for hoarding bugs, they just go their nest and chill, though they'll take scrap if you put it near them)

faint copper
#

I'm curious, why is AIFix necessary alongside EnemyEscape? does SetEnemyOutside not suffice in v50?