[Ace] Kendrick Core Script

tyler.kendrick

Caffeine Addict
Veteran
Joined
Nov 21, 2014
Messages
52
Reaction score
14
First Language
English
Primarily Uses
Before I make an official release of my core script, I want to open it to criticisms.  The goal is to enable greater ease-of-use and extensibility to both the rmvxa engine, and rgss3.

Some of the features I have included into the first planned release are as follows:

RGSS Extensions

  • Dependency resolution
  • Error Handling.
  • ::observable module - observer pattern implementation and attr_observable metaclass extension
  • ::Kernal#try_method - invokes if method exists.
  • ::Boolean module - enabling stricter boolean typing
  • ::Callback class - formal callback mechanisms
  • Eval optimizations
  • Safe-Navigation: simplifies null-coalescing index expressions.
RPGMaker VXAce Engine Extensions

  • Initializing Marshal loaded data.
  • Accessibility optimizations for Game_Battler and Game_BaseItem.

If anyone would like to offer advice or criticisms, I am happy to discuss them.  Also, please feel free to request additional features (or simply note objects you don't like extending) to be made available in a future release.

The link can be found [here].
 
Last edited by a moderator:

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
Can't say much about the custom stuff like callback related classes and modules (which seems to be your major focus) until it's used, but in terms of compatibility there shouldn't be any issues since you've maintained the same interfaces for all of the stuff in the default scripts.
 

tyler.kendrick

Caffeine Addict
Veteran
Joined
Nov 21, 2014
Messages
52
Reaction score
14
First Language
English
Primarily Uses
I was concerned about introducing "Observable" because it shares the same module name as RUBY 1.9.3 "Observable" - even though it has completely different usage.

Also, I was trying to write clean, semi-well documented, idiomatic RGSS/RUBY - so any consideration for improvement in that regard would be great :)

Was also hoping that the objects themselves weren't too obfuscated or that I wasn't committing any sins by introducing metaclass extensions or extending Kernal...

All-in-all, I'm glad this caught your attention.  I appreciate the input; very glad to hear about compatibility.

I would be especially interested in hearing about objects that you usually see people extending/overriding for scripts that can be painful (like your post about the battle manager a few days back) - and if it would be appropriate to add plugs for extensions to those objects into a core script like this one.
 
Last edited by a moderator:

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
For ruby stdlib stuff, I don't know much about it since I don't actually look at what the standard library offers (most of the ruby I write is RM, and RGSS doesn't have much of the stdlib). I don't look at much idiomatic ruby practices either (again, largely because I am focused on RM and there doesn't seem to be too much code reviews going on)


For example while Observable module is available in 1.9.3 lib, it isn't in RM, so unless someone actually adds it, shouldn't be too much of an issue. Now of course ruby devs may be confused so if that is a concern I'd just pick a different a name.

I would be especially interested in hearing about objects that you usually see people extending/overriding for scripts that can be painful (like your post about the battle manager a few days back) - and if it would be appropriate to add plugs for extensions to those objects into a core script like this one.
I have three "Core" scripts available on my script list


http://himeworks.com/rmvxa-scripts/


Those are the ones that are doing too much in a single method and are basically coding the logic according to their own specs and not providing much room for extension.
 
Last edited by a moderator:

tyler.kendrick

Caffeine Addict
Veteran
Joined
Nov 21, 2014
Messages
52
Reaction score
14
First Language
English
Primarily Uses
I'm a little overwhelmed at the shear volume of code and posts you have written for RM; always impressed when I visit your site.

The EffectsManager is interesting.  

What would be your opinion on using observables to enable more reactive extensions to objects (rather than explicit aliasing/overloading)?

So the syntax would be more akin to:

module EffectManager Game_Battler.subscribe_on:)use_item) { |options| options.success(->(item) { check_effects([item], "global")}) } def check_effects(item, scope) ... endendThis way, the extensions that do not overwrite methods, would be localized to the manager they were registered on.
 
