Jump to content

X2EventManager as an Enterprise Service Bus?


TeamDragonpunk

Recommended Posts

There are now 3 (or more) separate threads mentioning the issue of mod incompatibility and the problem of creating a "Highlander Mod".

 

http://forums.nexusm...function-calls/

http://forums.nexusm...creenlisteners/

http://forums.nexusm...order-with-nmm/

 

X2EventListener currently works as a messaging service similar to an Enterprise Service Bus. Unreal 4 has iMessageBus, but I don't believe there exists something similiar in UE3. The solution may just be that we agree to a certain implementation (be it UIListeners, etc..), wait until Firaxis seeds the code with more hooks, or creates a proper API.

 

What would it take to create a community ESB that extends X2EventManager however? Would it be logistically infeasible to report the need for an additional hook in a thread, and then maintain a single class override in an "ESB Mod" that everyone can call? I'll edit this first message into something more coherent as I explore this tonight.

//  This class provides a generic interface for registration for notification of game events 
//	  and hooks for receiving and dispatching those game events.
//
//---------------------------------------------------------------------------------------
//  Copyright (c) 2016 Firaxis Games, Inc. All rights reserved.
//---------------------------------------------------------------------------------------
class X2EventManager extends Object
	native(Core)
	dependson(XComGameState);

/////////////////////////////////////////////////////////////////////////////////////////
// Event Listeners
Edited by TeamDragonpunk
Link to comment
Share on other sites

I understand the importance of mod interoperability, but could you explain the advantage of a message bus over the event registration/invocation model via the EventListener?

 

I'm just asking because I'm honestly not familiar with ESB as a formal model and how it would apply to our Unrealscript modding.

Edited by Lucubration
Link to comment
Share on other sites

I understand the importance of mod interoperability, but could you explain the advantage of a message bus over the event registration/invocation model via the EventListener?

 

I'm just asking because I'm honestly not familiar with ESB as a formal model and how it would apply to our Unrealscript modding.

I'll draft up something more comprehensive tonight, but basically "That’s the main point of an event bus, disassociation of the publisher & subscriber, where as the Observer (Listener) pattern ties them together. "

 

http://stackoverflow.com/questions/3987391/why-people-use-message-event-buses-in-their-code

 

Also, this is a good read:

 

http://blogs.mulesoft.com/dev/mule-dev/to-esb-or-not-to-esb/

Edited by TeamDragonpunk
Link to comment
Share on other sites

  • 4 weeks later...

Sound a good idea to solve my problem : I want to overwrite a class, but a mod i use already overwrite it...

After some thought, i came with this pseudo-code :

class Stuff_suffixDoesntMatter extends Stuff {

    var bool GetXXX_returnValue;

