icone facebook icone facebook



Le principe

L'idée m'est venue pendant une de mes pauses-glandouille, pauses pendant lesquelles j'ai l'habitude de divaguer entre les vidéos débiles du net pour m'amuser et abrutir encore plus qu'elle ne l'est ma cervelle. Je tombe sur une vidéo assez populaire mais qui ne m'avais jamais été proposée, montrant un anglophone essayant sur lui un de ces colliers de dressage pour chiens.

Pour ceux qui ne connaissent pas, ces colliers sont capables de détecter (plus ou moins bien) les aboiements et émettent des décharges électriques assez désagréables pour que le chien finisse par associer l'aboiement avec la douleur et s'arrête donc d'aboyer pour rien.
Inutile de dire que la vidéo me parut fort drôle et qu'elle m'inspira très vite l'idée d'une TAC (Technology Assisted Connerie).

Dans le principe, l'idée est presque la même: associer un événement désagréable a une peine désagréable. Sauf que l'événement désagréable l'est pour celui qui subira la peine ! Pour que ca reste tout de même intéressant hormis l'intérêt purement masochiste, je me suis dit qu'il fallait que ces événements dépendent d'une qualité (agilité, vitesse, mémorisation...) ou de la chance du sujet. Le meilleur choix a mon goût était donc la console de jeu vidéo !

Concrètement, cela donne des jeux vidéos capables de piloter ces colliers pour infliger directement des bourres aux joueurs lorsqu'ils perdent (ou par exemple, lorsque l'adversaire gagne, les possibilités et variantes peuvent être nombreuses !).

Pourquoi la Megadrive ?

Au début, je pensais faire ca sur NES car c'était une console qui restait encore aujourd'hui relativement populaire, elle possédait une vaste ludothèque, et restait a ma portée au niveau complexité (bien que je sois connu par certains pour être un detésteur de cette horreur qu'est le 6502, son processeur).

Je me suis vite rappelle que la NES, en plus d'avoir un processeur de merde, était la console connue pour avoir une innombrable flopée de mappers, ces chips permettant de fragmenter la mémoire afin d'avoir des jeux plus ou moins gros. Comme je n'avais fait qu'une cartouche flash sur un circuit SLROM, on n'aurait pu jouer qu'aux jeux sortis sur ce même circuit, ce qui aurait d'un coup bien restreint nos choix (en plus d'avoir eu a flasher les différents jeux entre les parties, galère pour jouer chez des potes...).

Le fait que ma cartouche flash n'était plus reconnue quel que soit le jeu flashé m'a définitivement convaincu de changer de plateforme. Le choix suivant "logique" pour moi était la Master System (Alex Kidd, ca vous dit quelque chose ?).
Un choix de jeux a peu près comparable a celui de la NES, des capacités comparables également, un processeur que je connais bien mieux, mais malheureusement, bien que les cartouches utilisaient deux voire trois types de mappers différents, ceux-ci étaient intégrés dans les ROMs (puces de mémoire dans les cartouches), ce qui rendait encore la tache assez complexe et ne m'aurait pas empêché de devoir re-flasher la cartouche a chaque fois que je voulais jouer a un autre jeu.

On retourne chez Nintendo ? La SNES ? Non merci, je voulais m'amuser, pas avoir a apprendre a coder sur un patchwork simili-16 bits. C'est exactement a ce moment la que je me suis souvenu que le Grand Realmyop, dans sa divine bonté, m'avait offert une Everdrive MD afin de lui faire un logiciel de VJing (que je n'ai toujours pas fini d'ailleurs), et que cet outil était pose juste la, devant moi, pendant que je galerais avec ma NES.

Il ne m'a pas fallu plus de temps pour me décider: ca sera la Megadrive. Tout les éléments décisifs étaient réunis: Le choix de jeux, la simplicité de larguer les roms sur une carte SD, et la disponibilité du hardware en cas d'accident (car oui, ne me dites pas qu'il n'y a pas de Megadrive a 20 Euros dans le Cash Express a cote de chez vous...).

Le hardware cote joypads

Tout d'abord j'aimerais en rassurer certains. Pour ceux qui voudraient se lancer dans l'aventure de cette modification mais qui seraient trop protecteurs pour toucher a un poil de leur Megadrive, n'ayez crainte: Pas besoin de modifier votre chère console pour vous adonner aux joies de la haute tension, on ne touchera qu'aux joypads ! (Qui pourront encore très bien fonctionner normalement).

On veut pouvoir piloter des colliers en "tout-ou-rien", c'est a dire soit une décharge, soit pas de décharge. Pour cela il nous faut juste une sortie numérique du coté console. Connaissant déjà un peu la Megadrive, je savais qu'une telle sortie était disponible sur les ports joypads. Au premier abord, sans rien connaître de la console, on pourrait se dire que les ports joypads ne sont que des entrées, qui correspondent a chaque bouton. Mais comment ca se passe avec les 11 boutons des joypads alors que ce sont des ports DE9 qui n'ont que 7 broches de données (2 servant a l'alimentation) ?

