How to Extends / Alias / Overwrite existing class in MZ/ ES6 [Tutorial]

● ARCHIVED · READ-ONLY
Started by nio kasgami 113 posts Page 5 of 6 View original ↗
  1. OcRam said:
    This is also my final answer and sorry all for derailing... But it's important to change ideas and get some new perspectives.

    It was good article about understanding JS and totally agreed on everything he said (especially "If you don’t understand how statements in JavaScript are terminated, then you just don’t know JavaScript very well").
    BUT my point was that semicolons are not obsolete.

    And you can't run JS in single line if there are no semicolons to end statement - Not even JS leaders can do this (or any referenced institute/organization/individual)...
    Code:
    let i = 0 let j = 1 ... // Errors
    let i = 0; let j = 1; ... // No errors
    
    let obj = function {} obj.prototype.x = function () {} let y = 1 function b() {} let a = '' // Several errors
    let obj = function () {}; obj.prototype.x = function () {}; let y = 1; function b() {} let a = ''; // No errors...
    // Notice that for example function b() {} doesn't need semicolons even in single line mode

    I have been programmed with different languages all my life (now about 30 years and JS about 12 years) and I've seen these pit falls many times during this time... Just trying to help others here.

    Yeah I understand, just wanna share my point is if dev write a good and clean code enough and understand those edge cases, semicolons is not necessary :kaojoy: Otherwise, semicolon or not is most likely about the taste and project conventions.
  2. Nice semi colon discussion.

    For me, you'll notice semi colons in one line and no semi colon in another line, even if they're practically the same line just different assignments.

    I've crashed because I forgot to semicolon before IIFE.

    I have no semi colon standards when it comes to JS.
  3. Just checked the PluginCommandBase.js, which is supposed to be an official Kadokawa(and thus MZ) plugin, and it's still aliasing this way:
    Code:
    (() => {
        'use strict';
        const _Klass_aliasedMethod = Klass.prototype.aliasedMethod;
        Klass.prototype.aliasedMethod = function() {};
    })();
    I wonder if we're supposed to alias this way as well when writing MZ plugins(even though I won't use this way unless absolutely necessary) lol
  4. DoubleX said:
    Just checked the PluginCommandBase.js, which is supposed to be an official Kadokawa(and thus MZ) plugin, and it's still aliasing this way:
    Code:
    (() => {
        'use strict';
        const _Klass_aliasedMethod = Klass.prototype.aliasedMethod;
        Klass.prototype.aliasedMethod = function() {};
    })();
    I wonder if we're supposed to alias this way as well when writing MZ plugins(even though I won't use this way unless absolutely necessary) lol

    You should alias it in a way that works lol
    Whether it's a fancier solution using class syntax or otherwise.

    So we don't get any weird things like changes to the super-class don't get inherited by subclasses because they're pointing to the old method.

    Presumably if you were to change methods in Game_Battler using class syntax, the changes won't be applied to Game_Actor or Game_Enemy?
  5. yeah @Dr.Yami is right there it's just a question of convention. I just use semicolon because it's mandatory in C# and I am used to it.

    but it's all up how you guys want to work. Personally when I use typescript I use an explicit function declaration such

    Code:
    class Dummy extends Parent {
    
      private _variable : number;
      constructor(){
       this._variable = 10;
      }
     
      public foo(value: number): void {
     
      }
    }

    I always tell the returning type even if it's not necessary but this just old habit from C#.
    I even go as using generic when declaring parameters so I get specific typestructures.
  6. Visibility generally makes it easier to refactor since you know all private methods/variables will be strictly only inside your class.

    But I find that most of the time I prefer not having the visibility because half the time I'm hacking my way into variables that are not exposed (eg: accessing the scene's spritesets or windows, etc)
  7. This is what I'm planning to do on MZ, unless I find any problems with it:

    1597353986634.png
  8. Tsukihime said:
    You should alias it in a way that works lol
    Whether it's a fancier solution using class syntax or otherwise.
    I'm actually concerning about the fact that the original Klass.prototype.aliasedMethod, which is now _Klass_aliasedMethod, has become private, and this can be problematic in rare cases where the original Klass.prototype.aliasedMethod has to be accessed(I've encountered this in MV several times already).
    Is it impossible to fix with this way of aliasing? Of course not , and there are at least 2 ways to do so:
    1. Copy the original Klass.prototype.aliasedMethod into the compatibility fix, but this might violate the terms of use for few plugins, and the compatibility fix can break whenever the original Klass.prototype.aliasedMethod changes(actually, this way only works if you know which is the original Klass.prototype.aliasedMethod, and sometimes you can't even tell).
    2. Ask users to add yet another snippet right above the plugin aliasing that Klass.prototype.aliasedMethod, and all that snippet does is to capture the original Klass.prototype.aliasedMethod, but needless to say, it's less user-friendly than things can be.
    So, at the very least, I think the official RM plugins can consider storing the original Klass.prototype.aliasedMethod into some kind of a container that can be accessed by other plugins :)
  9. I think that anything which would need to access the original aliased method is probably better solved by controlling load order to guarantee that your plugin loads before the one being modified. While I can't deny that there might be some cases where it's necessary, it seems very suspicious that you'd need to access some other plugin's aliased method reference.
  10. Solar_Flare said:
    I think that anything which would need to access the original aliased method is probably better solved by controlling load order to guarantee that your plugin loads before the one being modified. While I can't deny that there might be some cases where it's necessary, it seems very suspicious that you'd need to access some other plugin's aliased method reference.
    It really depends, and sometimes changing the load order won't solve it, because in some rare cases things can be this crazy:
    1. Plugin A placed above plugin B -> Compatibility issue x
    2. Plugin B placed above plugin A -> Compatibility issue y
    Also, if the foreign plugin(the one you've no control over) to have compatibility issues addressed are written very, very poorly(but still works without nontrivial bugs), you can end up in a hellish nightmare like that:
    1. Place plugin A above plugin B -> Solve compatibility issue x with the need to access an aliased method which is now private
    2. Place plugin B above plugin A -> Solve compatibility issue y with the need to access an aliased method which is now private
    I've never faced something this extreme in MV(my cases in MV can indeed be solved by forcing a certain ordering, even though it'd still mean I'll have to solve more compatibility issues that are fortunately all easy to solve), but I've faced just once in VXA(it's so insane that the compatibility issues only manifest when 3 advanced complex scripts are in play, and different ordering leads to different compatibility issues which are all very difficult to solve, causing the fix to be ridiculously painful).

    Also, I'd like to know the reason of sealing off the access of the aliased method.
    Maybe it's to prevent any other plugin to bypass the new version and directly access the original version?
    Maybe it's to reduce plugin development workload?
    Maybe it's to deliberately reduce compatibility towards some other plugins(there are indeed plugin developers intentionally not caring about external compatibility issues)?
    I don't know, but I still believe that the official Kadokawa plugin developers, who are supposed to be seasoned professional software engineers, are knowingly making this aliasing decision, and there must be some wisdom that I still failed to grasp yet :)
  11. I am kind of bouncing between the old fashioned way and a way similar to what Hudell is going to use :

    Code:
    (function(_) {
    
      plugin.mixin(_.prototype, {
    
        initialize () {
          plugin.code.Scene_Map.initialize.call(this);
        },
    
        newMethod () {
          // ...
        },
    
      });
    
    })(Scene_Map);

    Which basically does the exact same thing as Hudell's code, just in a dynamic fashion. I created a small plugin class to handle plugin related whatnots ...

    Code:
    mixin (proto, object) {
      this.code[proto.constructor.name] = { };
    
      for (const name of Object.keys(object)) {
        if (typeof proto[name] === 'function') {
          this.code[proto.constructor.name][name] = proto[name];
        }
      }
    
      Object.assign(proto, object);
    };

    Kind of leaning toward the new method, but I dunno ...



    For anyone interested, here is the full code for the plugin class I have been tinkering around on :

    Plugin Class
    Code:
      // +=================================================|                         Plugin |
      // | [Class] Plugin
      // +==================================================================================+
    
        class Plugin {
    
      // +----------------------------------------------------------------------------------+
      // | [Method] constructor
      // +----------------------------------------------------------------------------------+
          constructor (namestring, version) {
    
            this.name     = namestring.split('.')[1].match(/[A-Z]*[^A-Z]+/g).join(' ');
            this.base     = this.namespace(_, 'Ossra.Plugin.' + namestring);
            this.code     = this.namespace(_, 'Ossra.Plugin.' + namestring + '.Code');
            this.imported = this.namespace(_, 'Imported.Ossra.' + namestring, version);
    
            this.data     = { };
            this.config   = { };
            this.helpers  = { };
            this.defaults = { };
    
          }; // Plugin << constructor
    
      // +----------------------------------------------------------------------------------+
      // | [Method] namespace
      // +----------------------------------------------------------------------------------+
          namespace (object, namestring, value = { }) {
    
            return namestring.split('.').reduce((parent, child, index, array) => {
              if (typeof parent[child] === 'undefined') {
                parent[child] = index + 1 === array.length ? value : { };
              }
    
              return parent[child];
            }, object);
    
          }; // Plugin << namespace
    
      // +----------------------------------------------------------------------------------+
      // | [Method] parameters
      // +----------------------------------------------------------------------------------+
          parameters (gid) {
    
            return JSON.stringify($plugins.find(function(plugin) {
              return plugin.parameters['gid'] === gid;
            })['parameters']);
    
          }; // Plugin << parameters
    
      // +----------------------------------------------------------------------------------+
      // | [Method] setup
      // +----------------------------------------------------------------------------------+
          setup (gid, defaults) {
    
            this.config = this.parse(this.parameters(gid), defaults);
    
          }; // Plugin << setup
    
      // +----------------------------------------------------------------------------------+
      // | [Method] parse
      // +----------------------------------------------------------------------------------+
          parse (json, defaults) {
    
            try {
              return JSON.parse(json, (key, value) => {
                let _def = isNaN(key) ? defaults[key] : defaults[0];
                let _val = isNaN(key) && value === '' ? _def : value;
    
                if (key) {
                  if (typeof _def === 'function' && typeof _val !== 'function') {
                    return this.helpers[_def.name.substring(6)].bind(this, _val);
                  } else if (typeof _val !== typeof _def) {
                    return this.parse(_val, _def);
                  }
                }
    
                return _val;
              });
            } catch (error) {
              return defaults;
            }
    
          }; // Plugin << parse
    
      // +----------------------------------------------------------------------------------+
      // | [Method] command
      // +----------------------------------------------------------------------------------+
          command (name, func, defaults) {
    
            const parse = this.parse.bind(this);
    
            PluginManager.registerCommand(this.name, name, function (args) {
              func.call(this, parse(JSON.stringify(args), defaults));
            });
    
          }; // Plugin << command
    
         
      // +----------------------------------------------------------------------------------+
      // | [Method] mixin
      // +----------------------------------------------------------------------------------+
          mixin (proto, object) {
    
            this.code[proto.constructor.name] = { };
    
            for (const name of Object.keys(object)) {
              if (typeof proto[name] === 'function') {
                this.code[proto.constructor.name][name] = proto[name];
              }
            }
    
            Object.assign(proto, object);
    
          }; // Plugin << mixin
    
        };
  12. Ossra said:
    I am kind of bouncing between the old fashioned way and a way similar to what Hudell is going to use :

    Code:
    (function(_) {
    
      plugin.mixin(_.prototype, {
    
        initialize () {
          plugin.code.Scene_Map.initialize.call(this);
        },
    
        newMethod () {
          // ...
        },
    
      });
    
    })(Scene_Map);

    Which basically does the exact same thing as Hudell's code, just in a dynamic fashion. I created a small plugin class to handle plugin related whatnots ...

    Code:
    mixin (proto, object) {
      this.code[proto.constructor.name] = { };
    
      for (const name of Object.keys(object)) {
        if (typeof proto[name] === 'function') {
          this.code[proto.constructor.name][name] = proto[name];
        }
      }
    
      Object.assign(proto, object);
    };

    Kind of leaning toward the new method, but I dunno ...



    For anyone interested, here is the full code for the plugin class I have been tinkering around on :

    Plugin Class
    Code:
      // +=================================================|                         Plugin |
      // | [Class] Plugin
      // +==================================================================================+
    
        class Plugin {
    
      // +----------------------------------------------------------------------------------+
      // | [Method] constructor
      // +----------------------------------------------------------------------------------+
          constructor (namestring, version) {
    
            this.name     = namestring.split('.')[1].match(/[A-Z]*[^A-Z]+/g).join(' ');
            this.base     = this.namespace(_, 'Ossra.Plugin.' + namestring);
            this.code     = this.namespace(_, 'Ossra.Plugin.' + namestring + '.Code');
            this.imported = this.namespace(_, 'Imported.Ossra.' + namestring, version);
    
            this.data     = { };
            this.config   = { };
            this.helpers  = { };
            this.defaults = { };
    
          }; // Plugin << constructor
    
      // +----------------------------------------------------------------------------------+
      // | [Method] namespace
      // +----------------------------------------------------------------------------------+
          namespace (object, namestring, value = { }) {
    
            return namestring.split('.').reduce((parent, child, index, array) => {
              if (typeof parent[child] === 'undefined') {
                parent[child] = index + 1 === array.length ? value : { };
              }
    
              return parent[child];
            }, object);
    
          }; // Plugin << namespace
    
      // +----------------------------------------------------------------------------------+
      // | [Method] parameters
      // +----------------------------------------------------------------------------------+
          parameters (gid) {
    
            return JSON.stringify($plugins.find(function(plugin) {
              return plugin.parameters['gid'] === gid;
            })['parameters']);
    
          }; // Plugin << parameters
    
      // +----------------------------------------------------------------------------------+
      // | [Method] setup
      // +----------------------------------------------------------------------------------+
          setup (gid, defaults) {
    
            this.config = this.parse(this.parameters(gid), defaults);
    
          }; // Plugin << setup
    
      // +----------------------------------------------------------------------------------+
      // | [Method] parse
      // +----------------------------------------------------------------------------------+
          parse (json, defaults) {
    
            try {
              return JSON.parse(json, (key, value) => {
                let _def = isNaN(key) ? defaults[key] : defaults[0];
                let _val = isNaN(key) && value === '' ? _def : value;
    
                if (key) {
                  if (typeof _def === 'function' && typeof _val !== 'function') {
                    return this.helpers[_def.name.substring(6)].bind(this, _val);
                  } else if (typeof _val !== typeof _def) {
                    return this.parse(_val, _def);
                  }
                }
    
                return _val;
              });
            } catch (error) {
              return defaults;
            }
    
          }; // Plugin << parse
    
      // +----------------------------------------------------------------------------------+
      // | [Method] command
      // +----------------------------------------------------------------------------------+
          command (name, func, defaults) {
    
            const parse = this.parse.bind(this);
    
            PluginManager.registerCommand(this.name, name, function (args) {
              func.call(this, parse(JSON.stringify(args), defaults));
            });
    
          }; // Plugin << command
    
       
      // +----------------------------------------------------------------------------------+
      // | [Method] mixin
      // +----------------------------------------------------------------------------------+
          mixin (proto, object) {
    
            this.code[proto.constructor.name] = { };
    
            for (const name of Object.keys(object)) {
              if (typeof proto[name] === 'function') {
                this.code[proto.constructor.name][name] = proto[name];
              }
            }
    
            Object.assign(proto, object);
    
          }; // Plugin << mixin
    
        };

    If that code can be included with every copy of RPG Maker MZ, then I think everyone would start using it.
    I see there's a "community" plugin included in my copy of MV, for example.

    I just don't want to have to

    1. require users to install some sort of "HIME CORE" plugin, or
    2. copy that mixin logic just to be able to avoid typing prototype a bunch of times

    Imagine if we all started pushing our own CORE plugin just to get that mixin patch.
  13. My problem is having to use some kind of core too. So I prefer writing a couple extra lines but without any dependency.
    Ideally the maker would come out with some way of handling this by default, but that would be asking too much at this point.
  14. For MV I crammed several functions into the initial section of the plugin so that I would not need to add a 'core' plugin. I may simply do that for MZ, and squish the plugin class into the plugin. The downside being that whenever I change code in that plugin class, I probably need to go back to the other plugins and change the code there, as well. So as much as I am not fond of a core plugin, I might go that direction to simply code updates. Not sure.

    For me, if the code does not look a certain way, I am displeased with plugin and attempt to find ways to refactor. So anything that allows the code to look sleek-ish I am probably going to gravitate toward due to personal preference. Unless it is a teeny patch plugin, then heck all of that.
  15. Tsukihime said:
    If that code can be included with every copy of RPG Maker MZ, then I think everyone would start using it.
    I see there's a "community" plugin included in my copy of MV, for example.

    I just don't want to have to

    1. require users to install some sort of "HIME CORE" plugin, or
    2. copy that mixin logic just to be able to avoid typing prototype a bunch of times

    Imagine if we all started pushing our own CORE plugin just to get that mixin patch.
    Yeah, I don't really like having a core plugin either, but I also don't like duplicating basic common code (such as parameter parsing) in all my plugins...
  16. Played around with the idea a little more and came up with a patchClass function that works like this:

    1597454073488.png

    And now I like it too much so I think I'll give in and make a core class too.
    What the above code does is equivalent to this:

    1597454309772.png
  17. Solar_Flare said:
    Yeah, I don't really like having a core plugin either, but I also don't like duplicating basic common code (such as parameter parsing) in all my plugins...
    The problem with the core engine is the public impression of it. People blame the "core engine" for incompatibility. In the past, the "core" engine usually modify the base code. Which lead to more complication.

    Now, if the core engine only provides the basic function you will repeatedly use, it SHOULD be fine. However, that raises another problem. When the user asking for support in this forum and provide the link to the said plugin. They will see some missing functions. The function existed in the core engine. Ideally, one specific plugin should contain all the info needed for the support question, now we need to look somewhere else (worse is if we don't know where to look the said core engine), in the collection of function in the core engine while the said plugin only uses a small part of it.

    That on itself is not exactly a big deal. But it just extra work for some people to provide support answers.
  18. For MZ you can specify dependencies in the plugin header to provide a bit more information :

    Code:
    /*:
    * @plugindesc [1.00] Plugin description.
    * @author
    * // Specify the RPGM the plugin is aimed toward :
    * @target MZ
    * @target MV
    * @target MZ MV
    * // Specify the base, required dependency :
    * @base MyCommonBasePlugin
    * // Specify placement in the plugin list :
    * @orderAfter MyCommonBasePlugin
    * @orderBefore MyOtherPlugin
    * // Specify URL for plugin :
    * @url
    */

    So, having a base plugin will not be too awful ... hopefully. The documentation says that if the base plugin specified in the plugin is not present, a warning will be displayed.
  19. Just in time for MZ's release, I found a way to use actual classes for my `patchClass` method and made it a bit better:

    1597895071987.png
  20. @Hudell Are you doing any framework plugin, or just shoving a few functions into a plugin class within the plugin?