Last edited by a moderator:

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
Is the example provided an example of defining an effect without explicit alias/overload/overwrite?
 

tyler.kendrick

Caffeine Addict
Veteran
Joined
Nov 21, 2014
Messages
52
Reaction score
14
First Language
English
Primarily Uses
yes - but its non-functional pseudo-code with a very naive understanding of your actual EffectManager code (not certain what else should be passed to use_item; maybe actor/enemy id?)

This way, your code "listens" to when Game_Batter#use_item is called, and invokes its own check_effects([item], "global") when Game_Battler#use_item is successful (doesn't throw an error).

This would only be for extending behavior, rather than mutating the behavior.  This means that any alias chains being invoked in Game_Battler#use_item will not be interrupted.

If you want to change the existing behavior and completely overload the existing behavior, then you will have to alias.

Effectively, this could change your EffectManager into a more localized Event Aggregator
 
Last edited by a moderator:

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
The effect manager is based on the existing implementation for effects, which is

1. For each effect2. Look up a table for the method to call for a given effect code3. Execute the methodI simply took the look-up table and changed it into "build the method name to call based on a bunch of arguments"That global example is actually a special case because of the way global events are handled, and I'm not too sure why they do it that way since the default engine doesn't execute common events until the action is finished anyways (which is why things like this exist)
 

Lemur

Crazed Ruby Hacker
Veteran
Joined
Dec 1, 2014
Messages
106
Reaction score
124
First Language
English
Primarily Uses
Quick runthrough test refactor, whether or not it entirely works depends on parts of RM I might not know.

Big things to consider:

  • This nonsensical thing most scripts have with ='s and -'s as lines is just distracting 90% of the time
  • In fact, most of the code doesn't need that many comments. You're being too verbose.
  • Imply returns, there's no need to use the word
  • You can use ``then`` with when case statements to get one liners
  • You can use ``tap`` for things that return themselves at the end of methods.
  • Remember you have ``unless``
I'd have to read through for content later when I have more time, those are just quick things I noticed. You all have a tendency to be extremely verbose. If you're going to do that, look into RDoc or something similar to have a standard comment instead of littering it all throughout your code.

