Just popping in to say that it's very interesting that the thinking around naming has led us, via procs, to the discussion of ID. Let's remember that we need to design ID from the ground up, rather than as a by-product of discussing procs ;-)
I favour the approach outlined by Derakon: every property tracked separately as either known or not known.
Pyrel dev log, part 5
Collapse
X
-
When someone adds a proc, they:
1) give it a name so that it can be referenced externally
2) have to add some point in the code that raises the event (trigger condition) that the proc will make use of
3) add something in the game that is aware of that trigger condition, and will thus be the initiator of the call to the proc
The number of points where the event is raised is limited. An item, for example, will raise the event for each of its onXXXX functions. It would not be difficult to ensure that every event that an item can raise is given the same base arguments -- it already does so for all but getting the name.
Procs can be attached to:
1) player
2) creatures
3) items
4) terrain
5) stat mod
For the procs to be run, something has to be aware of them and initiate them. For a given object type, it only makes sense for its procs to be initiated from an object of that type. One might raise an event with another object type (eg: changing weight might raise an "encumbrance change" event on the player), but it's that other object that will handle calling its own procs.
But then, how would one raise that event on the other object? If I want to raise an "encumbrance change" event on a player, how do I go about that? Do I call a method directly? (eg: player.onEncumbranceChanged()) Or indirectly? (eg: player.raiseEvent("encumbrance change") If I do it directly, I'll need to have all the parameters that go with the event on hand. If I do it indirectly, I can't pass in the parameters that might be needed.
For example, suppose items had a sturdiness rating, and instead of just an "it broke" event, there's a certain amount of damage done, and if it exceeds the sturdiness then it breaks. So I'd need to call an onDamage(type, amount) function. That would then need to call the item's procs (first for onDamage, then if broken call onBreak), passing in the type/amount data (and returning a value?), but will it have user and gameMap values?
So an event handler (onDamage(type, amount) function) needs to be aware of what data needs to go into, and what data needs to be returned from, any procs that are run due to a given trigger condition.
Conceivably, each of these event handlers can have unique requirements on both data to pass in, and data expected out, and any procs that handle that trigger condition have to conform to those expectations.
The problem (and it exists even in the current design) is when you want to write procs that react to an event, but don't necessarily conform to the original needs of the event handler.
Suppose, for example, we added an event handler that made a cracking noise when an item was damaged and an exploding noise when it broke. This is actually a variant on the preparing/doing event split. Another variant would be a proc on the player to make clangy/clashy noises whenever they attack or are attacked by a mob. Anyway..
The onDamage proc wouldn't want to modify the damage (as the onDamage() event handler would normally expect from any onDamage procs); it just wants knowledge that damage was done, and that the item didn't break yet (it needs to have a negative priority so that it's run after all procs that might modify that damage are finished). It probably doesn't even return a value, which is problematic if you're trying to use all the return values to determine the modified damage. sum([1, None, 4]) generates an error when it encounters a NoneType value, so its a potential bug even when packing up the return values in a list.
Plus, as we get back to the earlier example, the argument values that are being passed through are different from the values used in other event calls, so it's difficult to write a proc that can generically accept onDamage as one of its triggers if it also handles other triggers.
There's also always the issue of getting data back and forth.
If the procs return a value, that value can't (generally) be passed on to another proc. So return values are only good if we only expect a single proc to ever exist, and if that's the case we might as well just use a flag and an internal function call.
In the above example about an onDamage proc, each proc on the item should have the opportunity to modify the damage done, and it should be cumulative with previous results. Even though we can get a list of return values, that's more awkward for figuring out a combined effect. It's better if the input to one proc is the output from another proc.
However we can't use output > input mechanics freely, because the output isn't going to match the general input needed (all the other parameters that the function takes). That leaves us with only one easy option: a mutable collection of params. Note that we can't just use the function parameters themselves (as simple as that would make things) since they're (mostly) immutable, at least with respect to fixed pieces of data.
How much do we want to consider mutable or immutable? Is there always a general 'state' of things that one might expect? And what collection of things might you want to be able to change?
For example, any time we handle an event on an item, there's a few bits of state that are always expected: the item object, the user, and the game map info. If an orc picks up an object, is the orc the user? If you're looking at an object across the room, what is the game map telling you? If you fire an arrow, does it include the target creature/object/location? State is a general list of things that you may want to know about, but don't necessarily need to modify.
The params likewise vary per event. onDamage may include how much damage was done. onBreak may include how many objects broke. onEquip needs to know what slot the item is being equipped on. Some of these are fixed values and shouldn't be changed (eg: the equip slot), while others are things we may want to be able to modify. The ones we want to modify need to be enclosed in a mutatable object (ie: list or dict).
Can we encapsulate state? EG: onPickup(self, user, gameMap) -- the item object itself, the user that picked it up, and the game map state at the time. Because user comes in as a parameter, we can't refactor a simple getState() call that would be able to determine that. Querying for the game map info also seems like an awkward handling.
Event handlers in something like C# are very constrained in their signature:
Of course part of that is that they *must* have a consistent signature to work in that language, where Python isn't so restricted. However there are still certain benefits from that approach.Code:public delegate void EventHandler( Object sender, EventArgs e )
The basic event in C# always starts with the sender (ie: the object that called the event). We see an analogous aspect in the trigger signature:
Where the trigger starts by sending 'self' (in this case, the item).Code:proc.trigger(self, user, gameMap, *args, **kwargs)
It's also reasonable to expect a value for user and gameMap under any conditions that you're raising an event. Those are the three objects that define the overall state that controls the conditions under which the proc was triggered. Those are aspects that I think could be considered pervasive under any proc trigger.
Checking through the existing procs, the basic opening vars are either [terrain, actor, gameMap] or [item, user, gameMap]. actor/user/tunneler/etc are just renames for the same basic entity type - creature and/or player (which is derived from creature). item/terrain/etc are all the 'sender' object, the things that can actually have procs attached to them, and obviously tightly coupled with the proc type in question. And gameMap is the game map.
From that we can extrapolate that any proc must be able handle those three elements in its trigger signature (either explicitly, or just *args), and therefore those parameters must be considered fundamental for all procs.
That allows a certain degree of automatic flexibility -- any proc that is only interested in the event itself and the sender can always count on the sender to be present as the first field, and doesn't have to concern itself with any additional parameters passed in. That is, any proc that is only interested in the fact that the trigger condition was raised, and isn't interested in the particulars of the event. This solves the problem of mixing trigger conditions for the weight proc.
Any proc that is interested in addressing specific aspects of an event (eg: modifying the damage done to an object) must be able to handle the parameter lists required of said event. For a proc that handles multiple trigger conditions, this can be handled via the triggerMaps, as each function that's pointed to can be customized to the needs of the given trigger. This makes it possible to mix trigger conditions when you *do* want to modify the results.
For anything that expects to modify values, they must be able to operate on a mutable object that's provided as a parameter. Designing around that need is something I'll leave for later.
So it looks like the solution to the issue revolves primarily around fix #1: define fixed starting parameters.Leave a comment:
-
Really? I'd never noticed that happen. It always stays an ordinary item until I identify it.You learn the name (if unique) when you pick it up
However the finer granularity is still possible. Have an unknownAffix proc, and one instance per affix. Each time a given affix is identified, remove that proc. If an item is identified, obviously remove all of them.
When constructing the name, for each affix you attempt to add, do a check to see if an unknownAffix proc blocks knowledge of it. Can also have an unknownProperties proc for the basic info.
unknownPropertiesProc (blocks basic properties such as to-hit) [note: this isn't displayed in the present name construction yet anyway]
unknownAffixProc (blocks individual affixes or themes)
unknownItemProc (blocks variantName)
unknownNameProc (blocks artifact names)
In this case, returning a value from the function is probably the best pattern. Simply return True if it exists and it can be considered blocked.
This also implies procs should have an inherent ability to remove themselves to simplify handling:
Hit with fire
Scan through items looking for Resist Fire
For each one found, trigger an UpdateKnowledge(fire) event on that item
Proc receives trigger
If trigger params match what the proc is blocking, call parent.Procs.removeProc(self)
Obviously another instance where a single proc handling multiple triggers is useful.Leave a comment:
-
Keep in mind that identification isn't binary -- you can know or not know basically every trait of the item. You learn the name (if unique) when you pick it up, you learn the to-hit/dam pluses when you hit something with it, you learn it gives rFire when you get breathed on, etc. Each of these attributes can show up individually and will modify the name to suit.Leave a comment:
-
Another thought on name procs, evolved from the grammar changes and my pondering of the proc discussion.
Looking at the nameProcs that I made, there's almost nothing that isn't data that can be drawn entirely from the item itself. Essentially, there's no -real- reason that all that code can't be in the item object. It was made a proc solely because that's the way it was 'supposed to be'. This has bothered me a bit.
On the other hand, the extra hooks I left in for modification -- alterNameCallbacks -- are themselves exactly what I think name procs are really intended for.
My thoughts on a complete rewrite:
getName() is a normal function in the item class (or secondary class combined with grammar). It contains all the code currently in nameProc, as far as constructing the name.
One small tweak: the article is calculated before the call to grammar.getFullItemName().
Then:
Then the name procs can be stuff like:Code:item.getName(): ** current code** ** call name procs here ** grammar.getFullName(parts) return final name
That means that the called procs have to have access to all the parts of information constructed by the naming function (eg: flavors can't be pre-inserted into the prefix/suffix lists). That alters the structure of what gets passed around a bit, but is fairly minor.Code:uniqueNameProc: change article from "a"/"an" to "the" -- if identification is defined by procs, how do we determine that cleanly? -- priority -1 so that other procs happen first; how do we know if other -- 'unidentified' procs happened? unknownItemProc -- initially attached to factory, and copied when item is created. -- as they're identified, remove from item and factory. -- item can keep a reference to the factory to know whether item type -- was identified, and remove itself if so. remove any information aside from flavor and modifier/base noun unidentifiedProc: remove any affix information (leave variantName) unidentifiedNameProc: -- for items with different identified and unidentified names -- eg: Sword of Chaos vs. Doomcaller change base noun value
And what parameters need to get passed through the entire run?Leave a comment:
-
Now that I got the nice stuff out of the way, what about the not-so-nice stuff?
First, what values get passed to the proc?
From item:
proc.trigger(self, ...Code:## Use us -- invoke any appropriate onUse procs. def onUse(self, user, gameMap): self.triggerProcs(user, gameMap, 'item use') ## Trigger procs that have the specified trigger. def triggerProcs(self, user, gameMap, trigger, *args, **kwargs): for proc in self.procs: if proc.triggerCondition == trigger: proc.trigger(self, user, gameMap, *args, **kwargs)
That's going to pass in the current item to the proc. However the triggerProcs() call is going to loop through all procs. If we use a potion, does the "adjust weight" proc get called before or after the use effect?
Speaking of which, I left out an event from my earlier post -- onUse -- though it will only matter for consumables.
Anyway, we have an onUse event occur. One of the procs will apply the effect of the item (eg: restore hit points), while the other changes the quantity. However we don't know which is going to happen first.
Well, as it happens, the earlier revisions also allowed for this: give a priority value to the proc.
Assuming a default priority value of 0, this means the "use item" proc will always happen before the "adjust weight" proc.Code:"procs": [{"name": "adjust weight", "triggerCondition": ["onPickup", "onDrop", "onUse", "onThrow", "onFire", "onBreak", "onDestroy", "onSteal", "onOverflow"]}, {"name": "use item", "priority": 1, "triggerCondition": "onUse"}]
And, assuming the item class was revised to use the procs list instead of an explicit list, the call would instead be:
Code:## Trigger procs that have the specified trigger. def triggerProcs(self, user, gameMap, trigger, *args, **kwargs): self.procs.runAll(trigger, self, user, gameMap, *args, **kwargs)
However we've finally reached the sticking point that I didn't manage to work out last time -- parameter values.
'onFire', for example, will likely also pass in a target parameter, and maybe the launcher weapon that was used, whereas 'onOverflow' might have the item that was unequipped as one of the parameters. With this mixture of possible values coming in, how do you ensure that "adjust weight" always gets exactly the argument values that it needs, and always mapped to the correct parameter names?
The *args and **kwargs largely makes it transparent as long as the extra parameters are at the far end of the list, and we're only interested in the first parameters. However it's not a guaranteed solution, especially as the probable mix of multiple trigger conditions grows.
I see two possible solutions:
1) Define fixed starting parameters (eg: item, user, target, gameMap) that all procs need to recognize. That ensures that the manner in which it gets called will remain consistent, so that varying parameters don't get mixed in with things that we'll probably always be passing through. Ensures that certain parameters can always be counted on regardless of the mix of parameters. Works as long as procs that handle a mix of trigger conditions are only interested in those fixed parameters.
Downside: Requires procs have parameter slots for things they may not be interested in.
2) Require all args to be passed as keywords. EG: (item=self, user=user, target=None, gameMap=gameMap). That way we don't have to predefine anything, and the proc class only has to worry about the keyword parameters it itself is interested in. Means a proc can handle any mix of trigger conditions, as long as they all provide common paramaters that match what the proc is looking for.
Downside: Requires standardization on various parameter names. If you're going to build a new proc, you have to know what all the other procs have named a particular parameter, and that it agrees with whatever is used when calling the procs. Limits the ability to pass dynamic data.
Needs more thought, again.Leave a comment:
-
Suggestion for stress-testing procs:
Build everything possible using procs (including things such as weight). Put as much as possible of the code into procs to see what is and is not feasible, and how easy it is to achieve.
After done with procs, major systems that can logically be done much more succinctly in core code can be moved back to internal.
~~
Going through the process of how it would work with weight:
Given the last revision advancement, it looks like even things like weight may actually be nicely handled with procs.
When each trigger condition can only initiate a single proc, and each proc needs its own class, any event that has multiple trigger conditions (such as adding and removing weight) becomes a tedious affair.
However with procs being able to handle multiple triggers, and mapping those triggers to specific functions, something like handling weight becomes quite seamless.
Add this to the base item template:
And with this in the 'adjust weight' proc:Code:"procs": [{"name": "adjust weight", "triggerCondition": ["onPickup"]}, {"name": "adjust weight", "triggerCondition": ["onDrop", "onThrow", "onFire", "onBreak", "onDestroy", "onSteal", "onOverflow"]}]
And the only thing you need to worry about is making sure the item in question is in the player's inventory, if you're trying to remove weight. All the coding is centralized in one spot.Code:self.triggerMaps = {"onPickup": self.addWeight, "*": self.removeWeight}
It could even be streamlined further.
Since the mapping is stored in the proc, we don't need to separate out the different types in the object definition.Code:"procs": [{"name": "adjust weight", "triggerCondition": ["onPickup", "onDrop", "onThrow", "onFire", "onBreak", "onDestroy", "onSteal", "onOverflow"]}]
Alternatively, rather than add or remove weight, just add all the weight up. Then you don't need two separate functions.
Of course one could take that a step further (more in line with how stats in general are done) and say that you can just add up all the weight every time you need to calculate it, and not need procs for adding/removing weight. In that case it wouldn't be a call to adjust weight, it would be an adjustment to encumbrance when weight changes.
Though maybe you should break that into logical unique components.Code:"procs": [{"name": "adjust encumbrance", "triggerCondition": ["onPickup", "onDrop", "onThrow", "onFire", "onBreak", "onDestroy", "onSteal", "onOverflow"]}]
And the adjust weight proc doesn't have to actually calculate the weight, just fire the event that weight changed, which causes the player to re-calculate encumbrance, which requires querying the player's weight stat. [1]Code:item -- "procs": [{"name": "adjust weight", "triggerCondition": ["onPickup", "onDrop", "onThrow", "onFire", "onBreak", "onDestroy", "onSteal", "onOverflow"}] player -- "procs": [{"name": "adjust encumbrance", "triggerCondition": "onWeightChanged"}]
This also makes sense in case you might have effects that change effective encumbrance -- eg: Spell of Featherweight: effective inventory weight is reduced 25%; that's not something easily accessible from the "adjust weight" proc, and it's not like the weight actually changed, just its effect on encumbrance.
[1] Hmm. The player's carried weight doesn't fit into standard stat calculations neatly. Those are all based on there being stat mods that can be added up, but I wouldn't expect items to do that for their weight. If it did, though, it would tie all the way back to the original item procs -- "adjust weight" would have to change the statMod associated with that item. I suppose it would work well enough, that way. 'Adjust weight' just changes a specific stat mod (would have to be named uniquely, maybe match the item's ID value, like "10865-weight"), then fires the onWeightChanged event.Leave a comment:
-
Created a separate branch to fix up some of the things I noted in the earlier overview. Holding, pending review of main grammar branch.
They're minor changes, and not worth updating the main grammar branch for, so will put into a separate pull request later.Remove type-based function call for flavors from the item factory. Only nameInfo flavorTypes are used now.
Remove other references to .type from itemFactory.
Added a cap as a head armor type template. Not used by any current objects; just annoyed me that it wasn't there after seeing one too many Novice Rangers with little feathered caps.
Fix all the mushroom entries, which weren't noted as mushrooms. Along with that, fixed some minor flaws in the food template structure.
Does flavors.py really belong under gui? It's part of the naming code, so would expect it with grammar under util. But I guess it affects the colors too, so... Not moving it, just curious.
Updated comments, rearranged code, and renamed some functions in a few places to help clarify usage.Leave a comment:
-
I don't think players of FA think like that; I think they're quite comfortable with the idea of ego jewelry. I'm also not sure that catering to an ossified mindset is what Pyrel is about?Could you list some of those advantages? I can see much more of a case for rings/amulets than for the other flavored item types -- affixes make much more sense for equipment than for consumables IMO, especially with the attendant stacking issues.
I think the thing that bugs me most about this is that the player treats each ring and amulet flavor as its own separate type of item. Players don't think "Ring that has the Speed affix", they think "Rings of Speed". There will be inevitable confusion when they go to the backend and discover that it doesn't work that way. Perhaps I'm making a big deal over nothing -- it would help to see your couple-of-paragraphs description of how new items would be created under this system.
But leaving that aside, I think I can persuade people that the system will be better rather than worse. One of the benefits is no longer having this faintly ludicrous situation:
"I want to fiddle with speed a bit. Oh here we are, if I want to adjust speed on rings of teleportation I go to object.txt. But wait a minute, if I want to adjust speed on boots I go to ego_item.txt (or affix.txt). And if I want to adjust speed on rings I need to edit obj-make.c ??"Leave a comment:
-
-
Minor aside:
Magnate, if you're going to continue to break this up into multiple threads, please be sure that the first post has links to the previous threads, and that the last post has a link to the next thread.Leave a comment:
-
Went to write this procs wrapper class, and found that it seemed to work better a bit different than I originally thought. The class as a whole is pretty simple.
This moves the code to run a proc's triggers into the procs class, exposing just two functions: runAll() and runTop(). runTop() encapsulates the logic of running the most recently added, highest priority proc.
runAll() returns a list of all the results, while runTop() just returns the result of the function that was run.
Code:class Procs(list): # Add a proc to the procs list. # Inserted according to priority value, and with most # recent entry last. def addProc(self, proc): nextHigherPriorityIndex = next((p for p, q in enumerate(self) if q > proc.priority), None) if nextHigherPriorityIndex: self.insert(proc, nextHigherPriorityIndex) else: self.append(proc) # Get the first proc that matches a provided predicate def getProc(self, predicate): proc = next((q for p, q in enumerate(self) if predicate(q)), None) return proc # Get all procs that match a provided predicate def getProcs(self, predicate): return [p for p in self if predicate(p)] # Remove the specified proc. def removeProc(self, proc): self.remove(proc) # Run all procs that match the provided trigger, using the # specified arguments list. # Returns a list of the results of all proc calls. def runAll(self, trigger, *args): results = [] for proc in self: procFunc = proc.triggerFunction(trigger) if procFunc: results.append(procFunc(args)) return results # Run the most recent, highest priority proc that matches # the provided trigger. # Returns the result of the proc. def runTop(self, trigger, *args): for proc in self[::-1]: procFunc = proc.triggerFunction(trigger) if procFunc: return procFunc(args)
On your ideas:
onActive would be a simple way to switch other procs off and only have a single one run, however I'm not sure how it would determine which other proc to turn back on if/when the active one is removed.
isMetaProc allows for some significant power, but this time I'm the one saying, do you really need it?
Though I suppose my Procs class is like a tiny version of that. It handles maintaining an order for procs to run in (so higher priority procs will always run after lower priority ones, and recent ones after older ones), and allows you to select a mode for running procs (one or all). I can't think of anything offhand that would require moving to the next stage of allowing complete free-form rearranging of proc run order, or cherry-picking which ones to run.
Although.. I think I added a bit of that into the stats code, with the filter option (eg: get all temporary stat mods). Though you can do something a bit similar with Procs.getProcs(predicate). Or add a simple runProc() function to go with the getProc/getProcs functions to allow custom runs.
Code:# Run the specified proc, if it's one of our procs. def runProc(self, proc, trigger, *args): if proc in self: procFunc = proc.triggerFunction(trigger) if procFunc: return procFunc(args) # Run all the procs in the provided lists that are # one of our procs. def runProcs(self, procList, trigger, *args): results = [] for proc in procList: if proc in self: procFunc = proc.triggerFunction(trigger) if procFunc: results.append(procFunc(args)) return results
Done this way, you can do meta procs without a special flag. Just pass in the procs class as one of the parameters to another proc.Leave a comment:
-
Could you list some of those advantages? I can see much more of a case for rings/amulets than for the other flavored item types -- affixes make much more sense for equipment than for consumables IMO, especially with the attendant stacking issues.No. I have wanted to move rings and amulets to affix-based generation for as long as I've thought about affixes. This is just a handy incentive to get started. It is true that this doesn't necessarily mean that consumables also need to move to the same system, but I can see advantages beyond the naming issue.
I think the thing that bugs me most about this is that the player treats each ring and amulet flavor as its own separate type of item. Players don't think "Ring that has the Speed affix", they think "Rings of Speed". There will be inevitable confusion when they go to the backend and discover that it doesn't work that way. Perhaps I'm making a big deal over nothing -- it would help to see your couple-of-paragraphs description of how new items would be created under this system.Leave a comment:
-
Rather than try to teach each Proc how to handle being overridden and come up with a sensible way to order the Procs (which makes defining each individual Proc slightly more complicated), perhaps instead we could have meta-Procs that operate on the Proc list immediately prior to triggering. We would add the following atttributes to the base Proc class:
* isMetaProc: a boolean, determines if the Proc is invoked normally or as part of the self-examination process (see below)
* isActive: a boolean, if False then the Proc is not triggered.
These would default to False and True respectively, if not specified otherwise in the data file.
When we go to trigger Procs for a given trigger condition, first we look up all the MetaProcs with that trigger. Each of them is called (in undetermined order) with the list of Procs as an additional argument to its trigger function, and is allowed to mutate the list as it sees fit. Procs can be disabled by modifying their isActive boolean. If you want to modify a Proc's behavior, then you can disable it and insert a new Proc that does something different. Et cetera. Once all of the MetaProcs have been invoked, the normal Procs in the now-modified list are invoked.
(Note that a MetaProc can e.g. create a new Proc, and also spawn a timered event to remove it from the list later without requiring the MetaProc's trigger to be called again. This should suffice to handle any bookkeeping cleanup.)
Of course this system does not allow for recursion -- MetaMetaProcs. In practice I doubt this will be needed, but if it is then we can just replace the isMetaProc boolean with a metaLayer integer, and invoke Procs in order based on that value. Still, eliminate complexity where it is not needed, hence why we start with booleans.Leave a comment:
-
The ability to override an existing proc, though, is more difficult. Replacing a proc entirely is easy, but suspending it and putting it back when the current one goes away takes a little more finesse.
The standard logic for processing procs is to iterate over the list and allow each one to execute. However you may sometimes only want a single one to execute (eg: only use the single highest resistance value; the previous example of making a creature look like an item; etc).
It could be done if you abstract out the proc management instead of leaving it as a simple list that other code can directly manipulate.
Than you could have:
It inherently introduces another layer of abstraction to do this, though it could be argued as a good thing, even without the priority-handling code.Code:funcs = item.myProcs.getProcFunctions(trigger) for f in funcs: f(~params) procs.getProcFunctions(trigger): validProcFuncs = [p.getFunction(trigger) for p in self.procs] validProcFuncs = [p for p in validProcFuncs if p] maxPriority = max(validProcFuncs, key = lambda p: p.priority) return [p for p in validProcFuncs if p.priority == maxPriority]
However even that allows for multiple returns of the highest priority procs, so we're not out of the woods yet.
Note: we actually already have a situation where this comes into play with normal vs unique items. We get around it right now because the procs aren't actually entered as procs in the object data; only the name of the proc is listed in the nameInfo struct, and then the proc is determined from that. However, logically, nameInfo shouldn't have a "proc" entry, and the proc should be explicitly entered as such in the item record.
Or suppose we had a mimic that got hit with petrification, thus leading to two procs at priority 1 (as opposed to the default 0). We're back at the situation of two procs activating when we only want to allow one. You can somewhat dance around that by varying the priority values, but I think it would be better to handle it explicitly.
We can probably assume that, in such a situation, we always want the most recently applied, highest priority effect. Or, in computing terms, we want this to be a stack. Except we also want it sorted by priority.
We can also control the addition and removal of procs more precisely if we have an abstracted class for handling that.
Then the procs will be listed in order of both least-recent to most-recent, and low-priority to high-priority. You could then check if the top of the stack (last one in the list) considers itself 'singular'. If so, only return that item. Otherwise, return the list as normal.Code:procs.addProc(proc): nextHigherPriorityIndex = next((p for p, q in enumerate(self.procs) if q > proc.priority), None) if nextHigherPriorityIndex: self.procs.insert(proc, nextHigherPriorityIndex) else self.procs.append(proc)
However that seems more like something you'd need to know the logic of at the calling code level (eg: when wanting to get the item name) rather than internally to the procs, so probably better to have the encapsulating class provide two functions: getAllProcs, or getTopProc.Leave a comment:
Leave a comment: