-
Notifications
You must be signed in to change notification settings - Fork 6.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Pre-WIP] [3.3.5] The AI system redesign #22461
Conversation
A great proposal. What would make it really powerful is the ability to make templates that can be applied to more than one creature with level scalability. For example, if a template is made for a 'Frost Mage NPC', that template could be applied to innumerable different NPCs of different levels. Does that make sense? |
Kind of @Scytheria23, but what would this template consist of? If it's just the frostbolt spell, what's the point in having a template? (Also, different creatures use different Frostbolt spells...) |
Some behavior I saw on 3.3.5 sniffs of kologarn, there were a "cast sequence", measured in seconds passed from fight start: |
Hrm, a success/failure result for callbacks, perhaps? ED: Upon further thought, feels like an overcomplication. Callback can just reschedule itself if necessary. |
Since I was pinged here... Oh and all the info about their "actions" is in the client, I extracted it from a few builds (see #22421 for an example) and will post everything I have. |
Alright, read it. As for "Behavior flags", "combat flags" and "interaction flags", once I post the aforementioned list of actions dug out from the client, it will be very clear which flags and toggles you need to have, anything above that would only need to be added to account for whatever quirks the new system will have (ultimately none, hopefully). Also, adding to what I wrote above, if you decide to mimic Blizzard's scripting system, there are three main worries I see:
|
Do not forget master |
Keep in mind that some (most now?) bosses do not evade on party wipe, but despawn and respawn after a fixed timer.
A good check for this behavior is yors'ahj in DS, adds during the black phase are set to fixate, what do they do when their target dies? (I think they used to ignore feign death too) I also share concerns about callback hell. Tracing call stacks when bogus situations happen is also going to become a lot more difficult. Re LUA: Do you allow coroutines? They use some sort of SAI tool indeed (It's integrated into WowEdit, and they leaked it with a screenshot of the first Onyxia - at work, but the screenshot is out there somewhere), and some NPCs in that gun'drak dungeon with the optional dinosaur boss. |
Yeah, that's kind of what I meant when I said "restrict what they could do". I want to keep allowing C++ callbacks for extreme cases (also, for more efficient built-in callbacks) though. 99% of scripts should be doable in just Lua+DB.
Yes please.
Yeah, what I mean is evade and respawn would (in terms of behavior object) do exactly the same thing.
Ehhhh. Maybe. I'm not sure if there'd be any significant upsides to it. |
In my opinion this issue should be seperated into two main problems:
I did some research on this topic in the past (from which the
I did some experiments with boost fibers int the past actually I got the following example to work. It is similar to the design proposed by @Treeston with a single entry point: class MyAI : public AsyncCreatureAI
{
public:
void Reset() override
{
await OnEnterCombat();
Async([this] {
await OnDespawn();
await this->CastSpell(47474);
});
Async([this] {
while (auto damage = await OnDamageReceived())
{
// Do something
}
});
while (true)
{
RandomEvent(
[this] {
await Wait(10s, 15s);
await CastSpell(28373);
},
[this] {
await Wait(8s, 13s);
while ((await CastSpell(3746)) != SpellCastResult::Ok)
;
});
}
Async([this] {
// ...
await OnDespawn();
});
}
}; |
Here are the screenshots of blizzard's tools that @Warpten mentions. They were posted on mmo champion. You can see that in the Creature Editor, which presumably is for editing blizzard's equivalent of the |
Especially the creature spells editor is super interesting. |
We can try to deduce the following about the "creature spell lists" from that editor screenshot.
I have no idea what the Up top there is a box titled "Percent chance each tick", which lets you set a "Chance Support Action" and "Chance Ranged Attack". The value "-1" probably means "not applicable". There is an "Edit" button next to the spell name button, so maybe clicking that brings up a pop up window that lets you set additional data for that spell like some kind of flags. Or maybe it would bring up a "Spell Editor" to edit the spell itself. The version of the editor that was posted on mmo champion is from 2009, based on the URL, possibly shown at Blizzcon. Here is an earlier screenshot of the tool from vanilla. The early version is missing 3 of the fields on the right, the rest is the same. |
I would disagree with some of the points there.
|
CreatureSpellData.dbc, hint-hint. |
No, that's just a joke on players, who were always confused about the timing of that particular Onyxia attack. Blizzard teased them by showing a screenshot of Onyxia's spells, but deliberately hiding the Deep Breath values. |
Nah, it IS spell ID, but blizzard has internal names for everything. You think any of the designers would be happy to work with 200 spells named "Fireball"? Even the creature editor there shows a field with "Internal Name". |
I haven't even begun writing the theoretical part yet (it will contain my thoughts on WowEdit screenshots), but I've assembled the scripting actions for you to get started: https://docs.google.com/spreadsheets/d/1Wh1e1ZXYM-sw3wWrY9dGZ2ngK8oa3PA2XIftQJuf5R0/edit?usp=sharing If you don't understand the meaning of some of the things in there, there's a high chance I will explain them in my writeup, but feel free to ask (or comment on the document) anyway. |
If the values for Probability and Availability were reversed, i would think that too. Probability is chance to cast, and Availability is possibly a mask for difficulty. But instead you can see that Availability has a value of 100 for all the spells, which seems like percent chance to me, while Probability is 1. The ??? on Deep Breath is a joke for sure btw. Also i just noticed, from that vanilla screenshot we can also see that the "creature spell lists" contain self buffs - "Demon Armor III" is highlighted. |
I feel very stupid right now. |
This is where the screenshots are taken from if you want to watch the panel. |
What would make it really powerful is the ability to make templates that can be applied to more than one creature with level scalability. For example, if a template is made for a 'Frost Mage NPC', that template could be applied to innumerable different NPCs of different levels. Well, I'm thinking for coverage beyond Blizz-like, which I know isn't TCs objective but could be considered. Many devs have quite complex custom NCP behaviours scripted with smartAI, but these have to be individually scripted for each creature type. A more generic template could be applied to a range of NPC, with abilities scaling by level. |
These strings look horribly familiar and I don't know why. I remember finding them in some IDB and casting it aside as "probably debug shit". IMHO if youre going with Lua you wouldn't allow coroutines there, because that's more or less just moving the overhead somewhere else |
They are in every Wow.exe. They are probably located in some source code that's shared between client, server and WowEdit (e.g. our SharedDefines.h), and thus get compiled into the exe, even though they're never used anywhere by the client. |
switch (stepTemplate.StepType) | ||
{ | ||
case ACTIONSCRIPT_NULL: | ||
step = new NullStep(thread, stepTemplate); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If "steps" are anything like SAI's actions or even events, dynamic allocations every time a script is going to execute an action (or event) seems very detrimental to performance. Consider using std::variant, so no allocations will need to be done and all possible steps can live within the same already-allocated memory. Although it will probably make it very inconvenient to declare said variant, since you would need to list all possible step implementations in it. Another way would be to have a pre-allocated memory arena that will host all steps (with "placement new"s), however that would require defining strict limitations on step sizes (nothing a static_assert can't handle) to make sure they can even fit inside the arena.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good observations. Variant would be a complete pain to work with.
Looking into placement new into a pre-allocated region in the ActionThread
.
ActionThreadStep(ActionThread const& thread, ActionScriptStep const& stepTemplate) : _thread(thread), _template(stepTemplate) {} | ||
|
||
ActionThread const& _thread; | ||
ActionScriptStep const& _template; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sure it sounds logical to have these things as members here, but take a moment to think if it's really necessary. Will programmers have access to steps outside of the context of their execution? If not, these references can easily be passed to steps when needed instead of having them stored here permanently. Have a structure, say, ActionScriptExecutionContext
and pass it to every Evaluate
(Execute
seems to me like a better name?). Said structure will contain references to ActionThread
, ActionScriptStep
template (since they all seem to have explicit numbering, i'm sure you'll be able to access the list of all step templates and pick the one corresponding to the current step), and additionally things like current time or time delta, current unit, last invoker and whatnot. This way each step instance will only have to physically contain the actual data it requires to run, data, that's going to be unique to each instance of a step. Oh and as a bonus, such temporary context-like parameters tend to get optimized away neatly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Historically, not having access to (something like) the ActionThread
has always turned out to be a maintenance nightmare. A single extra pointer is not going to kill us.
ActionScriptStep
is only stored here, not in the thread. We need access to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's 2 pointers already which are going to be duplicated on every creature in the world using ActionScript. Well, I guess as long as only the "current" steps are being stored, the memory usage might be within reason, but if at any point it's going to be like SAI, where the entire script is copied into every of its instances because it might need to be changed (SMART_ACTION_CREATE_TIMED_EVENT, SMART_ACTION_INSTALL_AI_TEMPLATE etc), this could blow up.
I still think it's inefficient to store the reference to thread if the step can never be interacted with by anything other than the thread. Store just the state of the step, pass everything else needed for it to function as parameter(s). Unless you intend to do something like unit->GetCurrentStep().Cancel()
, which would require the step to have all the necessary information (thread, namely) to function in such independent context, but IMO that's not the best design.
But sure, this kind of optimization can be done when the design is finalized and proven to be working, if it doesn't clash any of the implemented functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By storing refs you also just implicitly deleted the other special member functions, maybe not too handy for later.
@@ -29,15 +29,16 @@ class NullStep : public ActionThreadStep | |||
void Abort() override {} | |||
}; | |||
|
|||
/*static*/ std::unique_ptr<ActionThreadStep> ActionThreadStep::StepTo(ActionThread const& thread, ActionScriptStep const& stepTemplate) | |||
/*static*/ void ActionThreadStep::StepTo(unsigned char* ptr, ActionThread const& thread, ActionScriptStep const& stepTemplate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is passing ptr
really needed here, if you already have access to the thread? &thread.GetCurrentStep()
and voila, no need for additional reinterpret_cast
s either. Would require thread
to not be const
, of course.
It might be needed, though, if at any point will you decide to have more than one "current step".
else | ||
return STATE_RUNNING; | ||
|
||
step.~ActionThreadStep(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it doesn't happen often to see a destruction being called explicitly
switch (stepTemplate.StepType) | ||
{ | ||
case ACTIONSCRIPT_NULL: | ||
new(ptr) NullStep(thread, stepTemplate); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why placement new for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because memory fragmentation is not a good thing to have in code that runs all the time, and we don't need malloc here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought TC had a dependency library that allowed static mempools. If not then I'll agree with you to use p-new.
Alright, so this is gonna be a two-parter comment. This here is the first part, where I talk about some more design considerations I've been making. Part two, probably somewhere below by the time you're reading this, is where I engage in some Real Talk™ and tell you why I'll be closing this for the time being. It's not for the reasons you think. So, AI design. If you've been following the PR, you will have already noticed the outlines of a SAI replacement taking shape. I called it ActionScript, name subject to change. Maybe ActionSequence would be better. I don't like calling it "Script", it sounds rigid. Maybe it's accurate, though. Either way. In my vision, this true kinda-synchronous "scripting" system would only be necessary for truly "scripted" passages in a fight. Let's take Lich King, for example - ActionScript would handle the transition phases. Triggered at a previously-set HP threshold by the core, the script would take control, stop auto-attacking, move LK to the center of the platform, start channeling big bad AoE, and start summoning stuff periodically (maybe there's even some aura that does this), wait X seconds, stop channeling, cast "break the platform stuff" spell, end of script. Here's why - one major problem I keep running into, which I really do not think should be a problem, is the consideration of what happens if multiple "things" try to directly control a given unit at the same time. What if ActionScript tells the creature to move to point A, but the creature behavior wants to chase to point B? You can see how this gets annoying really quickly. So, none of that. In my vision, only a single "thing" has control of a given unit at any one time. That could be ActionScript, or it could be the creature behavior engine, or something else, but never multiple of these. The creature starts executing an ActionScript, normal creature behavior stops doing anything until you finish. Once you finish, behavior comes back. Now you will notice that I said "has control" up there. In my vision, there's two ways you can run an ActionScript - I call them "synchronous" and "asynchronous", because the names fit. Synchronously running an ActionScript is what I described above - the script takes control of the creature, and does stuff with it, and normal behavior is paused. Asynchronously running the script lets it run concurrently to normal creature behavior, but you cannot control the unit (this is enforced by the engine). You cannot tell the unit to move to position X, or to cast spell Y, or to stop attacking. You can modify behavior flags or the creature's spell list, though, and the behavior of the creature will change based on that - so you can't directly cast spell Y, but you can add spell Y to the creature's spell list, and the creature will cast the spell whenever behavior deems it appropriate. Stuff like "cast spell Y if condition Z is met" uses this. Oh, and if that wasn't obvious: no two synchronous ActionScripts can run at the same time. Not sure on the details, but either the first one gets aborted, or the second one gets delayed until the first is done. Tending towards the latter. With that, we're done with the technical stuff. Real Talk follows in the next comment. |
Alright, part two, the Real Talk™ part. You may recall that I promised I'd do work on this over the break, and that work has only manifested to a limited degree. See, and this is where we veer right into Real Talk. We all make commitments sometimes - but there's a point where there are too many, and you are constantly just dealing with the next expectation looming over your head, where this gets really shitty for your mental health. For some more context - and some of you might be aware of this already - besides being a somewhat prolific contributor here, I'm also a second-in-command for a tiny internet spaceship alliance of a few thousand real people, I routinely obsess over card rulings for pointless card games on reddit, and also manage the full workload of being a student and TA at the same time. Apparently, when I have hobbies, I have them way too much. So yeah, for the last few years, I would wake up, and be on my phone checking Slack and Discord within the first five minutes of my day. After that, I'd go through my mail and deal with my Github inbox. After that, reading through all the threads about internet spaceships. After that, over to reddit and make sure all the card game rulings had accurate replies. Before I'd know it, it'd be time to leave for work, where you can guess how I spent my breaks. Once I got home, more of the same. Just before heading to bed, I'd be checking Slack, Discord and forums. Sounds kind of like your daily routine? As it turns out, constantly being under pressure to perform even more is not good for your mental health. Like, at all. There's this thing called "burnout". Yes, I know, we don't talk about it. It makes us seem less like the perfect, efficient, always functional, well-oiled rational machines we've all been trained to be. If you don't work hard, you're letting other people down and showing you're not worth keeping around, right? Yeah, fuck that. I spent my "break" still being torn between four different commitments, one of which is writing a new AI system. And, you know what? I still wanted to write a new AI system. Like, consciously, I wanted to. That's why there's the whole comment above with stuff I thought about. But, mentally? I can't anymore. I'm unable to focus on the work in front of me. In other words, I've got what we'd probably call "Mental Issues™". You know, the things you're not supposed to have, because they make you seem like a less valuable job applicant, and that's all we care about anymore. Maybe we should all face some truths about us not being all too perfectly functioning all the time sometimes, hm? Anyway, that's my Real Talk for today. No big lessons or great wisdom in here. Just a plea to watch your own mental state, before you end up where I am right now. I'll get better, I'm sure - but it won't be today, or in a week, or maybe even in the near future. Right now, more work is not something I am consciously deciding not to deal with, so I'm closing this. If I ever feel like working on it again, it may come back in the future - which is decidedly not a commitment. If that happens, it'll happen on my terms, because I actually want to work on it, not because I set myself up for yet another commitment that pressures me into meeting expectations. Mic is dropped. Tree out. |
Hej, sorry to hear to went through what many others went through already, hope you'll find your balance between work, family and hobbies. It's fun to have hobbies and it's very easy to get totally sucked up into those. Small tip from a guy who is trying to build a whole automated AI that is supposed to test all possible already fixed TC issues and find new ones on its own: when you have an idea that sounds very interesting to work on, take a note about it, write what you would like (or "dream of") to do about it, just like this AI redesign. Maybe it will take 10 years to finish, maybe it will take 1 year to even start writing the first line of code, it will just be a background thread running when you are bored enough to work on it and not exhausted by everything else. It will allow you to be curious about things while keeping your sanity and having time for your life :) |
Iv found out that sometimes you have to pause stuff and let it sit a for a while and when you feel up to work on something, do it. But don't do it because you promised it. Keep your head above water @Treeston . |
I really feel you. Even if we had our differences in opinion in the past, I have to say it was a pleasure debating, discussing and even arguing about literally everything. I really think I've learned from that, hope it was a two-way thing. Really wish everything goes well for you, I mean it. Cris. |
Thank you for all the things you already have provided to TC. |
Remember this is all open-source free contributions you're making here. You should never feel pressured to write anything, as you've already contributed a lot. Take some time off @Treeston . Thanks for your work. |
Hey Treeston, you have done awesome work for the community and seem all arround like a really nice guy. Had to deal with the same issue aswell in the past, and it will get better, but like you said, it takes time. Wish you all the best ❤️ |
It's Happening™. Eventually. Keep reading.
So.
Our current AI system micromanages practically everything. From making sure the creature keeps swinging in melee, to attacking a new victim, to updating eventmaps or checking health. It also has a bunch of hooks that get invoked from the outside on the most miniscule of events (spell hit, any damage taken, any movement at all).
It's also ridiculously rigid. Events are timed down to the millisecond (+- one server tick) and will always execute at exactly that time.
Retail.... doesn't do any of that. Retail AI is (mostly) asynchronous, and written in a way where delaying AI callbacks a few ticks doesn't make much of a difference. Their scripts set behavior flags for common behavior (should auto attack, should be unkillable, etc.) and those behavior flags are then handled by the core. The result is a large reduction in script complexity, and the core can also make assumptions about what scripts will or won't do. Scripts become less powerful, which is actually a good thing.
(@xvwyh actually wrote a very nice summary of some more differences over here - you should read that)
This PR is where all of that changes. We're throwing out the old AI system in its entirety.
(Realistic aside: it'll survive as
LegacyCreatureAI
or something for now, likely with reduced capabilities.)Let's talk about the replacement. Timetable for actual implementation is, most likely, christmas break, because I'm about to be consumed by the upcoming semester, so there's that. Until then, let's figure out what won't work in the proposed model, and then change the proposed model so it does work.
Here's how I see this working out.
The broad strokes (aka: How Do I Do Things)
It's a callback-based model. The script registers callbacks (functions) to happen upon certain events.
Examples of events are health thresholds and timers. Replicating the current update loop with these is possible, but not the intended usage.
Callbacks can be registered/unregistered on the fly. The idea is that any given callback sets up the next "steps".
For example, if a boss changes phases at 60% and 40%, this would look like this:
Not that different from our current scripts, right? The main difference is that all of the handling of callbacks is done core-side, and we don't need to call into the actual script on every tick anymore.
(Aside: Evading would also be core-side, and that'd let us get rid of quite a few hacks. I'm also intending to fully reset the "AI object" (I'm not sure I'd even call it that - "Behavior object"?) when reaching home after an evade.)
Behavior flags (aka: How Do I Do When I'm Not Doing)
Pretty much all of our scripts do the same things. Yes, depending on the boss, there's a few special things (that's why callbacks exist) but overall they all do the same thing. We can handle those things coreside using what I call "behavior flags" that tell the core how the creature should act.
Below is a list of behavior flags I'm currently considering for implementation. Please comment if you can think of things you can't do with these.
The combat flags (aka: How Do I Do When I'm Doing The Do-Do)
true
REACT_AGGRESSIVE
in base CAI)false
true
/false
- whether we should ever do melee swingstrue
we might use spells instead depending on priority!NO
/FIXATE
/YES
- whether we should care about the threat listNO
orFIXATE
, the core will never override the victim selectionFIXATE
and current victim goes away, snaps back toYES
true
/false
- what it says on the tin; whether we should evade if there's no targets to punch anymoreThe interaction flags (aka: How The Do-Do Is Done To Me)
true
/false
true
/false
true
is set - bosses should set this outside their final phaseSpell priority system (aka: I Do The Do-Do Without Any Do!)
Some more thoughts on callbacks (aka: How Do I Do It Do the Doing)
I'm envisioning three different kinds of callbacks that can be used more-or-less interchangably:
Callbacks don't have to be real time, and shouldn't depend on this! If a callback happens to happen a second or two late, this shouldn't break anything.
(Aura scripts are for precise stuff. Those are staying as is.)
What I Want You To Do(-Do)
Think about scripts you've worked on. What things do they do that doesn't cleanly fit the model above? Bring it up. I would prefer to find out now rather than halfway into the implementation.
What things would you like it to do it currently doesn't? Do you think some things could be more streamlined? More input on how you've found retail does things? I'm interested.
Like anything above a bunch? Tell me, so I can make sure not to cut it if possible. This is a draft after all.
PS: I am truly a bit of a masochist, I suspect.