[tech][wip] Rimgazer event driven API extension

Started by RawCode, August 16, 2014, 04:46:54 AM

Previous topic - Next topic

RawCode

Source code:
https://github.com/RawCode/Rimgazer

This is event driven framework (API) for RimWorld.

List of supported events:

GameStarted\Saved\Loaded\Event
FirstTick
GenericTick
Unit(Pawn)Damaged

Replace:
def-Human

[attachment deleted by admin: too old]

Neurotoxin

That is a tall order, implementing an event api, especially considering the number of classes that will require modification. Awesome work though. Events will definitely allow a lot more complex mods to be made, including global-scope code. If only there was a like or rep button on these forums.

RawCode

there is way to replace\inject arbitrary payload to c# application:
http://www.codeproject.com/Articles/463508/NET-CLR-Injection-Modify-IL-Code-during-Run-time
I tested code provided with article and it actually works.

There is article from microsoft with tools provided:
http://msdn.microsoft.com/en-us/magazine/cc188743.aspx

With lots of spare time it's possible to implement ANY modification to game, only question is amount of time required.

I posted this thread early since some help will be needed to keep API up to date with main game.
Any change to game code must be reflected by API.

RawCode

Little update about listener (class that do have event handlers) processing and registration:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace RC.Rimgazer.Event
{
    class EventListenerWrapper
    {
        static public Dictionary<Type, EventListenerWrapper> resolvedEventListeners =
                  new Dictionary<Type, EventListenerWrapper>();
        static readonly Type annotation = typeof(EventListenerAttribute);

        public FieldInfo    AnchorField;
        public MethodInfo   IniterMethod;

        private EventListenerWrapper(FieldInfo Anchor, MethodInfo Initer)
        {
            this.AnchorField = Anchor;
            this.IniterMethod = Initer;
        }

        public EventListenerWrapper getOrCreateWrapperFor(Type Target)
        {
            EventListenerWrapper store = null;
            if (resolvedEventListeners.TryGetValue(Target,out store))
                return store;
            Object tmp = Target.GetCustomAttributes(annotation, false)[0];
            if (tmp == null)
            {
                EventException.pushException("Type " + Target +" does not have valid annotation.");
                return null;
            }

            CustomAttributeData annotationData = CustomAttributeData.GetCustomAttributes(Target)[0];
            if (annotationData == null)
            {
                EventException.pushException("Type " + Target + " have malformed annotation.");
                return null;
            }
            string anchorDef = (string)annotationData.ConstructorArguments[0].Value;
            string initerDef = (string)annotationData.ConstructorArguments[1].Value;

            if (anchorDef == null || anchorDef.Equals(""))
            {
                EventException.pushException("Type " + Target + " have invalid anchor field def");
                return null;
            }
            if (initerDef == null || initerDef.Equals(""))
            {
                EventException.pushException("Type " + Target + " have invalid initializer def");
                return null;
            }

            FieldInfo anchorField = Target.GetField(anchorDef,BindingFlags.Static | BindingFlags.Public);
            if (anchorField == null)
            {
                EventException.pushException("Type " + Target + " have no staticpublic field " + anchorDef);
                return null;
            }

            MethodInfo initerMethod = Target.GetMethod(initerDef, BindingFlags.Static | BindingFlags.Public);
            if (initerMethod == null)
            {
                EventException.pushException("Type " + Target + " have no staticpublic method " + initerDef);
                return null;
            }
            store = new EventListenerWrapper(anchorField, initerMethod);
            resolvedEventListeners.Add(Target, store);

            return store;
        }
    }
}


Registration can be performed at arbitrary time and wont corrupt game state if type is malformed.

RawCode

"Exception" handling implementation for API internal use (and suggestion for anyone who probably will use API).


namespace RC.Rimgazer.Event
{
    class EventException
    {
        static private readonly Dictionary<string, List<string>> registeredExceptions =
                            new Dictionary<string, List<string>>();

