Aller au contenu principal
Voir

Le monkey-patch : principe et application

PSDK est un moteur qui se met à jour régulièrement. Si on modifie directement un fichier du moteur pour changer un comportement, la prochaine mise à jour écrasera les modifications. Le monkey-patching permet de modifier le comportement de PSDK depuis ses propres scripts, sans toucher au code source du moteur. Ce guide couvre les différentes techniques de monkey-patching utilisées dans PSDK : prepend pour modifier globalement une méthode, l'héritage pour un changement local à une seule scène, et la réouverture de module pour les constantes et fonctions d'enregistrement.

Qu'est-ce que le monkey-patching ?

Le monkey-patching consiste à modifier, à l'exécution, le comportement d'un code existant sans toucher à son fichier source. En Ruby, c'est possible parce que les classes et les modules sont ouverts : on peut les rouvrir à tout moment pour ajouter, remplacer ou enrichir des méthodes.

Dans PSDK, le monkey-patching est indispensable. Le code du moteur n'est pas dans le projet — il est chargé en interne par PSDK. On ne peut pas (et on ne doit pas) le modifier directement. On travaille donc depuis ses propres scripts, placés dans scripts/, qui sont chargés après le moteur.

Trois techniques permettent de monkey-patcher dans PSDK :

  • prepend — insérer un module avant une classe ou un module dans la chaîne d'héritage, pour intercepter ou enrichir une méthode existante. C'est la technique principale.
  • Réouverture de module — rouvrir un module PSDK pour y ajouter des méthodes, des constantes ou des fonctions d'enregistrement, sans modifier l'existant.
  • Héritage — créer une sous-classe pour un changement local à une seule scène ou un seul composant, sans affecter le reste du jeu.

prepend est privilégié par rapport à la réouverture directe de classe (redéfinir une méthode sans module) grâce à sa composabilité : si plusieurs plugins modifient la même méthode, chaque super passe le relais au suivant dans la chaîne. Redéfinir la méthode directement écraserait les patches des autres plugins.

Où placer ses monkey-patches

Tous les scripts utilisateur vont dans le dossier scripts/ à la racine du projet. C'est le seul endroit où PSDK charge du code personnalisé. La structure typique :

scripts/
my-project/
001 Patches/
000 BattleLogic.rb <- monkey-patch sur Battle::Logic
001 ItemUsage.rb <- monkey-patch sur Util::Item
002 Features/
...
  • Les scripts dans scripts/ sont toujours chargés après le code interne de PSDK. Le prepend fonctionne donc naturellement : la classe ciblée existe déjà au moment du chargement.
  • On regroupe les patches dans un sous-dossier dédié pour les retrouver facilement.

Prepend simple : intercepter et déléguer

Le cas le plus courant : on veut intercepter un appel à une méthode PSDK, vérifier une condition, et soit court-circuiter le comportement soit laisser PSDK faire son travail via super.

Exemple : bloquer l'EXP après capture selon la génération

module Battle
class Logic
# Patch to prevent EXP distribution after catching when using gen-based EXP sharing
module EXPAfterCatchingPokemon
# Function that process the battle end when Pokemon was caught
def battle_phase_end_caught
return if $game_variables[Yuki::Var::GEN_EXP_SHARE].between?(1, 5)

super
end
end

prepend EXPAfterCatchingPokemon
end
end
  • module Battle / class Logic : on rouvre la classe PSDK existante. On ne crée pas une nouvelle classe, on étend celle qui existe déjà dans le moteur.
  • Le module EXPAfterCatchingPokemon est déclaré à l'intérieur de Battle::Logic. C'est une convention PSDK : le module de patch vit dans la classe qu'il modifie.
  • battle_phase_end_caught est la méthode PSDK originale qu'on intercepte. Il faut connaître son nom exact (en lisant le code PSDK ou la documentation).
  • return if ... court-circuite la méthode : si la condition est vraie, on sort immédiatement sans distribuer d'EXP.
  • super appelle l'implémentation originale de PSDK. Sans ce super, le comportement original serait entièrement supprimé.
  • prepend EXPAfterCatchingPokemon insère le module avant Battle::Logic dans la chaîne d'héritage. Tout appel à battle_phase_end_caught passe d'abord par le module.

Prepend complexe : enrichir avant de déléguer

Quand on veut gérer un nouveau cas qui n'existe pas dans PSDK, puis laisser PSDK gérer tous les autres cas normalement.

Exemple : intercepter l'utilisation d'items offensifs

module Util
module Item
# Patch to handle attack items in battle
module AttackItemPatch
# Use an item in a GamePlay::Base child class
# @param item_id [Integer] ID of the item in the database
# @return [PFM::ItemDescriptor::Wrapper, false] item descriptor wrapper if the item could be used
def util_item_useitem(item_id, &result_process)
item_wrapper = PFM::ItemDescriptor.actions(item_id)
return super unless item_wrapper.attack_item && $game_temp.in_battle

if item_wrapper.chen
display_message(parse_text(22, 43))
return false
elsif item_wrapper.no_effect
display_message(parse_text(22, 108))
return false
end

return util_attack_item_on_use_sequence(item_wrapper, result_process)
end
end

prepend AttackItemPatch
end
end
  • return super unless item_wrapper.attack_item && $game_temp.in_battle : si l'item n'est pas un item offensif ou qu'on n'est pas en combat, on délègue entièrement à PSDK. Le patch est alors invisible pour tous les cas existants.
  • Les cas chen et no_effect sont des gardes supplémentaires : on affiche un message et on retourne false pour bloquer l'utilisation.
  • util_attack_item_on_use_sequence est une nouvelle méthode définie ailleurs dans nos scripts (pas dans le module de patch). Le module de patch ne contient que l'interception.
  • Le pattern return super unless condition est idiomatique dans PSDK : on traite son nouveau cas, et tout le reste passe par le comportement original.