Les 7 broches de ces ports sont en fait configurables en tant qu'entrée ou sortie. Typiquement, les jeux en configurent 6 comme entrées, et 1 en sortie. Il y a un circuit intégré dans les joypads qui est câblé de sorte a "écouter" cette sortie pour donner l'état d'un groupe de boutons, ou de l'autre. Le jeu qui souhaite lire l'état de tous les boutons mettra donc la sortie a 0 pour lire les boutons Haut, Bas, Gauche, Droite, B et C, puis mettra la sortie a 1 pour lire les boutons Start et A.
Ca semble pas très logique ni facile comme ca, mais c'est une question de simplicité et de réduction de coûts au niveau électronique.
J'appellerais cette sortie qui pilote les joypads "SELECT" (non, aucun rapport avec un bouton du même nom chez d'autres marques).

La plupart des jeux basent leur timing et la lecture de ces boutons sur la fréquence de rendu des images a l'écran, c'est a dire 50 ou 60Hz. On a donc des impulsions sur SELECT 50 ou 60 fois par seconde. Par contre, il n'est pas dit qu'un jeu mette d'abord SELECT a 0 puis a 1, il peut très bien faire l'inverse, il n'y a pas de standard et j'ai vu les deux cas dans des jeux commerciaux (chez les mêmes developpeurs !).

Je pensais donc utiliser cette sortie pour véhiculer des informations supplémentaires, et rendre les joypads un peu plus intelligents pour qu'ils soient capables de comprendre ces nouvelles informations. Au départ je pensais utiliser un circuit compteur classique, qui compterait le nombre d'impulsions reçues sur SELECT et qui se remettrait a zéro lorsqu'il n'y avait pas d'impulsion pendant un certain temps (pour éviter de compter les impulsions 50/60 fois par seconde).

N'ayant pas trouvé de solution simple pour cette remise a zéro sachant que le signal pouvait prendre plusieurs formes différentes selon les jeux, j'ai opté pour la simplicité en utilisant un microcontroleur ATTiny25 (Atmel). Ils sont petits, ont 5 broches d'entrées/sorties, fonctionnent en 5V, ont un oscillateur interne, et sont peu chers. Ils offrent tout ce dont on a besoin: comprendre un signal défini en entrée et activer ou non une sortie en fonction, et le tout peut simplement être modifie au niveau logiciel. Comme je me doute que beaucoup d'entre vous n'ont pas de quoi les programmer, je peux vous dépanner sur simple demande par mail.

Passons donc aux choses sérieuses. Il y a de grandes chances que vous ayez un joypad officiel Sega, si ce n'est pas le cas, il vous faudra trouver par vous même quel fil correspond a la ligne SELECT. Dans les joypads Sega, c'est simple, il est sur la broche 1 du seul circuit intégré (un 74HC157, quadruple multiplexeur 2-vers-1).

On va le connecter a la broche 2 de notre ATTiny25. On va aussi lui apporter du courant en repiquant le 5V comme indiqué en rouge, et la masse comme indiqué en gris. En option, on peut ajouter une LED après une résistance de quelques centaines d'ohms sur la broche 7 du microcontroleur, qui pourra être utile pour savoir si le signal est bien compris sans avoir de collier pour tester. Finalement, la broche 3 servira de signal de commande pour le collier (appelé ici SIG).

Tout ce petit monde peut facilement tenir sur un petit bout de stripboard et placé dans une des zones creuses du joypad. Un trou pourra être fait près du câble pour passer la LED si vous avez choisi d'en mettre une. C'est aussi une bonne idée de garder le microcontroleur sur un support lyres, la hauteur n'est pas un problème et ca évitera d'éventuels soucis causés par un mauvais flashage.

On peut aussi profiter de l'occasion pour nettoyer les contacts des boutons a l'alcool, passer un coup de crayon de papier sur les membranes conductives, et mettre quelques épaisseurs de papier sous le disque de la croix directionnelle pour durcir le toucher.

Pour la connection au collier, j'ai utilisé un connecteur Molex 3 voies (Alim + signal) a l'apparence peu esthétique mais très robuste et simple a installer. J'ai coupé 3mm du bord du plastique sur le bas du joypad pour placer le connecteur mâle, avec ses broches a souder pliées vers l'extérieur pour l'empêcher de bouger (voir photo). Il faut bien penser a ne pas faire passer de fils ou de broches devant les colonnes qui servent a guider les vis ou ca coincera au remontage.

Sur la photo on peut voir que mon brochage du connecteur de sortie est le suivant, de gauche a droite: +5V, Masse, Signal de sortie vers le collier. Libre a vous d'en choisir un autre, l'important étant de vous en souvenir pour ne pas cramer le collier.

 

Le hardware coté collier