Code:
=begin--------------------------------------------------------------------------------Author:     Tyler KendrickTitle:      Kendrick - CoreVersion:    v0.9.2 Language:   RGSS3Framework:  RPG Maker VX AceGit:        [URL="https://github.com/TylerKendrick/rmvxa--------------------------------------------------------------------------------=end%24imported"]https://github.com/TylerKendrick/rmvxa--------------------------------------------------------------------------------=end$imported[/URL] ||= {}$imported['Kendrick::Core'] = 'v0.9.2' # Note: The Kendrick Module will contain all Kendrick scripts and addons as a # namespace prefix.module Kendrick   # Note: Simplifies access for registering scripts.  def self.require(values = {})    Core.require(values)  end    # Note: This module exists to build and provide common errors.  module Errors    class << self            def build_errors        {          missing_method: -> (context, method) {            method ||= caller[0][/`.*'/][1..-2] # putting this in an actual Module#method destroys caller values.            missing_method(context, method)          },          missing_script: method(:missing_script)        }      end            def missing_method(context, method)        ::NotImplementedError.new("#{method} must be implemented on #{context}")      end            def missing_script(*args)        name, value, errorNo = args                ::ScriptError.new case errorNo          when :exact_version then "missing script: #{name} #{value}"          when :min_version   then "script: #{name} requires minimum version #{value}"          when :range_version then "script: #{name} must be between version range #{value.to_s}"          when :bool_include  then "script: #{name} #{value ? 'should' : 'shouldn\'t'} be included."          else "There was an error parsing the dependency: #{name}"        end      end            def [](index)        @@errors[index]      end              end # class << self     @@errors = ::Kendrick::Errors.build_errors     # This method is called by implemented classes to raise a common error.    def error(key, *args)      ::Kendrick::Errors[key].call(self, args)    end          end # Kendrick::Error    # Note: This object contains the majority of data structures in use by  # derived Kendrick Scripts.  module Core    include Kendrick::Errors        @@scripts = {}    Load_Method = :load_resources        def self.load_data(path, result)      result.tap { |r|        result.is_a?(::Array) ?          result.compact.each { |x| x.try_method(Load_Method) } :            result.try_method(Load_Method)      }    end            # This method is used to ensure dependencies are included before loading    # assets and data.    def self.preload_database      resolve_dependencies { |name, value, errorNo| ::Kendrick::Errors[:missing_script].call(name, value, errorNo) }    end        # Reserved for aliasing.    def self.load_database    end        # This method is called by implemented classes to register a dependency.    def self.require(values = {})      values.each { |key, value|        @@scripts[key] ||= []        @@scripts[key] << value unless item.include?(@@scripts[key])      }    end        # This method is called by Kendrick::Core to handle dependencies.    def self.resolve_dependencies(&on_error)      @@scripts.all? { |key, values|        values.all? { |value| resolve_dependency(key, value, &on_error) }      }    end        # Determines how to handle individual dependencies    def self.resolve_dependency(name, value, &on_error)      !$imported.has_key?(name).tap { |has_error|        on_error.call(name, value) if has_error && on_error      }    end      end # Kendrick::Core    # Note: This class simply wraps a method for unsubscription from observables.  class Observer            def initialize(&method)      @method = method    end        def call(*args, &block)      @method.call(*args, &block)    end      end # Kendrick::Observer    # Note: This module allows for notifications to be sent to observers.  module Observable     def create_observer(&method)      Observer.new(&method)    end        # Provide a method, lambda, anonymous method, proc, or callback.    def subscribe(&method)      create_observer(&method).tap { |observer|        (@observers ||= []) << observer      }    end        # Stops the observer from listening to notifications.    def unsubscribe(observer)      @observers.delete(observer)    end        protected        # Sends notifications to all registered observers.    def notify(*args, &block)      notify_all(@observers, *args, &block)    end        # Iterates through each observer to invoke #notify_observer.    def notify_all(observers, *args, &block)      @observers.all? { |observer| notify_observer(observer, *args, &block) }    end          # Invokes the observer with an optional callback that can be overloaded.    def notify_observer(observer, *args, &block)      observer.call(*args, &block)    end        end # Kendrick::Observable end # Kendrick # ::Game_BaseItemclass ::Game_BaseItem  # This field doesn't have a public accessor.  def id() @item_id endend # ::Game_BaseItem # ::Game_Battlerclass ::Game_Battler    # Determines if #current_action returns an ::RPG::Item instance.  def item?    current_action._?(:item)._?(:is_a?, ::RPG::Item)  end    # Determines if #current_action returns an ::RPG::Skill instance.  def skill?     current_action._?(:item)._?(:is_a?, ::RPG::Skill)  end    # Obtains the last used skill.  def skill    $data_skills[(skill? ? current_action.item : last_skill).id]  end    # Simplifies accessibility between classes "Game_Actor" and "Game_Enemy".  def data    if actor?      actor    elsif enemy?      enemy    end  end  end # ::Game_Battler # ::DataManagermodule ::DataManager  class << self        alias :kendrick_load_database :load_database    # Invokes the original #load_database with the new callback idiom.    def load_database      @callee ||= callee(:kendrick_load_database, {        before:   -> (*args)  { Kendrick::Core.preload_database },        complete: -> (status) { Kendrick::Core.load_database }      })       @callee.call    end        alias :registrar_load_data :load_data    # Allows classes generated by #load_data to be intercepted on creation if    # they implement #load_resources.  This is neccessary because Marshal loaded    # objects don't call initialize and, therefore, cannot be overridden.    def load_data(path)      Kendrick::Core.load_data(path, registrar_load_data(path))    end      end # class << selfend # ::DataManager # ::Kernalclass ::Kernal   # Tries to invoke a method if the context responsds to the symbol.  def self.try_method(context, method, *args, &block)    result = false    context.respond_to?(method).tap { |c|      if c        result = context.send(method, *args)        block.call(result) if block      end    }  end   # Evaluates text and returns the expression as a lambda.  def self.eval_method(text, *args)    parameters = (args ||= []).join(',')    eval "-> (#{parameters}) { #{text} }"  end  end # ::Kernal # ::Objectclass ::Object      # Obtains a method as a new Callee instance.  def callee(symbol, hash={}, &callback)    method(symbol).to_callee(hash, &callback)  end   # Allows for safe navigation.  Simplifies null coalescing index expressions.  def maybe(*args, &block)    is_a?(::NilClass) || !respond_to?(args.first) ? nil : send(*args, &block)  end  alias :_? :maybe   # Invokes ::Kernal#try_method with the caller provided as the current context.  def try_method(method, *args, &block)    ::Kernal.try_method(self, method, *args, &block)  end    # Invokes ::Boolean#convert with the caller provided as the current context.  def to_b    ::Boolean.convert(self)  end    # Simplifies multiple assignment operations as explicit block expressions.  def as    yield self  end end # ::Object # ::Calleeclass ::Callee < ::Kendrick::Observer    # Creates and registers a new callback for the target method.  def subscribe(hash = {}, &options)    @callbacks ||= []    ::Callback.new(hash, &options).tap { |callback| @callbacks << callback }  end    # Invokes a target method with exception handling provided by an options hash.  def call(*args, &block)    status = :not_modified        if @callbacks.all? { |x| x.before(*args) }      begin        @callbacks.each { |x| x.success(super(*args, &block)) }        status = :success      rescue Exception => e        @callbacks.each { |x| x.error(e, :error) }        status = :error      end    end     @callbacks.each { |x| x.complete(status) }  end  end # ::Callee # ::Callbackclass Callback    def self.options(hash = {}, &block)    Options.new(hash).tap { |options| block.call(options) if block }  end    def initialize(hash = {}, &block)    @options = Callback.options(hash, &block)  end      def before(*args)    @options.before.call(*args)  end    def error(*args)    @options.error.call(*args)  end    def success(*args, &block)    @options.success.call(*args)  end   def complete(*args, &block)    @options.complete.call(*args)  end      end # ::Callback # Provides a common idiom for callback structures.class ::Callback::Options    def self.before(*args)    true  end    def self.error(error, type)    raise error  end    def self.success(data = nil)    data  end    def self.complete(status)    status  end    Hash = {    before:   method(:before),    error:    method(:error),    success:  method(:success),    complete: method(:complete)  }    def initialize(options = {})    options   = ::Callback::Options::Hash.merge(options || {})    @before   = options[:before]    @error    = options[:error]    @success  = options[:success]    @complete = options[:complete]  end    def before(&before)    @before = before ? before : @before  end    def error(&error)    @error = error ? error : @error  end    def success(&success)    @success = success ? success : @success  end    def complete(&complete)    @complete = complete ? complete : @complete  end  end module Function   def to_callee(hash={}, &options)    ::Callee.new(&self).tap { |callee| callee.subscribe(hash, &options) }  end end Method.send(:include, Function)Proc.send(:include, Function) class Symbol    def to_callee(hash = {}, &options)    to_proc.to_callee(hash, &options)  end  end # This module just makes handling and parsing booleans much easier.module ::Boolean  # Be explicit about booleans, 0 isn't false here, don't treat it as such.  @@true_strings  = %w(true t)  @@false_strings = %w(false f)    # Attempts a custom, overloadable, conversion of objects to ::Boolean.  def self.convert(object)    case object    when ::Numeric then object != 0    when ::String  then @@true_strings.include?(object.downcase) # We can assume others are false    else !!object    end #case  end #self.convert      end # ::Boolean TrueClass.send(:include, ::Boolean)FalseClass.send(:include, ::Boolean) # This global method returns evaluated text as an anonymous Proc.def eval_method(text, *args)  ::Kernal.eval_method(text, *args)end
 