Prepend sur un module inclus

La même technique fonctionne sur les modules PSDK qui sont inclus dans des classes. On rouvre le module, on déclare le patch, et on prepend.

Exemple : rediriger les items offensifs dans l'UI de combat

module BattleUI
module PlayerChoiceAbstraction
# Patch to redirect attack items to the attack item choice flow
module AttackItemShortcutPatch
# Redirect attack items to the attack item choice flow
# @param item [Studio::Item] the item to use
def use_item(item)
item_wrapper = PFM::ItemDescriptor.actions(item.id)
return super unless item_wrapper.attack_item

scene.attack_item_shortcut = item_wrapper
@result = :bag
end
end

prepend AttackItemShortcutPatch
end
end
  • BattleUI::PlayerChoiceAbstraction est un module PSDK inclus dans plusieurs classes de scène de combat. Le prepend affecte toutes les classes qui incluent ce module.
  • Le pattern est identique : return super unless condition, puis le traitement spécifique.

Extension d'une API d'enregistrement

Quand PSDK expose un hash ou une structure d'enregistrement via module_function, on peut étendre cette structure sans prepend. On rouvre le module et on ajoute de nouvelles fonctions d'enregistrement.

Exemple : ajouter une API d'enregistrement d'items offensifs

module PFM
module ItemDescriptor
module_function

# Define a usage of an attack item from the bag
# @param klass [Class<Studio::Item>, Symbol] class or db_symbol of the item
# @yieldparam item [Studio::Item] the item to use
# @yieldparam scene [GamePlay::Base]
def define_on_attack_item_use(klass, &block)
raise 'Block is mandatory' unless block_given?

EXTEND_DATAS[klass] ||= Wrapper.new
EXTEND_DATAS[klass].on_attack_item_use = block
EXTEND_DATAS[klass].attack_item = true
end

# Wrapper extension for attack items
class Wrapper
# Tell if the item is an attack item
# @return [Boolean]
attr_accessor :attack_item
# Register the on_use block
attr_writer :on_attack_item_use

# Call the on_use block
# @param scene [GamePlay::Base]
def on_attack_item_use(scene)
@on_attack_item_use&.call(@item, scene)
end
end
end
end
  • EXTEND_DATAS est un hash existant dans PSDK. On ne le recrée pas, on y ajoute des entrées via ||=.
  • module_function rend define_on_attack_item_use appelable comme PFM::ItemDescriptor.define_on_attack_item_use(...).
  • La classe Wrapper est rouverte pour y ajouter de nouveaux attributs. Cela fonctionne parce que les constantes PSDK ne sont pas gelées avec .freeze.
  • Cette approche est non-destructive : les enregistrements existants dans EXTEND_DATAS restent intacts.

Héritage : monkey-patching local

Le prepend est global : il affecte toutes les instances de la classe dans tout le jeu. Quand on veut un changement local (une seule scène, un seul composant), l'héritage est la technique de monkey-patching à privilégier.

Exemple : personnaliser GenericBase pour une scène spécifique

module UI
# Custom base UI for the Mystery Gift scene
class MysteryGiftBase < GenericBase
private

# Return the background filename
# @return [String]
def background_filename
return 'mystery_gift/background'
end

# Disable the background scroll animation
def create_background_animation; end
end
end
  • MysteryGiftBase < GenericBase crée une sous-classe qui n'affecte que la scène Mystery Gift.
  • Si on avait utilisé prepend sur GenericBase, toutes les scènes du jeu auraient été affectées.
  • Règle : si le changement est propre à une seule scène et ne doit pas affecter les autres, préférer l'héritage au prepend.

Règles de survie

Quelques règles essentielles pour un monkey-patching propre dans PSDK :

  • Toujours appeler super : sauf si le but explicite est de bloquer le comportement original. Oublier super casse silencieusement la chaîne.
  • Jamais de .freeze sur les constantes : .freeze empêche l'extension des structures via prepend ou réouverture. Il faudrait alors supprimer et reconstruire la constante au lieu de simplement ajouter.
  • Un module par classe patchée : si on patche trois méthodes d'une même classe, on peut les grouper dans un seul module de patch. Mais ne jamais mélanger des patches de classes différentes dans un même module.
  • Nommer le module de patch explicitement : le nom doit décrire ce que fait le patch (EXPAfterCatchingPokemon, AttackItemPatch), pas juste Patch ou Fix.
  • Documenter les méthodes patchées : noter quelque part (README, commentaire en tête de fichier) quelles méthodes PSDK sont modifiées. Lors d'une mise à jour de PSDK, cela permet de vérifier rapidement si les méthodes patchées ont changé.

Conclusion

  • Le monkey-patching est nécessaire dans PSDK : modifier directement le moteur serait écrasé à la prochaine mise à jour. On travaille toujours depuis ses propres scripts.
  • prepend est le mécanisme standard pour modifier une méthode PSDK existante. Il préserve la chaîne d'appel via super et permet à plusieurs patches de coexister.
  • Le pattern idiomatique est return super unless condition : on traite son nouveau cas, et tout le reste passe par le comportement original.
  • Pour étendre une API d'enregistrement, on rouvre le module et on ajoute de nouvelles fonctions sans écraser l'existant.
  • L'héritage est préférable au prepend quand le changement est local à une seule scène ou composant.
  • Ne jamais oublier super, ne jamais utiliser .freeze, et toujours documenter les méthodes patchées.