Le collier que j'ai acheté sur eBay se trouve a un prix étonnement bas quand on connaît ceux pratiqués par les boutiques spécialisées (surtout en VPC, important du matériel Américain). De nombreux brevets tels que celui-ci et celui la décrivent clairement comme ces colliers délivrent les décharges électriques. Un simple transformateur et un(deux) transistor(s) sont utilisés pour convertir des impulsions de basse tension provenant de la pile vers des impulsions de tension plus élevée avec un courant moindre.
Les colliers capables de moduler la puissance des impulsions le font soit en les espacant temporellement, soit en court-circuitant le transformateur pour réduire leur durée et leur valeur maximale.


Le vendeur a eu la gentillesse d'offrir la pile 6V (qui n'est pas des plus courantes) et une petite ampoule néon pour tester le collier sans avoir nécessairement mal.

Ce genre d'ampoules peut encore se trouver dans certains appareils électroménagers anciens ou bas de gamme comme indicateurs de présence d'une tension relativement élevée (centaine de volts) sans avoir recours a des résistances et des diodes. En effet, ici une LED ne survivrait sûrement pas aux impulsions de haute tension, bien que très courtes et de faible intensité. En revanche, une ampoule classique ne conviendrait pas non plus car elle s'allumerait avec une tension de l'ordre de quelques volts et serait donc un mauvais indicateur du bon fonctionnement du collier.

Comme il était impossible de trouver des valeurs de tension et de courant sur eBay et dans le mode d'emploi, j'ai du effectuer quelques mesures simples moi même pour au moins savoir a quoi m'attendre (si j'allais souffrir beaucoup ou énormément).

Pour avoir des valeurs qui veulent dire quelque chose, il faut simuler les conditions réelles d'utilisation, c'est a dire en alimentant le collier avec le 5V qui sera délivré par la Megadrive plutôt que le 6V de la pile, et aussi simuler la résistance de la peau avec une résistance couche carbone classique.

Mon multimetre indiquait environ 1Mohm par centimètre de peau, j'ai donc bêtement pris une résistance de 5.6Mohms (valeur normalisée) pour la placer entre les électrodes du collier et mesurer la tension a ses bornes avec mon oscilloscope. Il faut savoir que la résistance de la peau varie beaucoup selon son humidité. En mouillant mes doigts et en appuyant les pointes de touche le plus possible, la valeur descendait a 800kOhms.

La visualisation a l'oscilloscope a malheureusement un intérêt limité car mes sondes ne sont capables d'atténuer que par 10 (et pas par 100). J'ai pas voulu monter un circuit diviseur avec un AOP en suiveur par simple flemme... Je me suis juste contenté de remarquer que le pic de tension positive dépassait amplement les 200V (on peut imaginer le tracer monter jusqu'au double), et qu'il dure une centaine de microsecondes.

En prenant 500V pour viser large, on trouve 500/5,6.10e6 = 90μA, rien de dangereux en somme (ca ne veut pas dire que vous n'aurez pas mal !).

On ne le voit pas sur la photo mais j'ai également observé le signal sur la base du transistor qui commute la tension de la batterie sur le primaire du transformateur, histoire de voir a quoi ressemblaient réellement les décharges. Il se trouve que le fabricant du collier a choisi de balancer des pics de 50μs espacés de 300μs par rafales de 50, ces rafales étant elles-meme espacées de 100ms et répétées 30 fois (pour une durée totale d'environ 3.5 secondes).

Le collier est pilote par son propre microcontroleur mais je ne vais pas détailler plus son fonctionnement (détection des aboiements, punitions progressives...) car on sait déjà tout ce qu'il faut savoir.

On va se contenter de bypasser complètement le cerveau du collier en coupant la connection entre son microcontroleur et le transistor connecte au transformateur. Sur le schéma ci-contre on peut voir qu'on utilisera qu'en fait une petite partie des composants du collier, juste de quoi fournir les impulsions de haute tension.

Il faudra donc que le microcontroleur qu'on a ajoute dans les joypads produise un signal de commande similaire a celui d'origine, rien de complique a faire avec quelques boucles et temporisations. Ceci permettra même de prévoir plusieurs durées de décharges afin de punir plus ou moins sévèrement !

Pour faire la liaison avec les joypads, j'ai utilise un vieux câble USB avec données. Comme on a besoin que de 3 conducteurs, un fil ne servira a rien.

Les connections pour l'alimentation 5V vont se faire sur les deux grosses soudures du ressort et de la plaque positive pour la pile tandis que le fil SIG ira sur la résistance juste avant le transistor. A noter que la valeur de cette résistance peut varier selon les modèles. Vu la faible durée des impulsions de commande on peut l'omettre pour obtenir des pics de tension plus élevées si on aime le risque.

Comme il est fort probable que le câble subisse des tractions pendant la séance de torture, il vaut mieux ne pas faire confiance aux soudures et faire un nœud dans le câble avant qu'il ne sorte du boîtier du collier. De cette manière, c'est le plastique du boîtier qui s'opposera a la force et non les soudures (pratique commune dans le matériel bas de gamme).