        static public void pushException(string Exception, string channel = "default")
        {
            List<string> store = null;
            if (!registeredExceptions.ContainsKey(channel))
            {
                store = new List<string>();
                registeredExceptions.Add(channel,store);
            }
            registeredExceptions.TryGetValue(channel, out store);
            store.Add(Exception);
        }

        static public string popException(string channel = "default")
        {
            List<string> store = null;
            if (!registeredExceptions.ContainsKey(channel))
                return "No such channel is found.";
            registeredExceptions.TryGetValue(channel, out store);
            string Text = store.Last<string>();
            store.Remove(Text);
            return Text;
        }

        static public List<string> allExceptions(string channel = "default")
        {
            List<string> store = new List<string>();
            registeredExceptions.TryGetValue(channel, out store);
            return store;
        }
        static public void terminateChannel(string channel = "default")
        {
            registeredExceptions.Remove(channel);
        }

    }
}


With usage demo:

                            EventListenerWrapper tz = EventListenerWrapper.getOrCreateWrapperFor(t);
                            if (tz != null)
                                Log.Warning(t + " is registered as listener");
                            else
                            {
                                Log.Warning(EventException.popException());
                            }


If something can go wrong, you should implement some kind of tip - what exactly gone wrong and where.

This small utility class allows to store debug information and pass it around code, developer free to define as many information channels as he want.

Jerethi50

Would you be willing to write up a small guide on using this ? I'm very interested in trying it out but I'm not entirely sure what I'm supposed to do with it.

RawCode

Sample specially crafted for this case:
https://github.com/RawCode/Rimgazer/blob/master/Testgazer/Testgazer.cs

1) Current implementation support limited amount of events, more events will be added over time.

2) Class structure subject to major re factoring.

3) When you want to implement something, like preventing skill decay, friendly fire or severe wounds (that permanently cripple colonists).
Or anything else you need to:

a) navigate to https://github.com/RawCode/Rimgazer/tree/master/Event/
b) check subfolders that store event classes (folder structure subject to change)
c) read currently absent comments about when event is triggered and host type of event
d) probably you wont found event you want, in this case you can post request or provide implementation of that event and post pull request

4) If you found event, lets say, you want to spawn lightning at random part of map every 10th tick.
This belong to: https://github.com/RawCode/Rimgazer/blob/master/Event/Game/GameTickEvent.cs

5) You create separate type (class), it should be "public %Name% : extends anything you want this does not actually matters".
Add annotation "EventListenerAttribute" to that class.


    [EventListener("instance", "entry")]
    class SampleOne
{}


6) System expected to save state and work in multiple threads, this will severely change annotation and rules later.
Currently only single thing is done - event methods hosted on instance of eventlistener.

7) You can have any constructor for your type, but must wrap that constructor by static public void() method.
Name is that method is second param of annotation


        public static void entry()
        {
            instance = new SampleOne(any params you want);
            Log.Warning("Event listener demo is initialized");
        }


8) instance field is expected to store instance of listener, this is placeholder and will be removed later.

9) event handlers added by public void method that take desired event as only param with event handler annotation.


[EventHandler(EventPriorityEnum.FINAL)]
public void testa(GameTickEvent e)


10) If you done everything right - your "testa" method will be invoked every tick in game after all other handlers and have final word on event outcome.

11) Event priority system is WIP, later it will allow full control of event outcome with list of transaction passed around.

Lets say, handler with priority ENTRY set damage to 100, second handler with priority LOW see both original event and list of all transitions, it may cancel transaction done by lower level event or add his own (it may change damage type or any other param).

FINAL is special in invoked two times.

FINAL
ENTRY
LOW
DEFAULT
HIGH
FINAL

When FINAL invoked first time, it may suppress event completely or disable suppression feature but not allowed to see\change event data or transaction list. Also transactions may be disabled (or enabled) some events like "every tick" have it disabled by default.

