PDA

View Full Version : Using Substitution Principles in FG



LordEntrails
March 12th, 2024, 19:19
B. Embrace change. The work that SmiteWorks is doing to reduce technical debt in the framework is positive for independent rulesets, not negative. A correct use of inheritance and Liskov's substitution principle in your ruleset code will do wonders, but it requires that you accept to do some refactoring at the same time SmiteWorks does its job. My ruleset has survived the latest big updates with little or no impact, but only after I had refactored it so that it extended the features of CoreRPG instead of overwriting them. SmiteWorks is moving in this direction, and it is a good direction. Follow their lead.
As someone who is not a developer (though I am an applications engineer and IT business analyst, among other things) but who pokes a little bit in the FG community developer role (I'm working on a ruleset).

Would you or someone be so kind to point me to more information on Liskov's substitution principle and how to extend CoreRPG so that a programming idiot like me might use it in my code? i.e. specific to FG and not really a general Google article, I will do that shortly but I'm quite confident it won't give me the practical knowledge to use it in FG.

edit: For reference, I found this article useful (other not so much) but am still not sure how it would be implemented in FG. Liskov Substitution Principle in C# | by Alexandre Malavasi | Medium (https://medium.com/@alexandre.malavasi/liskov-substitution-principle-in-c-1f4bdff2b92f)

Mike Serfass
March 12th, 2024, 20:26
You've done coding in FGU already, so I'm not sure how to answer this in a way that's helpful and not condescending or too simple to help. But I'll try, and apologies in advance.

My rules of thumb when doing any kind of development against APIs, etc:
Start with adding on to or enhancing what it does.
Alter what it does only if necessary.
Avoid overriding / replacing that functionality. Changing behavior will end in tears.

The best way to see this in FGU is to look at an official ruleset compared to CoreRPG for how they did it.
In my case, I look at Savage Worlds. It extends a lot. Some D&D behavior is baked into CoreRPG, and SWADE must sometimes work around that, so there are examples of overrides.
The more common things between rule sets like NPCs and items have more overrides and extensions than rules-specific objects like character sheets.
Character sheets are a great example of subtyping.

I wrote an extension that adds a tab to the character sheet that tracks character advances (level-ups). It also allows players to plan out future advances.
This is for the Savage Worlds rule set.
There's already code that advances a character.
What I want to do is, when a character advances, look at the list of planned advances, then add that edge (feat) or skill or attribute increase to the character sheet.
I don't want to rewrite or replace or copy the current CharacterManager.onAdvance. It already does stuff I want on the character sheet.
Instead, I add a listener to my own onAdvance function to do my own thing in conjunction with the base functionality.
My method in no way changes behavior or functionality of the original CharacterManager.onAdvance.
My method doesn't duplicate or affect any work the original CharacterManager.onAdvance does. I only enhance what it does.
I wrote it in a way that it doesn't matter what the original function does, or the order the functions fire in.



function onInit()
....
-- add a listener to my function
CharacterManager.onAdvance = onAdvance
....
end

function onAdvance(nodeChar, nGainedAdvances)
.... code doing cool stuff here ....
end


Your link to C# is a good one, but lua and C# are very different languages, so you may find it challenging to translate how to apply those principles unless you know both languages well.
There are lectures on YouTube that may help.
This is where peer programming is really, really helpful.

RosenMcStern
March 12th, 2024, 20:52
It's quite simple. The idea is that some part of CoreRPG calls a method, defined on a base template or window, via a reference to a window or control. However, since you have extended that control or window, what is actually called is your redefinition of that method, that adds functionality instead of replacing it.

For example, wherever you see a reference to "self" in CoreRPG, the framework developers are creating a hook for you to use Liskov's principle. That method call does not know, in principle, which piece of code will be called, as this will be determined by the derived/extended code. However, the new behaviour you define is seamlessly integrated in the base behaviour of the core classes. If well applied, this produces solid, extensible code that benefits from improvement to the generic components, and does not break when they change.

Practical case. In ct_entry.lua, line 30, the base code for the CoreRPG combat tracker entry calls self.linkPCFields(). My ruleset makes a near complete rehash of the combat tracker, and redefines linkPCFields() to add many more fields to the ct entry. This piece of code produced the one and only glitch in my ruleset after last week's mega-update, ignoring the value of the 3D token even if it was there. The reason? I had not called super.linkPCFields() in the extended method, which is something you should always do when using inheritance, and thus I was overwriting the method in CoreRPG instead of extending it. Adding a call to super (3 lines) did the magic and all the functionalities of the new 2.5D started working in my ruleset. And please note that I should have done it before, so the new functionalities shouldn't have broken anything, had I done things properly.

When things are done the right way in both the core and the derived rulesets, updates don't break functionalities, they enhance them.

RosenMcStern
March 13th, 2024, 15:16
function onInit()
....
-- add a listener to my function
CharacterManager.onAdvance = onAdvance
....
end

function onAdvance(nodeChar, nGainedAdvances)
.... code doing cool stuff here ....
end


Out of curiosity... aren't you replacing the event handler in the standard CharacterManager here? In this way you will have your code called by the existing CoreRPG code, but how do you ensure that the original onAdvance is preserved?

Mike Serfass
March 13th, 2024, 18:58
It doesn't replace. This adds an event handler to the stack. The base handler is still called.
I could add multiple event handlers, and all would be called.
I could add my own even handler and another extension calls its own. (I have two extensions that do that.)
I can't guarantee the call order, however.

There is a way to replace an event handler. I do that in one of my extensions because I want to ensure my code runs before the base code.
It's in my Setting Tweaks extension. When exporting Savage Worlds modules, I want to remove various nodes that are duplicated in db.xml and are then exported to client.xml.
Here's how I do that.

In my main lua script file I declare this exposed variable:


local oldPerformExport = nil


In the init method is this:


function onInit()
-- replace my method with the current export method
oldPerformExport = ExportManager.performExport
ExportManager.performExport = performExport
end


Then my replacement handler:


-- main function that performs the export with my injected functionality
local function performExport(wExport)
prepareModuleExport(wExport)
oldPerformExport(wExport)
end


My performExport event handler and prepareModuleExport function have the same signature as the base ExportManager.performExport.
This way I control the call order. I still don't replace functionality. I'm adding functionality and controlling the order.
Though, if another extension adds a handler for this same event my extension won't know that and won't control that one.