♥SOURCE♥

Too sexy for your party.
Veteran
Joined
Mar 14, 2012
Messages
693
Reaction score
411
Primarily Uses
  • This nonsensical thing most scripts have with ='s and -'s as lines is just distracting 90% of the time
  • In fact, most of the code doesn't need that many comments. You're being too verbose.
I'd have to read through for content later when I have more time, those are just quick things I noticed. You all have a tendency to be extremely verbose. If you're going to do that, look into RDoc or something similar to have a standard comment instead of littering it all throughout your code.
The header and method separators are not a bad idea when writing code (for public use) for RPG Maker, consistency in style is important.

Most scripts people publish around here are written for non-programmers, and they would greatly appreciate detailed commenting of the code if they try to edit or change something (or even learn), even if the comments seem obvious to a programmer.
 

Archeia

Level 99 Demi-fiend
Developer
Joined
Mar 1, 2012
Messages
15,141
Reaction score
15,473
First Language
Filipino
Primarily Uses
RMMZ
Quick runthrough test refactor, whether or not it entirely works depends on parts of RM I might not know.

  • In fact, most of the code doesn't need that many comments. You're being too verbose.
This is not a bad thing considering this is the RPG Maker community where public scripts and compatibility is in high demand. The more info in the script, the better it is for non-programmers and programmers alike.
 