    function bool GetXXX( int i, bool b, string s ) {
        local ArgumentBag argumentBag;
        //TODO init argumentBag with given arguments
        `XEVENTMGR.TriggerEvent('Stuff.GetXXX:Start', argumentBag, self);
        GetXXX_returnValue = super.DoStuff( `SOME_MACRO(argumentBag) );
        `XEVENTMGR.TriggerEvent('Stuff.GetXXX:End', argumentBag, self);
        return GetXXX_returnValue;
    }

The class' behavior remains the same, it's only wrapped to dispatch events, and the modification logic would happen in the handler defined in a class specific to the mod.

If both (ideally ALL) mods use the same convention, we could quite happily listen to "Start" event to modify arguments, and "End" event to modify return value (which i left public for readability, but should probably be encapsulated).

Or course, to works best, once we overwrite a class when should fire those events for all its methods (not just one). Or that won't increase mod-compatibility that much...

I see 2 solutions for that :

1) Create an utility-mod that overwrite every game classes, and tell the modders to not use class-overwriting anymore.
Pros :
- resolves compatibility problem once for all, if everybody adhere

- Can overwrite X2EventManager, or even do an event system from scratch if needs be.
Cons :
- adds a dependency to a utility-mod that nobody would like to maintain...
- Event dispatching is pretty cheap in any language... However firing 2 events for *every* function call might have a significative impact...


2) Provide auto-generation tool, and each modder will be responsible of using it when he wants to overwrite a class.

Pros :
- no dependency to other mod
- only overwrite needed classes (and only fire event for them).

Cons :
- while classes from 2 mods would typically be the same, an old outdated mod could still break compatibility at some point.

- Cannot overwrite X2EventManager class except if every mod includes it.

I'm not sure which one i prefer (the second probably), either way a generation tool will have to be made.

There's also possible enhancements i can think of :

- Allow handlers to disable vanilla code (to no call super.DoStuff() in my example)

- Allow to stop the event propagation (i'm really not sure if i would be an enhancement tho...).

- Use it to detect probable mods incompatibilities more or less accurately, and warn the player (eg : if 2 different mods try to resign the return value of the same method, ...). That would probably imply a mechanism to deal with false positives (eg : AssumeCompatible("MyMod", "BobMod").

That would require to overwrite X2EventManager tho.

I'll probably do a proof of concept soon since i'm stuck with my mod anyway, because of this very mod-compatibility issue...

But the important things is to have an interface that wont change often (if at all) that everyone could be happy with ; because in the end this will only be useful if most modders use it.

So i'd like to hear your suggestions.

Edited by OVNI
Link to comment
Share on other sites

One significant problem with this is that "native core" classes cannot be extended one by one. The only way to extend them is with the "highlander" approach. And the drawback of the highlander approach (the reason why the name is chosen) is that a user can only have one highlander mod active at a time.

Link to comment
Share on other sites

Didn't thought about the native classes, thanks.



So the solution might be an "highlander" utility-mod that modify each classes so they :



- dispatch events when methods are called, and allow modification of the wrapped function's inputs/output or whole method implementation if no other choice.


This wrapper wont be in a subclass like in my previous example, but directly in the same class, stealing the name of the original method implementation.


The original method implementation itself would be renamed, and become a delegate, used by default by the corresponding wrapping method.



- make all members public



- add an array of Objects that works like an hashtable with the name of the mod as key so mods could add datas (no hashtable in UnrealScript, but linear search should do : i don't think that enough mods would want store data in the same object instance to make it a performance issue).




This would be a bit like using a dynamic prototype based language (javascript, lua,...) where we extend objects instead of classes.




edit : hum, unrealscript doesn't allow static fields... That's a problem to mimic "prototype-like" behaviour (eg : to modify the method's implementation that will be used for ALL instances of a class). Have to think a bit more but it should be doable one way or another.


Edited by OVNI
Link to comment
Share on other sites

Only one "highlander" framework-mod that provides some prototype-based features



For other mods, class overwriting would be forbidden (have to test, but the whole point is to avoid the need of overwriting classes, so...), but they could do without it :


(temporary syntax, probably all wrong, making it up on the fly)



// change implementation of XComCivilian.PostBeginPlay for all XComCivilian
// instances *without* having to overwrite the whole XComCivilian class
// so another mod could change the imp of another func and everyone will be happy
// we could also make setImplementation() detect possible incompatibilites
// if 2 mods try to change the impl of the same method
`ENGINE.getPrototype('XComCivilian').setImplementation('PostBeginPlay', someFunction);

// change implementation of XComCivilian.PostBeginPlay just for one civilian
aCivilian.PostBeginPlayDelegate = someFunction;
// changed my mind, use prototype's impl
aCivilian.PostBeginPlayDelegate = none;

// changes return value of XComCivilian.GetName (let's assume it exist)
// this will work even if another mod changed GetName implementation !
`ENGINE.getPrototype('XComCivilian').listen('GetNameCalled', HandleGetNameCalled);
[...]
function HandleGetNameCalled( Object source ) {
// vanilla implementation was called at this point (except if another mod switched it using the prototype)
local XComCivilian civ;
civ = XComCivilian(source);
if( civ.PostBeginPlay_returnValue == "Bond" ) {
// the current GetName impl returned "Bond", good, but not enought...
civ.PostBeginPlay_returnValue $= "007";
// now the code that called GetName for this civilian in the first place will receive "Bond007"
}
}

// get/set data (specific to this mod) on a civilian
struct MyData { ... };
[...]
local MyData data;
data = MyData( aCivilian.getModData() );
data.foo = data.bar;
// alternative version : we provide modname so getModData won't have to read stacktrace to determine it
data = MyData( aCivilian.getModData('nameOfMyMod') );

// and maybe even implement equivalent of static fields
MyStaticData( `ENGINE.getPrototype('XComCivilian').getModData() ).staticFoo = "bar";

Edited by OVNI
Link to comment
Share on other sites

I've made some progress, and i'm pretty exited :smile:



I've done a working proof-of-concept that allowed me to create one mod that replaces implementation of UIScreen.Show(), and another that replace UIScreen.Hide() (UIScreen being a native class).


So it allows method-level overwriting instead of class-level.