La prise est réalisée avec le complément femelle du connecteur Molex précédemment installe sur le joypad. Pas besoin de pince a servir ici, il suffit de souder les fils sur les broches métalliques et de les insérer dans le plastique.

Dans un souci de résistance mécanique également, ces connecteurs sont dotes d'un clip auto-bloquant. J'ai ajoute quelques centimètres de gaine thermoretractable sur la base du connecteur, que j'ai remplie de colle chaude afin d'infliger l'effort de la traction sur le connecteur plutôt que sur les soudures des fils.

 

Le collier uns fois connecte au joypad. J'admet que le connecteur blanc n'est pas ce qu'il y a de plus beau mais rien ne vous empêche de le peindre ou de mettre un coup de marqueur noir !

Notez aussi la LED rouge a droite du câble en haut.

 

 

 

 

Le firmware du microcontroleur

Le code du microcontroleur a été écrit en C pour AVR-GCC. Voici les telechargements disponibles:

Le code source (v1). Le projet AVRStudio complet. Le binaire hex (v1).

Le principe général est simple et reprend ce que j'ai explique concernant les modifications sur le joypad et le collier.
On veut détecter un signal particulier venant de la console, et dans le cas de sa détection, fournir un signal de commande en sortie.
Afin de simplifier la modification des jeux Megadrive, j'ai décidé de déléguer la temporisation des décharges au microcontroleur. Les jeux devront donc simplement envoyer un signal pour lancer une décharge, qui s'arrêtera automatiquement X secondes plus tard.

Comme décrit plus haut, on veut détecter un signal que le jeu devra générer spécialement pour commander le collier et on veut ignorer tous les autres signaux (question d'éviter a tout prix les faux déclenchements, sinon c'est pas juste !).

On sait qu'en temps normal, le signal SELECT reçu par le joypad change d'état 50 ou 60 fois par seconde. Il y a cependant un cas donc je n'ai pas parle, qui est celui des joypads a 6 boutons. Je ne vais pas détailler leur fonctionnement ici car ca nous importe peu pour ce qu'on veut faire, mais il faut savoir que les jeux qui supportent ces joypads peuvent provoquer jusqu'a 8 changements d'état par lecture !
Pour éviter donc que ces lectures spéciales ne déclenchent intempestivement les colliers, il va falloir que notre code ignore tout signal comportant 8 fronts ou moins. Toujours dans un souci de fiabilité, j'ai choisi de mettre le seuil a 10 fronts.
Pour réaliser le comptage de ces fronts, j'ai eu recours a l'interruption Pin Change du ATTiny25. Cette interruption se déclenche des lors que l'état d'une ou des broches définies change. Nul besoin de savoir si la broche passe de 1 a 0 ou de 0 a 1, seul compte le changement d'état. De cette manière, l'état par défaut du signal ne nous dérangera pas.

Il faut également que le comptage des fronts se reinitialise a zéro après une durée d'inactivité sur SELECT, afin de ne pas incrementer le compteur a chaque lecture du joypad (qui je le rappelle, intervient 50/60 fois par seconde). Pour cela on utilisera le Timer0 et son interruption Overflow. Le Timer0 est un compteur 8bits qui s'incrementera automatiquement a intervalles courtes et régulières, lorsque sa valeur maximum sera atteinte (255), il déclenchera l'interruption Overflow et permettra de remettre le compteur de fronts a zéro.
Lorsqu'un front est détecté par l'interruption Pin Change, on remettra le Timer0 a zéro pour reinitialiser la durée d'expiration.
Il faut veiller a choisir la fréquence d'incrementation du Timer0 de sorte a ce que l'interruption Overflow soit provoquée sous moins de 1/60=17ms (le pire des cas étant lorsque la console est en 60Hz), mais que cette interruption n'ait pas lieu entre les changements d'état (sinon le compte ne dépassera jamais 1).

On mettra le prescaler du Timer0 a 3, qui selon la documentation Atmel indique une division de la fréquence principale par 64. Si on fait tourner notre microcontroleur a 8MHz, on peut calculer la fréquence d'incrementation du Timer0: 8.10e6/64 = 125kHz. Avec cette valeur on peut savoir combien de temps le Timer0 mettra pour atteindre sa valeur max et déclencher l'interruption: 1/125.10e3*256 = 2ms. C'est bien en dessous de 17ms et bien au delà de la durée entre les fronts qu'on va recevoir, qui avec une Megadrive tournant a 7.6MHz, feront au minimum 7,6.10e6 / 4 cycles par instruction = 530ns (soit 0.5ms) ! Parfait.

On va avoir besoin de deux gestionnaires d'interruptions, d'abord pour le Pin Change:

ISR(PCINT0_vect) {
edge++; // Incrementer le compteur de fronts
TCNT0 = 0; // Remettre le Timer0 a zero
}