tyler.kendrick

Caffeine Addict
Veteran
Joined
Nov 21, 2014
Messages
52
Reaction score
14
First Language
English
Primarily Uses
@Lemur

I sincerely appreciate the feedback; I also entirely relate to your sentiments regarding excessive comments.

Clean, well-written, code should document itself through meaningful symbol names.  Anything more than that just adds to the amount of text you have to maintain in your code file...and I loathe maintaining documentation.

Also, I was entirely unaware of the existence of the "tap" keyword.  I am more familiar with that feature being called "pipe" or "with" in other languages.  Glad to see some functional goodies in Ruby - will be looking forward to exploring what else Ruby has to offer in that paradigm.

Didn't know about the "%w" feature either.  Very useful information.

Thanks for taking the time to do a quick refactor too - especially helpful to see the difference.
 

Lemur

Crazed Ruby Hacker
Veteran
Joined
Dec 1, 2014
Messages
106
Reaction score
124
First Language
English
Primarily Uses
http://yardoc.org/guides/index.html

Then use a standard documenting tool already out there. This prevents a lot of needless commenting and forces you to consider the size of your functions if you honestly can't document one properly. The reason so much RM code needs that verbosity is because people try and stuff too much into one method.

If you honestly want to get better at programming, read this book: http://www.amazon.com/Practical-Object-Oriented-Design-Ruby-Addison-Wesley/dp/0321721330

As far as piping and other language mods, you could look at:

Me and banister tend to have a back and forth on hacking this into Pry somehow once in a while.

Now as far as making things friendly for non-programmers, that's what you make a DSL for. Heck, Ruby is probably the second strongest language in existence for this task, beaten only by Lisp. Consider RSPEC:

describe Person do  let :)person) { Person.new('foo', 20) }   describe '#name' do    it 'has a name' do      expect(person.name).to eq('foo')    end  endend
Even if you've never used Ruby, that's fairly straightforward. The issue is that most scripts focus on the nuts and bolts approach to configuration. Consider reading:

To me, honestly, I cannot comprehend the mentality that one would want to program and not strive to become better at it. It's beyond me. Anything worth doing in life is going to be hard, it's going to take time, and it's going to probably make you feel like a fool more times than you care to admit.