Both mods relies on a single highlander-style mod (which i called protolander for now)


I'll probably create a public git repository for it soon, but for now, here's what the client-side code looks like :



ModUsingProlander1 mod :


 



class ModUsingProlander1_Main extends UIScreenListener;

var private UIScreenPrototype _UIScreenPrototype;

var private bool _isInitialized;

///
/// Entry point
///
event OnInit(UIScreen screen) {
// OnInit can be called multiple times, prevent that...
if( _isInitialized ) return;
_isInitialized = true;

// replace UIScreen.Show() implementation by our own
_UIScreenPrototype = class'GlobalDataBag'.static.GetInstance().GetPrototypeManager().GetUIScreenPrototype();
_UIScreenPrototype.Show = UIScreen_Show;
}

// new UIScreen.Show impl, does the same thing as vanilla impl, except it logs a message.
function UIScreen_Show( UIScreen _self ) {
`log( "PROTOLANDER : called UIScreen.Show ModUsingProlander1's implementation" );
if( _self.Movie.Stack.bCinematicMode && !_self.bShowDuringCinematic ) {
_self.bIsVisiblePreCinematic = true;
}
else {
// _UIScreenPrototype.GetBase() returns the base class prototype,
// an UIPanelPrototype instance in this case
_UIScreenPrototype.GetBase().Show(_self);
}
}

defaultProperties // '{' in separate line or defaultProperties will be ignored !
{
//TODO init ScreenClass (so OnInit would be called only once)
}

 



ModUsingProlander2 mod :


 



class ModUsingProlander2_Main extends UIScreenListener;

var private UIScreenPrototype _UIScreenPrototype;

var private bool _isInitialized;

///
/// Entry point
///
event OnInit(UIScreen screen) {
// OnInit can be called multiple times, prevent that...
if( _isInitialized ) return;
_isInitialized = true;

// replace UIScreen.Show() implementation by our own
_UIScreenPrototype = class'GlobalDataBag'.static.GetInstance().GetPrototypeManager().GetUIScreenPrototype();
_UIScreenPrototype.Hide = UIScreen_Hide;
}

// new UIScreen.Hide impl, does the same thing as vanilla impl, except it logs a message.
function UIScreen_Hide( UIScreen _self ) {
`log( "PROTOLANDER : called UIScreen.Hide ModUsingProlander2's implementation" );
if( _self.Movie != none && _self.Movie.Stack.bCinematicMode && !_self.bShowDuringCinematic) {
_self.bIsVisiblePreCinematic = false;
}
else {
// Hide tooltips on a screen that was previously hidden
if(_self.bIsVisible && `TOOLTIPMGR != none) {
`TOOLTIPMGR.HideTooltipsByPartialPath(string(_self.MCPath));
}
// _UIScreenPrototype.GetBase() returns the base class prototype,
// an UIPanelPrototype instance in this case
_UIScreenPrototype.GetBase().Hide(_self);
}
}

defaultProperties // '{' in separate line or defaultProperties will be ignored !
{
//TODO init ScreenClass (so OnInit would be called only once)
}

 



For now i've only created a UIScreenPrototype, and part of UIPanelPrototype (only for methods referenced by UIScreen using the super.SomeMethod() syntax or that aren't overriden).


Now that the proof of concept works, the hard part would be to write a tool to generate the others 1929 prototypes !



I've investigated on the possibility to encapsulate implementation changes, eg :



proto.SetImplementation('methodname', SomeDelegate)

instead of



proto.MethodName = SomeDelegate;

but since delegates are strongly-typed reference, that would require to create a generic delegate signature, that would lead to terrible syntax overload, like "ValueWrapper MethodName( Object instance, array<ValueWrapper> arguments )" and client-code would have to unwrap and type-cast everything. Plus an array for arguments isn't very convenient...


So i think i'll drop the idea of detecting mod incompatibilities like that.


I think i'll just add an optional opt-in compatibility management class instead, and modders will be responsible and encouraged to declare the changes they make.


eg : compatMgr.AnnouceMethodOverwritting( 'MyModName', 'UIScreen.Show')





EDIT : Also, I need a globally accessible Object variable that last during the whole game session.


For now I store my stuff in the game main menu but that's just a temp solution since prototypes state is reset as soon as we leave this menu...


I'm not familiar at all with UDK, are gamestates object the only way to do it ? (even if i don't need my data to be stored permanently in the savegame files ?! )


Edited by OVNI
Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...