Et un second pour l'Overflow, ou on mettra le code servant a générer le signal de commande du collier. Pour avoir deux décharges de durées différentes, j'ai choisi de faire la différence entre deux nombres de fronts: entre 10 et 15 fronts pour une courte décharge, et 16 fronts ou plus pour une longue décharge.

ISR(TIMER0_OVF_vect) {
	uint8_t pulse,zap;
	if (edge >= 16) {                        // Si nombre de fronts >= 16: gros zap
      PORTB = _BV(LED);
      for (pulse=0;pulse<20;pulse++) {	     // 20 rafales de 10 pics
      	 for (zap=0;zap<10;zap++) {
      	   PORTB = _BV(SIG) | _BV(LED);      // SIG a 1
      	   _delay_us(300);
      	   PORTB = _BV(LED);                 // SIG a 0
      	   _delay_ms(4);
        }
        _delay_ms(50);
      }
      PORTB = 0;
   } else if ((edge < 16) && (edge > 10)) { // Si nombre de fronts > 10 et < 16: petit zap
      PORTB = _BV(LED);
      for (pulse=0;pulse<2;pulse++) {		 // 2 rafales de 10 pics
        for (zap=0;zap<10;zap++) {
          PORTB = _BV(SIG) | _BV(LED);      // SIG a 1
          _delay_us(300);
          PORTB = _BV(LED);                 // SIG a 0
          _delay_ms(4);
        }
        _delay_ms(50);
      }
      PORTB = 0;
   }
   edge = 0;	// Remet le compteur de fronts a zero
}

En basant la validation du signal sur l'Overflow du Timer0, on sait implicitement que la réaction au signal sera en retard de 2ms après la réception du dernier front, ce qui est complètement négligeable dans notre cas. Le reste du code n'est que le main() avec les initialisations nécessaires et un clignotement de la LED pour dire que tout se passe bien.
Se référer aux définitions bit par bit de la doc des Tiny25 pour plus de précisions.

PORTB = 0b00000000;            // Etre sur que toutes les sorties seront a zero
DDRB = _BV(SIG) | _BV(LED);    // SIG et LED comme sorties
TCCR0A = 0b00000000;			// Timer0 mode normal
TCCR0B = 0b00000011; // Prescaler Timer0 = /64
TIMSK = 0b00000010; // Interruption Overflow active GIMSK = 0b00100000; // Interruption Pin change active
PCMSK = _BV(SELECT); // Interruption Pin change sur SELECT seulement _delay_ms(100); for (blink=0;blink<10;blink++) {
PORTB = _BV(LED); // Faire clignoter la LED 10 fois
_delay_ms(10);
PORTB = 0;
_delay_ms(100);
} sei(); // Activer les interruptions

 

Le logiciel de debug

Vous conviendrez que de devoir lancer un jeu et perdre volontairement pour tester le bon fonctionnement du collier serait assez fastidieux, on va donc créer un petit ROM de debug de toute pièce. Premiere partie en assembleur 68000.

Même en assembleur c'est bouclé en 350 lignes de code, et cela permet de tester les deux sortes de décharges sur chacun des colliers très facilement grâce a un petit menu.
Le fonctionnement est simple, le logiciel affiche un menu avec 6 choix: décharge courte joueur 1, 2, les deux, décharge longue joueur 1, 2, les deux. Le choix est naturellement fait avec les boutons haut et bas, et le choix validé avec le bouton A.
Lorsqu'un choix est fait, un décompte se lance de 3 a 0 avant de balancer la bourre. Il est visible dans la source qu'il s'écoule exactement une seconde entre chaque étape du décompte (comptage de 50 images), si votre console est en mode PAL. Si elle est en NTSC, le décompte sera légèrement plus rapide.

Rien de particulier dans le code, le strict minimum pour afficher du texte: nettoyage de la RAM, de la VRAM, chargement de l'alphabet, de la palette, une routine pour écrire des null-terminated strings, et les routines magiques qui servent a donner des ordres au microcontroleur fraîchement installé dans les joypads.

Une telle routine ressemble a ceci (exemple pour une courte décharge pour le joueur 1):

zap1s:
     move.b #$40,$A10009     ; Configurer SELECT port 1 comme sortie
     move.l #6-1,d0          ; On veut 6 transition (12 fronts)
.z1s:
     move.b #$40,$A10003     ; SELECT a 5V
     nop
     move.b #$00,$A10003     ; SELECT a 0V
     dbra   d0,.z1s          ; Boucler
     rts