When ENTRY invoked first time, it can see list of transactions (empty) done by every registered handler, it may exclude handlers from play for specific event instance or change invocation order.
This level wont invoke if transactions are disabled by FINAL or event is suppressed.

When FINAL invoked second time, it may perform final changes to transaction list.

LOW DEFAULT HIGH events do not see "upcoming" transactions and not allowed to suppress event.

Jerethi50

should i include the entire folder structure at your github into my assembly or is there things i should leave out ?

RawCode

There are 3 "separate" projects inside provided solution:

Rimgazer API - implements all stuff related to API, does not alter gameflow in any way by it's own.
Defs required for mod to run not yet embedded into project, this is subject to change in upcoming updates.
Basically injections will be performed runtime without any "XML" present in folder.
Rimgazer will feature it's own def re instrumentation API that will allow to process def database and alter it.

Re instrumentation configuration will be "plain text" (actually JSON or XML not yet decided) file placed in same folder as assembly.

If some mod have custom implementation of projectiles (or Pawns or anything else defined by XML) that do support events (or just want to alter flow of things without), it may feature re instrumentation configuration.
That configuration may ship without any defs or assembly.

When native re instrumentation is implemented, system will allow to push custom implementations of native types, in this case it will ask for strict version specific signed type.

It will allow you to write scripts that will replace def "a" content if content equals "b" to "c" at runtime.
This may be used to replace bullet class of all weapons added by specific mod.
In case of native level - replace bullet type itself, this will affect any other type that may extend bullet.

If you want to write your own mod that do handle events - everything you need is rimgazer.dll as reference.
Add it just like unity or rimworld reference and it will work.

If you want to play with rimgazer implementation - you will need all files.

ConsoleApplication1 used to run code from workspace without runing RimWorld.
Testgazer is testing implementation to run code from RimWorld.

You also need native that not yet included, natives will be used to replace type definitions at native level.

Neurotoxin

I'd like to help out with event Implementation. Besides forking, where do I start?

There's no .sln included so how's the project set up? Get me that far and I can figure the rest out. I assume you need Assembly-CSharp and UnityEngine References.

RawCode

I will include all required files with setup instruction to git hub and provide task list 23.08.2014.
Currently i have no time to ever open work space.

Currently i am working on API itself ( transaction based event handling and general re factoring to ensure stable work of internals ), currently added events implemented in order to test system and are placeholders.

Planned features, anyone welcomed to help with anything listed:

1) General re factoring in order to place methods to valid types, currently methods are static and placed to random type without any logic.
This will be fixed, everything will be not static and will be placed inside proper type.
Number of types at play will be halved.
Listener type removed completely, handler apply both to methods and types now.

2) Transaction based event handling, not really needed will be used to hone coding skills, actually useful only for debugging, to see what handler did what changed to event.
Will be disabled by default.

3) Custom log with watchdog and exception tracker - mods that trigger exceptions inside handlers or consume too much time to process will be removed from handling list.
All data related to API will be placed into separate roaming log file.
This required for more efficient debugging and overall stability.
Also it will have embedded profiling.
Handlers that managed to block main thread will be instantly removed from list instead of crashing entire game.

4) Def database injection framework.
With re instrumentation config and re instrumentation handlers.
Will work over standard XML defs just like everything else.
Basic setup is sourcedef targetdef and conditionslist.
later will be updated with option to replace base types.

4) Save file IO framework - read and write arbitrary data to save files in order to support custom states of objects.
Also required for runtime defs to work after loading saved game.

As for coding ~70% of events will be around pawns, since all other objects not really interactive and there is nothing to do with them.

Moved randomly - event.
Received order from player - event.
Received order from blueprint - event.
Finished job - event.
Started job - event.
Learn skill (including decay) - event.
Received mood - event.
Lost mood - event.
Mood changed - event.
Damage received - event.
Wound received - event.
Wound healed - event.

I will suggest to pick Pawn class and add event injections placeholders to all possible places.
Due to c# rules all injections must be tested - not every method is virtual and under some conditions ever if method is virtual - base method will invoked and injection wont have any effect.

