User:Armithaig/Spring Molting Moddability: Difference between revisions

From Caves of Qud Wiki
Jump to navigation Jump to search
m (Rm todo)
(Add modded min event section with example)
Line 51: Line 51:


== Modded MinEvents ==
== Modded MinEvents ==
TODO:ModSingletonEvent, ModPooledEvent, IModEventHandler
Adding your own custom MinEvents has always been rather laborious and had significant performance drawbacks because the game had to invoke any handlers of the event using [https://learn.microsoft.com/en-us/dotnet/fundamentals/reflection/reflection reflection].
 
Spring Molting introduces the <code>IModEventHandler<T></code> interface for performance, as well as the <code>ModSingletonEvent<T></code> and <code>ModPooledEvent<T></code> classes to make things less cumbersome. All of these are generic and take your new event class as the <code>T</code> parameter.
<syntaxhighlight lang="c#>
using XRL.World;
 
namespace MyCoolMod
{
 
    public class ExampleEvent : ModPooledEvent<ExampleEvent>
    {
 
        public static readonly int CascadeLevel = CASCADE_EQUIPMENT | CASCADE_EXCEPT_THROWN_WEAPON;
 
        public string Value;
 
        // A static method that fires your event,
        // this isn't strictly necessary but is how the game prefers to organize it
        public static string GetFor(GameObject Object)
        {
            var E = FromPool();
            Object.HandleEvent(E);
 
            return E.Value;
        }
 
        // Resets the event before it's returned to the pool
        public override void Reset()
        {
            base.Reset();
            Value = null;
        }
 
        // How far our event will cascade,
        // this example will cascade to equipped items
        public override int GetCascadeLevel()
        {
            return CascadeLevel;
        }
 
    }
 
    public class ExampleHandler : IPart, IModEventHandler<ExampleEvent>
    {
 
        public override bool WantEvent(int ID, int Cascade)
        {
            return base.WantEvent(ID, Cascade)
                  || ID == ExampleEvent.ID
                ;
        }
 
        public bool HandleEvent(ExampleEvent E)
        {
            E.Value = "Handled!";
            return true;
        }
 
    }
 
}
</syntaxhighlight>
 


== Opinions ==
== Opinions ==
Line 75: Line 137:


         /// <summary>The turns it takes for this opinion to abate.</summary>
         /// <summary>The turns it takes for this opinion to abate.</summary>
         public virtual int Duration => Calendar.TurnsPerDay * 7;
         public override int Duration => Calendar.TurnsPerDay * 7;


         public override void Initialize(GameObject Actor, GameObject Subject)
         public override void Initialize(GameObject Actor, GameObject Subject)

Revision as of 23:49, 6 June 2024

This page is about modding. See the modding overview for an abstract on modding.
This page is about modding. See the modding overview for an abstract on modding.
For best results, it's recommended to have read the following topics before this one:

There's been numerous internal changes to events, parts, abilities, text, serialization, and the feelings/allegiances of creatures which may be relevant to your current or future mods. To stay brief, this will mostly focus on the larger shifts within the 2024 Spring Molting patch and won't/can't cover much beyond what has changed.

MinEvent Registration

Prior to this patch MinEvents were restricted to only being usable within their hardcoded cascade level, which restricted the depth they would cascade to for performance reasons. For example, if you wanted to handle an object being given life with the AnimateEvent from a piece of a equipment you're wearing, you'd be out of luck as the event does not cascade to worn equipment and you would have to create some funky workaround.

MinEvents can now register to events from external event sources that are outside their cascade range. For example a global game system can listen for events directly on the player, or the member of a party can listen for events from their leader, etc.

TODO:show some examples of registering to the player, leader, and the game itself, should IEventRegistrar go here or its own section?

IEventHandler

IParts and Effects that are attached to GameObjects have for the most part had the monopoly on the handling of events so far. This patch introduces the IEventHandler interface which defines the handling of MinEvents, meaning any class can now register for and handle a MinEvent if they implement it.

The IGameSystem class which is extended to define a lot of global game behavior (such as psychic hunters, village checkpointing, and ambient sounds to name a few) has been converted to use this new interface, allowing it to seamlessly interface with the existing event system.

You can read more about interfaces and how to use them here.

using XRL;
using XRL.World;

namespace MyCoolMod
{

    public class MyClass : IEventHandler
    {

        public static void Register()
        {
            var myClass = new MyClass();
            The.Player.RegisterEvent(myClass, AfterDieEvent.ID);
        }

        public bool WantEvent(int ID, int Cascade)
        {
            return ID == AfterDieEvent.ID;
        }

        public bool HandleEvent(AfterDieEvent E)
        {
            XRL.UI.Popup.Show("You died!");
            return true;
        }

    }

}

Modded MinEvents

Adding your own custom MinEvents has always been rather laborious and had significant performance drawbacks because the game had to invoke any handlers of the event using reflection.

Spring Molting introduces the IModEventHandler<T> interface for performance, as well as the ModSingletonEvent<T> and ModPooledEvent<T> classes to make things less cumbersome. All of these are generic and take your new event class as the T parameter.

using XRL.World;

namespace MyCoolMod
{

    public class ExampleEvent : ModPooledEvent<ExampleEvent>
    {

        public static readonly int CascadeLevel = CASCADE_EQUIPMENT | CASCADE_EXCEPT_THROWN_WEAPON;

        public string Value;

        // A static method that fires your event,
        // this isn't strictly necessary but is how the game prefers to organize it
        public static string GetFor(GameObject Object)
        {
            var E = FromPool();
            Object.HandleEvent(E);

            return E.Value;
        }

        // Resets the event before it's returned to the pool
        public override void Reset()
        {
            base.Reset();
            Value = null;
        }

        // How far our event will cascade,
        // this example will cascade to equipped items
        public override int GetCascadeLevel()
        {
            return CascadeLevel;
        }

    }

    public class ExampleHandler : IPart, IModEventHandler<ExampleEvent>
    {

        public override bool WantEvent(int ID, int Cascade)
        {
            return base.WantEvent(ID, Cascade)
                   || ID == ExampleEvent.ID
                ;
        }

        public bool HandleEvent(ExampleEvent E)
        {
            E.Value = "Handled!";
            return true;
        }

    }

}


Opinions

The feelings of creatures, towards both each other and the player, have so far been relatively simple. Each creature has a Feeling value ranging from -100 to +100 where any number below 0 normally meant the creature would be hostile. This number could be influenced in any number of ways such as reputation, attacking them, or beguiling them. But there was little opportunity to suss out why or when a creature's feeling towards another had changed (which was a nightmare for debugging any kind of erroneous hostility). Not to mention these feeling values would often be cleared upon leaving the zone for a while, a common tactic was to just run about two zones away if you pissed off the town allowing their feelings to reset like nothing had happened.

IOpinion is the new class introduced to alleviate these issues. Each kind of feeling altering event (attacking, stealing, etc), now has its own type of opinion which is entered into a history of opinions about each creature. This allows for much more complex modeling of relationships between creatures.

Below is an example of a positive opinion added to a creature when it is petted.

using XRL.World;
using XRL.World.AI;

namespace MyCoolMod
{

    public class OpinionPetted : IOpinionSubject
    {

        public int Level;

        /// <summary>The base feeling change from this opinion.</summary>
        public override int BaseValue => 50;

        /// <summary>The turns it takes for this opinion to abate.</summary>
        public override int Duration => Calendar.TurnsPerDay * 7;

        public override void Initialize(GameObject Actor, GameObject Subject)
        {
            Level = Subject.Level;
        }

        public override string GetText(GameObject Actor)
        {
            return $"Petted me at level {Level}.";
        }

    }

}

namespace XRL.World.Parts
{

    public class LikeWhenPetted : IPart
    {

        public override bool WantEvent(int ID, int Cascade)
        {
            return base.WantEvent(ID, Cascade)
                   || ID == AfterPetEvent.ID
                ;
        }

        public override bool HandleEvent(AfterPetEvent E)
        {
            if (E.Object == ParentObject)
            {
                E.Object.AddOpinion<MyCoolMod.OpinionPetted>(E.Actor);
            }
            return base.HandleEvent(E);
        }

    }

}


Allegiances

TODO:the current history of allegiance, the new player faction, IAllyReason

Text Variable Replacers

Text frequently needs to refer to, and be formatted with, various state from the game. It can be someone's pronouns, the name of an object, or the time of the day. To do this the game uses text delimited with = to define a variable which will be parsed by the game and replaced with the textual representation of the state we're interested in.

Take the description of a dog for example, =pronouns.Subjective= =verb:are:afterpronoun= a snarling mess of matted hair.. Here two variable replacers are defined for the subjective pronoun and a verb which for a female dog would become She is a snarling mess of matted hair..

With this patch it is now possible to define your own custom variable replacers to use within your mod. To do this you simply decorate a class and one or more methods with an attribute.

using XRL.World.Text.Delegates;
using XRL.World.Text.Attributes;

namespace MyCoolMod
{

    [HasVariableReplacer]
    public static class MyVariableReplacers
    {

        [VariableReplacer]
        public static string MyReplacer(DelegateContext Context)
        {
            // Context.Parameters is a list of every colon separated string in order, e.g. =replacer:one:two:three=.
            return "hiho it's " + Context.Parameters[0];
        }
        
        [VariableObjectReplacer]
        public static string MyObjectReplacer(DelegateContext Context)
        {
            // The Context.Capitalize flag is true if the variable name was capitalized.
            if (Context.Capitalize)
            {
                // Context.Target has a reference to the object from the prefix in the variable, e.g. =subject.replacer= or =object.replacer=.
                return "Big " + Context.Target.DisplayName;
            }
            else
            {
                return "small " + Context.Target.DisplayName;
            }
        }

        // A name can be manually defined in the attribute if you don't want to use the method name
        [VariablePostProcessor("customName")]
        public static void MyPostProcessor(DelegateContext Context)
        {
            // The Context.Value is a StringBuilder with the result of the variable replacement, ready for post processing.
            Context.Value.Replace("snapjaw", "snapfriend");
        }

    }

}

To this we could then feed the text Oh =subject.myObjectReplacer|customName=, =myReplacer:Ut yara Ux=. =object.MyObjectReplacer= is your friend? which would be parsed into Oh small snapfriend scavenger, hiho it's Ut yara Ux. Big Irudad is your friend?.

ReplaceBuilder

TODO:mention the complexity of providing all relevant parameters to variable parsing, demonstrate at least AddObject, AddReplacer, Execute and ToString

Serialization

The save system has been upgraded to be able to gracefully remove missing mod data from an ongoing save without corrupting it, this works out of the box for anything derived from IComponent like parts, mutations, skills, and effects. Or anything that implements the new IComposite interface.

IComposite will by default use reflection to serialize the public instance fields of the implementing type, but you can optionally disable this behavior and use the defined Write and Read methods to serialize it manually.

/// <summary>
/// Contracts a type as composited entirely from public fields supported by <see cref="FastSerialization"/>
/// and/or handling its own serialization.
/// </summary>
public interface IComposite
{
    /// <summary>
    /// If true, public instance field values will be serialized via reflection.
    /// </summary>
    public bool WantFieldReflection => true;

    public void Write(SerializationWriter Writer)
    {
        
    }

    public void Read(SerializationReader Reader)
    {
        
    }
}


IPart Priority

TODO:event handling order will be covered in registration, refer to that and present this alternative which has serialization implications if one part depends on another's existence

IObjectBuilder

TODO:mass object processing without parts that isn't easily handled directly in xml

IsWorldMapUsable

TODO:ability things