User:Armithaig/Spring Molting Moddability
![]() |
This page is about modding. See the modding overview for an abstract on modding. |
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.
public interface IEventHandler
{
/// <summary>A tokenized singleton that serializes bindings to a specific implementation of <see cref="IEventHandler"/>.</summary>
public virtual IEventBinder Binder => null;
/// <summary>Check if the current event handler is still valid for receiving events.</summary>
/// <remarks>
/// Usually this is invalid when the event handler has been removed.
/// E.g. when an <see cref="IPart"/> has been removed from its parent <see cref="GameObject"/>.
/// </remarks>
public virtual bool IsValid => true;
public virtual bool WantEvent(int ID, int Cascade)
{
return false;
}
public virtual bool HandleEvent(MinEvent E)
{
return true;
}
}
TODO:IEventBinder needs blurb at least to its purpose and necessity, an example implementation and registration
Modded MinEvents
TODO:ModSingletonEvent, ModPooledEvent, IModEventHandler
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 virtual 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)
{
return "hiho it's " + Context.Parameters[0];
}
[VariableObjectReplacer]
public static string MyObjectReplacer(DelegateContext Context)
{
if (Context.Capitalize)
{
return "Big " + Context.Target.DisplayName;
}
else
{
return "small " + Context.Target.DisplayName;
}
}
[VariablePostProcessor("customName")]
public static void MyPostProcessor(DelegateContext Context)
{
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?
.
TODO:add code comments detailing parameters, context target and attribute names more explicitly
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