Also under some conditions methods not invoked at all and must be invoked from other method that invoked from other method (that is not virtual and must be reimplemented and invoked from other method), random color for projectiles costed me 2 hours of debugging due to this.

project file included



[attachment deleted by admin: too old]

Neurotoxin

Quote from: RawCode on August 20, 2014, 05:47:39 AM
I will include all required files with setup instruction to git hub and provide task list 23.08.2014.

I grabbed the project files and I'll take a look but I'll wait until you get what you need set up first anyway. I have a few other things I'm working on at the moment but was looking to help contribute when I've got time. As for the rest of the post, those are some pretty nicely laid out goals and I hope you/we as a community can see this through.

I do see that a lot of events will be pawn related but there's also game events, some of which you've already started work on, gameloaded etc.. as well as some other, potentially lesser used possible events, (e.g. PlantGrown, TileChanged, BuildingDamaged, IncidentTriggered etc..). This has potential to be an amazing addition.

RawCode

I already have on paper WIP"Urist McPunisher" storyteller implementation - with full support of all events related to storyteller or incident maker.
By default it will replace implementation of all 3 default storytellers with itself and apply rules that will make him act just like default ones, but with event support.

If specifically selected - custom rules will apply:
System grant to storyteller "currency" based on player actions and stats, unlike current implementation, this value not bound to time or wealth directly.

Doing *bad* things like killing baby boomrats, executing or selling people (or exceeding population limit) will grant storyteller flat bonus, like 1000 points for execution.
All storyteller stats will be *public*, each day going to be marked in history with amount of points accumulated\spared and reasons of point's gain.
Also com console will feature option to "view spoilers" with same data displayed at real time.

Having empty graves attracts *visitors*, unlike events that grant bonus single time and fixed value empty graves gives points every day and additionally multiply other bonuses earned.
So if player want some challenge - he may simply dig large amount of graves, game will provide opportunity to fill them up.
Or capture large amount of prisoners and execute them all at once (probably will cause game over, ever if player very well prepared, executing prisoners is "i want fun" button).

Based on accumulated points and stats storyteller may enter constant siege mod and throw endless small waves of attackers (zombie siege anyone), eventually driving colony mad or just killing everyone.

Or start throwing composite events like sun flare + raid + siege + AI core + animal insanity AT ONCE.
Also it may throw siege 4 times at different corners of map or perform similar actions.

If large amount of points accumulated it may throw every single event at same time, and repeat this every day, but never will spawn 100 pirates on top of your base and never will spawn 20 centipedes from AI core.
If large amount of events are thrown, like siege and raid at same time, they all will be very small, like ~5 pawns and single mortar or single centipede.

No matter how many points accumulated, no more then 30 hostile pawns will appear on field.
If limit is hit - all other events that spawn pawns will be suspended.

Traders and positive events follow same rules, if player executed lots of prisoners - all traders will leave and no positive events will happen as long as large amount of points still accumulated.

Pants are "Buildings" actually, support for building and energy grid is planned.
Tiles, roofs and filth belong to map, support also planned.

mrofa

Wow thats very nice lay down rules.
But wont it choke the game ?, i mean if you got a raid of 400 raiders, they only use one squad brain, if you got 30 raids od 1-2 raiders , each of them will use squad brain for its  own raid.
All i do is clutter all around.

Neurotoxin

Quote from: mrofa on August 20, 2014, 08:05:03 AM
Wow thats very nice lay down rules.
But wont it choke the game ?, i mean if you got a raid of 400 raiders, they only use one squad brain, if you got 30 raids od 1-2 raiders , each of them will use squad brain for its  own raid.

Unless you hook the enemy spawn event and set them to use an available existing squad brain. Sure for multiple different squad brains it would be an issue in some cases (like the 4 corners siege scenario obviously would use 4 SB's) but if it's throwing a constant wave of attackers in a raid you could change the SB instance to an already active one.