If you're looking for a good book list to get you started, give this a read: http://www.dreamincode.net/forums/topic/339753-bestwhich-books/page__view__findpost__p__1968668
 

tyler.kendrick

Caffeine Addict
Veteran
Joined
Nov 21, 2014
Messages
52
Reaction score
14
First Language
English
Primarily Uses
Then use a standard documenting tool already out there.
After looking into YARD, I will likely be using that from now on.  I wasn't aware that powerful documenting tools like that existed - much less so for Ruby.  I'm surprised, but very happy, to see a product this well developed.  Thanks again for the introduction.

As far as piping and other language mods...Neat; I didn't know you liked Haskell.  Ever played with F#? You will likely enjoy it if you haven't tried it already.

I was actually writing something nearly identical to funkify before posting this.  Nice to know the functional paradigm isn't completely missing from the Ruby community.

Speaking of paradigms, I also noticed that none of your hosted code seems to implement reactive behaviors?

Streamable, Qenumerable, and Sails could probably benefit from some reactive love.  In case you weren't aware of the paradigm, here are a few links:



The reason so much RM code needs that verbosity is because people try and stuff too much into one method.
I find that less true with Ace code.  The excessive documentation seems to be more for accommodating people who don't want to write code - which is probably most of the RM Community.  The methods are usually broken into very small units - I would even argue it looks like the engine was developed by a team that implemented TDD practices.

Now as far as making things friendly for non-programmers, that's what you make a DSL for
Interesting idea; I see merit in configuring your apis to work as an internal DSL, but because the majority of code in that script are simple extensions to core objects, I don't think it really applies.  Even with most configuration sections in RGSS Scripts, the values are not dynamically configured - not even by the api consumer; they are usually just simple constants defined at the top of the script. 

Also, the community coding conventions seem to require comments that describe what each unit of code does, and how it works.  I don't like it either; but writing code that doesn't follow the accepted conventions tends to not get adopted by the community.

That being said, I recently wrote a quick script to configure Commands for Menus in Ace which I will absolutely be writing a dsl for (Because of dynamically configured behavior, rather than constant values).
 

Engr. Adiktuzmiko

Chemical Engineer, Game Developer, Using BlinkBoy'
Veteran
Joined
May 15, 2012
Messages
14,682
Reaction score
3,003
First Language
Tagalog
Primarily Uses
RMVXA
The methods are usually broken into very small units
Which I think most would call a modular approach or something. It's good, especially for writing stuff in which you want the parts to be easily replaceable/removable without altering the main process. And makes it easier to use the same codes for different stuff rather than writing the same methods over and over again.
 
Last edited by a moderator:

tyler.kendrick

Caffeine Addict
Veteran
Joined
Nov 21, 2014
Messages
52
Reaction score
14
First Language
English
Primarily Uses
Which I think most would call a modular approach or something
That was almost exactly my point :)

My use of the word "unit" in that context was to suggest that the RM engine code for Ace was developed in a way for the development team to develop unit tests first, and implementation to satisfy those tests later.  i.e. Test Driven Development. 

This was to counter the argument lemur provided:

Code:
The reason so much RM code needs that verbosity is because people try and stuff too much into one method
 

Users Who Are Viewing This Thread (Users: 0, Guests: 1)

Latest Threads

Latest Posts

Latest Profile Posts

Couple hours of work. Might use in my game as a secret find or something. Not sure. Fancy though no? :D
Holy stink, where have I been? Well, I started my temporary job this week. So less time to spend on game design... :(
Cartoonier cloud cover that better fits the art style, as well as (slightly) improved blending/fading... fading clouds when there are larger patterns is still somewhat abrupt for some reason.
Do you Find Tilesetting or Looking for Tilesets/Plugins more fun? Personally I like making my tileset for my Game (Cretaceous Park TM) xD
How many parameters is 'too many'??

Forum statistics

Threads
105,865
Messages
1,017,059
Members
137,575
Latest member
akekaphol101
Top