Jofairden / EvenMoreModifiers

A mod for Terraria that adds a system for Modifiers that can apply to items giving various bonuses
https://zond.tech/emm/
Other
20 stars 10 forks source link

Add a prioritization system for ModifierEffects' delegation system #19

Closed Jofairden closed 6 years ago

Jofairden commented 6 years ago

Currently, delegations in ModifierEffect classes cannot be prioritized against others. This can lead to problems where, for example, multiplicative effects might want to be delegated last but happen too early.

Possibly allow the following attributes: [CustomPrioritization(DelegationPrioritization.Last) [OverridePrioritizationLevel(99)]

The attributes would make the delegation be forced to be last in stack, and the level influences how much it will try to force it to do so (0-99 allowed, default 0)

Use-case: if another delegation is also prioritized last and level of 99, there is loss of control (either one must go first) (so as the note: this is not a water-tight solution! but it offers at least some flexibility to prioritization control!)

Control-flow: if OverridePrioritizationLevel's level is out of bounds: throw an exception

Use-case: I know of a specific effect from another mod that I need mine to be delegated before or after that one Possibly allow the following: [CustomPrioritization(DelegationPrioritization.InBefore, typeof(Some.Mod.TheOtherEffect)) The InBefore option would prioritize this delegation before the one specified, and place it in the stack before it. The OverridePrioritizationLevel attribute could control how much it forces to be, in this case, right before the one specified. For example consider another effect wanting to be in before the exact same effect. If none specify a custom level (or both the same), they will go in no particular order. If either one specifies a higher number, that one will be in before lastly. Vice versa for InAfter

Use-case: deeper complexity by having multiple delegation prioritization happening. Consider the following situation: (loaded in this particular order too)

pub EffectA

[CustomPrioritization(DelegationPrioritization.InBefore, typeof(EffectA))
pub EffectB

[CustomPrioritization(DelegationPrioritization.InBefore, typeof(EffectA))
[OverridePrioritizationLevel(10)]
pub EffectC

[CustomPrioritization(DelegationPrioritization.InAfter, typeof(EffectC))
pub EffectD

[CustomPrioritization(DelegationPrioritization.Last)
pub EffectE

In this scenario the complexity is quite deep. In this scenario the following prioritization should be followed: EffectC, EffectD, EffectB, EffectA, EffectE In particular note that EffectD is placed right after EffectC, but before EffectB

Jofairden commented 6 years ago

Consider the following scenario:

a goes last
b goes before a
c goes after b
d goes before c
e 
f goes first

load order is as shown above (a b c d e f)

The order for the subjects can be coordinated by a set of rules:

1) First, get any subjects that want to go first or last: a wants to go last, f wants to go first. current order: f a 2) Get any subjects that have no requirements: e, inject e f a e check requirements again of all subjects a doesn't match, because a wants to be last. move a f e a 3) Go over all remaining subjects with InBefore or InAfter requirements in the order they were loaded: b c d 4) Now workout the requirements for each following subject

take the next subject to go in order: b inject b: f e a b -> check requirement: b goes before a -> move b so b is before a: f e b a -> recheck first/last requirements: is f still first and a still last? -> yes

take the next subject to go in order: c inject c: f e b a c -> check requirement: c goes after b -> no move needed, c is after b -> recheck first/last requirements: is f still first and a still last? -> no -> move a to end: f e b c a -> is c still after b? -> yes (in case the answer would become no, that means discarding this requirement as it can't be matched.. more on that later)

take the next subject to go in order: d inject d: f e b c a d -> check requirement: d goes before c -> move d so c is after d: f e b d c a -> recheck first/last requirements: is f still first and a still last? -> yes

Done! Now the steps that this took in total: quite a lot because we inject the subject before we even look at its requirement. This is fine for subjects without requirements, but ideally for subjects that do have requirements, we check them first so we can potentially skip useless injects (since we have to move them anyway):

f e a b: before a insert before a: f e b a check first/last requirements (f and a)

f e b a c: after b insert after b: f e b c a check first/last requirements (f and a)

f e b c a d: before c insert before c: f e b d c a check first/last requirements (f and a)

Notice how we have much lesser steps. In total, we pruned n steps, where n is the number of subjects with a requirement. Imagine 50 subjects with requirements, that's already 50 pruned steps. As another improvement, we can recheck the first and last requirements at the end if we want to, pruning another 2 steps. In the above example, it works out anyway because none requires to go before f or after a, but what if a subject does? Imagine the following:

a goes last
b goes after a
c goes after b
d goes before f
e goes before d
f goes first

How this works out if you follow all their requirements: e d f a b c First/last requirements overrule InBefore and InAfter rules at all times: f e d b c a In this case, we can prune more moves because we can check beforehand whether a move makes sense or not with the following rules: 1) If a subject goes after another subject and that subject goes last, we skip the step because the subject wants to be last. 2) If a subject goes before another subject and that subject goes first, we skip the step because the subject wants to be first.

In the above scenario, it means we can skip a few steps because we can workout beforehand that a and d should be skipped: their requirement is invalid: -> f a -> skip requirement from b, so just inject b: f a b -> c can be matched, inject c after b: f a b c -> skip requirement from d, so just inject d: f a b c d -> e can be matched, so inject e before d: f a b c e d Now we recheck first/last at the end: f b c e d a You may notice the order of subjects may have changed, but all their requirements still match! Feel free to check them if you don't believe me.

Now what if we have a scenario in which multiple subjects go before or after the same subject? Let's work it out.

a goes first
b goes after a
c goes after a
d goes after c
e goes before d

You may notice both b and c want to go after a, and e wants to go before d but d wants to go after c. Let's follow their requirements in order: a b c e d Now if we workout the requirements, we will find that in this scenario all of them are actually still matched. Check them if you want.

But what if, a subject has multiple requirements? Let's try this one:

a goes first
b goes after a
c goes after a, before d
d goes after c
e goes before d

You may notice the problem immediately: c wants to be after a, but before d. but d wants to be after c. This is a lock problem because, neither of them can have their requirements met. If this happens, we discard the first of the impossible requirements, and then recheck the other requirements to see if they can now be met. In this case, that means we get rid of c goes before d, and now d goes after c can be met.

Let's assume an even more complex scenario:

a goes first
b goes after a
c goes after a, before d
d goes after c, before a
e goes before d

Again, we are put in an impossible situation: c cannot go before d when d goes after c. and also, d cannot go before a, when c goes after a. How can we workout which requirements we should put away? Just like before, let's look at c and d: c goes after a, before d d goes after c, before a Before we remove InBefore/InAfter requirements, remember to remove requirements first that are overrulled by first/last: so we remove d goes before a (because a is first) We remove the first impossible requirement: c goes before d. Now d goes after c can be matched. The next impossible requirement is c goes after a. Now d goes before a can be matched. If we work this out:

a goes first
b goes after a
c 
d goes after c
e goes before d

-> a b c e d

The following scenario has made us scrap a total of THREE requirements that couldn't be met. That's quite a lot! This is where PrioritizationLevel can come into place, where a modder can signify how important their requirement is.

Consider the following:

(all levels default to 0, if not shown)
a goes first
b goes after a
c goes after a, before d (50)
d goes after c (25), before a
e goes before d

In this case, the level of c goes before d is higher than d goes after c, so we remove d goes after c instead. In this case, our end result will remain the same though:

a goes first
b goes after a
c goes after a, before d (50)
d 
e goes before d

-> a b c e d

However! The level also scales how much we should force that requirement, so in this case the level 50 is higher than level 0 (from e), so c should go after e:

a b e c d

This isn't a perfect solution, but at least provides some control for otherwise impossible scenarios and let modders signify how important their prioritization rule is.

Alternatively, we can stick to only numbers instead (for example 0-999) prioritizing subjects either to be early or later in the chain. Example: [CustomPrioritization(DelegationPrioritization.Later, 500)] -> Quite heavily wants to be later in the chain [CustomPrioritization(DelegationPrioritization.Early, 500)] -> Quite heavily wants to be earlier in the chain Working with just numbers of course is much easier than all of the above, but definitely provides much lesser control.

Jofairden commented 6 years ago

Topological sorting might be a solution, but just number-based prioritization at first could be enough.