STAR TREK 2d20
  1. #1
    LordEntrails's Avatar
    Join Date
    May 2015
    Location
    -7 UTC
    Posts
    17,272
    Blog Entries
    9

    Using Substitution Principles in FG

    Quote Originally Posted by RosenMcStern View Post
    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
    Last edited by LordEntrails; March 12th, 2024 at 19:39.

    Problems? See; How to Report Issues, Bugs & Problems
    On Licensing & Distributing Community Content
    Community Contributions: Gemstones, 5E Quick Ref Decal, Adventure Module Creation, Dungeon Trinkets, Balance Disturbed, Dungeon Room Descriptions
    Note, I am not a SmiteWorks employee or representative, I'm just a user like you.

  2. #2

    Join Date
    Sep 2020
    Location
    Ballston Lake, NY
    Posts
    600
    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.

    Code:
    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.

  3. #3
    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.

  4. #4
    Quote Originally Posted by Mike Serfass View Post
    Code:
    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?

  5. #5

    Join Date
    Sep 2020
    Location
    Ballston Lake, NY
    Posts
    600
    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:
    Code:
    local oldPerformExport = nil
    In the init method is this:
    Code:
    function onInit()
        -- replace my method with the current export method
        oldPerformExport = ExportManager.performExport
        ExportManager.performExport = performExport
    end
    Then my replacement handler:
    Code:
    -- 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.
    Last edited by Mike Serfass; March 13th, 2024 at 19:00.

Thread Information

Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  
5E Product Walkthrough Playlist

Log in

Log in