Pour ceux qui programment un peu, les commentaires devraient parler d'eux mêmes mais voici quand même le déroulement détaillé pour les allergiques a l'assembleur:

  • On met la valeur hexa $40 (01000000 en binaire, autrement dit bit 6 = 1) dans le registre $A10009. D'après la documentation Sega, ce registre sert a configurer les entrées/sorties du port joypad du joueur 1. En mettant $40, on indique que l'on veut que la broche correspondant au bit 6 soit une sortie (SELECT).
  • Ensuite, comme décrit dans la partie sur le firmware, on veut plus de 10 transitions sur SELECT mais moins de 16, j'ai choisi 12. On met donc 6 dans le registre D0 (-1 car il y aurait une itération de trop), pour faire un décompte dans une boucle.
  • On place un label "z1s" pour notre boucle, a l'intérieur on va mettre SELECT a 1 en plaçant $40 dans le registre $A10003 (état des sorties du port joypad 1).
  • On attend un peu en faisant rien grâce a l'instruction NOP pour être sur que le microcontroleur détecte bien le changement d'état.
  • On met ensuite SELECT a 0 en plaçant 0 dans le même registre.
  • Grâce a l'instruction dbra on décrémente D0 et on boucle au label "z1s" tant qu'on a pas atteint 0. Pas besoin d'un second NOP ici car l'instruction DBRA est déjà assez lente.
  • 6 fois 1 puis 0 donne bien 12 transitions sur SELECT et notre microcontroleur pourra le détecter.

Pour le second joueur, on utilisera simplement le même code mais cette fois avec les registres $A1000B et $A10005 au lieu de $A10009 et $A10003.
Pour la décharge longue, on voudra au moins 16 transition sur SELECT, donc on chargera D0 avec 8-1.

C'est exactement les mêmes routines qu'on utilisera pour patcher les jeux afin qu'ils puissent commander les colliers.

Le code source en assembleur pour ASW (v1). Le ROM en .bin (v1).

 

Patcher les jeux

C'est la partie qui risque d'en faire reculer plus d'un, car c'est la qu'on va devoir attaquer de l'assembleur 68000 qu'on a pas écrit nous même.

Le principe du patch ici est d'introduire du code a des endroits bien précis dans les jeux pour envoyer nos commandes aux colliers. Il faut donc d'abord trouver par ou le code passe lorsque l'événement qui nous intéresse se produit (perte de vie, prise de coup...), introduire un saut vers un espace vide du ROM, remplacer ce vide par notre code, et retourner au code du jeu. Il faut bien entendu que l'exécution de notre code ne perturbe pas le code du jeu.
A noter qu'il est impossible d'insérer du code a n'importe quel endroit car cela décalerait toutes les adresses suivantes et le code du jeu ne voudrait plus rien dire (le jeu freezerait tout simplement).

Comme ceux qui ne connaissent pas du tout la programmation ne vont certainement pas passer par cette case et utiliseront simplement mes patchs prets-a-jouer, je ne vais pas détailler comment coder en assembleur 68000 mais seulement comment s'y prendre dans le cas de Sonic, les autres jeux seront abordes plus rapidement. Ceux qui connaissent devraient s'y retrouver sans trop de peine.

Voici donc avant tout, les patchs Ninja tout prêts que j'ai réalises jusqu'a présent:

Bien sur si vous avez réalisé la modification des colliers et des joypads mais qu'aucun de ces jeux vous tente, n'hésitez pas a m'écrire pour me demander de rendre compatible votre jeu favori, je verrais ce que je pourrais faire ;)

Pour appliquer ces patches il vous faut le ROM original (que je vous laisse trouver sur le net, je ne les distribue pas), et l'utilitaire Ouinja qui apportera mes modifications au ROM. Afin d'éviter toute corruption et de crash, le patch ne s'appliquera pas si le ROM n'est pas exactement le même que j'ai utilise (verif CRC).

Dans cette partie, j'ai utilise IDA pour désassembler les jeux, l'emulateur Fusion pour trouver des valeurs en RAM, MESS pour la recherche plus approfondie et le debug, et finalement ASW encore pour assembler le code. On peut avoir recours a différentes techniques pour trouver ou patcher les jeux:

  • Le plus souvent j'ai fait appel au "cheat finder" de Fusion. C'est une fonction qu'on retrouve entre autres dans les Action Replay et qui permet de trouver l'adresse de variables en RAM selon leur valeur et leur évolution au cours du jeu.
  • Les codes GameGenie sont aussi très utiles pour trouver les bonnes adresses. Un tel code représente en fait une adresse et l'opcode d'une instruction a remplacer dans le jeu. Les GameGenie sont en fait des patcheurs en temps réel.
  • Une autre technique consiste a poser des watchpoints dans de grosses zones en RAM dans MESS pour affiner petit a petit l'adresse qu'on désire trouver.

Petit rappel ou résumé pour utiliser le debugger de MESS: Pour poser un breakpoint il suffit d'écrire bpset adresse, pour le virer, on met bpclear numéro. Pour les watchpoints on écrit wpset adresse,r/w,taille, pour les virer, wpclear numéro.
Par exemple pour watcher les écritures dans le word a $FF5A18, on écrira wpset ff5a18,w,2.

 

Patcher Sonic 1

Sonic a été l'un des jeux les plus simples a patcher. Tous les événements que je voulais hooker provoquaient des changements de valeurs qui étaient affichées a l'écran (nombre d'anneaux et de vies). Le seul petit souci a été la verif de checksum, bloquant le jeu si le ROM était modifie. Rien de bien complique a faire sauter cependant.

Petite décharge quand on perd tous ses anneaux (hit):

  • Cheat search en mode "count" avec Fusion sur la valeur des anneaux, donne rapidement un résultât unique a $FFFE21.
  • Watchpoint byte sur $FFFE21 avec MESS, skip des initialisations.
  • Quand on prend un coup, break sur le move.w #0 a $9D72, la variable anneaux est en fait a $FFFE20 (un word).
  • Recherche d'espace dispo avec Tile Layer, zone vide a $71370.
  • Remplacement du move.w par un jmp vers $71380 (pile 6 bytes).
  • A $71380, on remet le move.w qu'on vient d'effacer pour bien mettre les anneaux a 0, on pusher D0, on met le code pour la décharge courte, et on pop D0.
  • Retour dans le code avec un jmp a $9D78.

 

Test: Écran rouge au démarrage, donc verif checksum quelque part.

  • MESS montre qu'on est bien coincé dans une boucle infinie a $3D4.
  • Watchpoint sur la lecture de $200.w (la routine de checksum conseillée par Sega fait la somme des words a partir de $200)
  • Breaks sur le add.w (a0) a $32C, c'est dans la boucle d'addition. Un peu plus bas le cmp.w pour comparer avec le bon checksum et en dessous le bne en cas de différence.
  • Remplacement du bne par deux nops pour ignorer la différence (on pourrait aussi sauter la boucle d'addition pour gagner un peu de temps mais... la flemme). Le jeu se lance.

Grosse décharge quand on meurt:

  • Cheat search en mode count avec Fusion sur le nombre de vies, donne rapidement un résultât unique a $FFFE12.
  • Watchpoint sur $FFFE12 avec MESS, skip des initialisations.
  • Quand on meurt, break sur le subq.b a $138A0, la variable est bien un byte.
  • Il reste encore de la place a $71400, pas besoin d'en chercher ailleurs.
  • Le subq.b fait seulement 4 bytes donc remplacement du addq.b suivant également par un jmp vers $71400 et un nop (6+2 bytes) pour combler.
  • A $71400, on remet le addq et le subq, on push D0, on met le code pour une décharge longue, on pop D0
  • Retour dans le code avec un jmp a $138A4.
Patcher Battletoads

Battletoads a été un peu plus compliqué car on voulait recevoir une petite décharge quand on perdait de l'énergie (une barre de vie avec 6 cases), pas de valeur numérique donc il a fallu ouvrir un peu plus les yeux. J'ai commencé par faire le plus facile: hooker la perte de vie.

Grosse décharge quand on perd une vie:

  • Cheat search en mode count avec Fusion sur le nombre de vies en mode un joueur, donne un résultât unique a $FFE046.
  • Watchpoint sur $FFE046 avec MESS, on a une init a 5 quand on démarre une partie, ce qui correspond bien au nombre de vies de départ.
  • Quand on meurt, break sur le subq.b avec indexage sur A6 a $4360. A6 est donc l'index pour la structure du joueur ($FFE000 quand c'est le joueur 1 qui meurt, $FFE080 quand c'est le joueur 2). Les jeux multijoueurs ont typiquement le même code pour les deux joueurs mais appelé avec des index différents.
  • Recherche de place avec TLP: zone libre a $7FE90.
  • Remplacement du subq.b et du bpl $4378 suivant par un jmp vers $7FE90 et un nop (6+2 bytes).
  • A $7FE90, on doit faire la distinction entre le joueur 1 et joueur 2, donc on compare A6 avec $FFE000, si c'est égal, on colle une châtaigne au joueur 1, sinon au joueur 2.
  • En fin de routine on remet le subq qui sert a decrementer les vies (supprimé pour placer notre jmp), et le bpl vers $4378. Si le branch n'est pas pris, jmp vers $4366.

Test, le jeu se lance, pas de checksum visiblement (ou beaucoup de chance !).

Petite décharge quand on perd de l'énergie (plusieurs hits).

  • Les codes GameGenie pour l'invincibilité patchent 3 words a $A3D8 pour remplacer un btst qui sert de condition pour un saut si on est invincible (sprite clignotant). Pas vraiment d'utilité ici car on veut seulement détecter les hits qui decrementent l'énergie et pas -tous- les hits.
  • On sait ou sont les structures des données joueurs ($FFE000 pour le joueur 1), avec le memory viewer de MESS on remarque que la valeur a $FFE006 est modifiée quand on perd de l'énergie.
  • Watchpoint sur $FFE006 avec MESS, on a une init a $17 quand on commence une partie (soit 23 en décimal), on aurait donc 4 points de vie par case (?).
  • Break sur le move.w d1 a $3BE8 quand on perd de l'énergie, toujours avec indexage sur A6.
  • Il reste de la place a $7FF20. Remplacement du move.w et du move.b suivant par un jmp vers $7FF20 et un nop (6+2 bytes).
  • A $7FF20, push de D0, on remet le move.w et le move.b, et comme pour la grosse décharge, on compare A6 avec $FFE000 pour faire la distinction entre joueur 1 et joueur 2, pop D0 et retour jmp a $3BEE.
Patcher Mega Bomberman

Bomberman a été encore un peu plus complique car on avait pas de valeur en RAM a trouver, seulement des événements: exploser, prendre une maladie, et exploser son kangourou. En espérant que cela me faciliterait la tache, je suis d'abord allé voir si il y avait des codes GameGenie pour ce jeu. Il y a un "master code", c'est un code qu'on doit obligatoirement mettre avec ceux qu'on veut utiliser sinon le jeu ne fonctionne pas, c'est typiquement un code qui patch la verif de checksum, on va donc d'abord s'occuper de ca.

  • Le "master code" est CV7T-AN9T. Grâce a cette page sur Segakore, on peut le décrypter pour obtenir l'adresse et le word a remplacer. Le code veut dire: Remplacer le word a l'adresse $7BF0 par $6614.
  • Si on regarde le code du jeu dans IDA a cette adresse, on tombe pile sur la même routine de checksum qu'on a rencontre pour Sonic. A $7BF0, on trouve l'instruction BEQ, qui effectue un saut vers le démarrage du jeu seulement si le checksum est bon. Le code GameGenie remplace le $6714 que BEQ représente par $6614, qui veut dire BNE. On saute donc si le checksum n'est pas bon.
  • Dans un souci de simple logique, j'ai préféré remplacer le BEQ par un BRA tout simple, pour faire un saut sans condition (des fois qu'on tombe sur le même checksum même en patchant... Ca peut arriver). Je remplace donc le $6714 a $7BF0 par $6014 (BRA).

Le jeu se lance sans problème, le checksum ne nous dérangera plus.
Premiere étape, trouver par quelle adresse le jeu passe lorsqu'on ramasse une maladie. Pas de code GameGenie malheureusement.

  • Cheat search en mode state avec Fusion: on trouve plusieurs résultats mais en jouant aussi avec le deuxième joueur, on remarque que deux adresses qui se suivent restent: $FF9E97 pour le joueur 1 et $FF9E98 pour le joueur 2.
  • Retour sur le joueur 1 et watchpoint sur $FF9E97 avec MESS, init a zéro en début de partie.
  • Quand on attrape une maladie, break sur le move.b dans A0 a $6AB. Plus au dessus on remarque que A0 est charge avec $FF9E97 + D4. D4 est donc notre index pour le joueur (qui est a zéro pour le joueur 1, comme prévu). On remarque aussi que D4 est charge juste avant avec la valeur a $FFA93F.
  • Recherche de place avec TLP: zone libre a $FF930.
  • Afin d'éviter d'écraser deux instructions, je décide plutôt de remplacer le jsr $1CFA a $6AD8 (juste un peu plus haut dans la même routine). Remplacement donc avec un jmp vers $FF930 et un nop (6+2 bytes).
  • A $FF930, on doit encore faire la distinction entre le joueur 1 et joueur 2, comme D4 n'est pas encore charge, on lit directement l'index joueur a $FFA93F, qu'on compare avec 0 et 1 pour savoir a quel joueur en inflige la courte décharge.
  • En fin de routine on remet le jsr $1CFA et un retour au code avec un jmp vers $6ADE.

Deuxième étape, donner une grosse décharge quand on meurt. Ici aussi pas de chance, les codes GameGenie pour l'invincibilité étaient incorrects (du moins, ceux que j'ai trouves), ils patchaient des données graphiques... C'est reparti pour trouver avec Fusion.

  • Cheat search en mode state avec Fusion: entre le jeu normal et juste au moment ou le son de victoire retentit, on trouve sur plusieurs parties avec plusieurs paramètres différents l'adresse $FFA432 (mise a zéro quand on meurt avec le joueur 1).
  • Watchpoint sur $FF432 avec MESS, init a 1 en début de partie. Visiblement c'est un flag qui indique si on est mort ou pas.
  • Quelques frames après qu'on explose, break sur le clr (a4) a $317A. A4 est a $FFA432 pour le joueur 1 et $FFA472 pour le joueur 2.
  • Il reste de l'espace vide après notre précèdent patch, a $FF9C0.
  • Afin d'éviter d'écraser deux instructions, je décide de remplacer le move.b a $3174 (qui fait 6 bytes) par un jmp vers $FF9C0.
  • A $FF9C0, on doit encore faire la distinction entre le joueur 1 et joueur 2, on compare A4 avec $FFA432 et $FFA472 pour savoir a quel joueur en inflige la courte décharge.
  • En fin de routine on remet le move.b et un retour au code avec un jmp vers $317A.


Liste
creative commons
date
CC:BY,NC,SA - Engine V2.0
8ms
4488 *1
symbol symbol symbol symbol symbol