Guest User

saveedit.js

a guest
Oct 3rd, 2019
852
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. "use strict";
  2.  
  3. /*
  4.  * constants
  5.  */
  6.  
  7. // total size of FRAM
  8. const SIZE=512;
  9.  
  10. const S3_SAVE_NAME ="sonic3.srm";
  11. const S3K_SAVE_NAME="s3&k.srm";
  12. const MIME_TYPE="application/x-sonic3-save-file";
  13. const STORAGE_NAME="sonic3";
  14.  
  15. // single-player for Sonic 3
  16. const S3_SECTION_LENGTH=52;
  17. const S3_SLOT_LENGTH   =8;
  18. const S3_SLOTS =6;
  19. const S3_START1=0x0b4;
  20. const S3_START2=0x0fa;
  21.  
  22. // single-player for S3&K
  23. const S3K_SECTION_LENGTH=84; // also competition section length
  24. const S3K_SLOT_LENGTH   =10;
  25. const S3K_SLOTS =8;
  26. const S3K_START1=0x140;
  27. const S3K_START2=0x196;
  28.  
  29. // competition mode
  30. const RANKINGS=3; // number of competition rankings
  31. const CP_SLOT_LENGTH=4;
  32. const CP_START1=0x008;
  33. const CP_START2=0x05e;
  34.  
  35. // characters
  36. const SONIC_TAILS=0;
  37. const SONIC=1;
  38. const TAILS=2;
  39. const KNUCKLES=3;
  40. const NOBODY=-1;
  41.  
  42. const S3=false, S3K=true;
  43. const CLEAR=0x01, CHAOS_CLEAR=0x02, SUPER_CLEAR=0x03, NEW=0x80;
  44. const S3_LAST_ZONE=0x06;
  45. const SONIC_LAST_ZONE=0x0d, TAILS_LAST_ZONE=0x0c, KNUCKLES_LAST_ZONE=0x0b;
  46.  
  47. const defaults=[0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 1, 2, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 1, 2, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 1, 2, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 1, 2, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 1, 2, 0, 76, 68, 46, 90, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 1, 2, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 1, 2, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 1, 2, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 1, 2, 0, 128, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 1, 2, 0, 76, 68, 46, 90, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 66, 68, 229, 251, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 66, 68, 229, 251, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 66, 68, 112, 244, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 128, 0, 0, 0, 0, 0, 0, 0, 3, 0, 66, 68, 112, 244, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
  48.  
  49. /*
  50.  * initialization
  51.  */
  52.  
  53. window.addEventListener("load", function() {
  54.     const editor=new Editor(defaults);
  55.     const store=new Storage(STORAGE_NAME);
  56.  
  57.     let mem=store.load();
  58.     let save=new Save();
  59.  
  60.     if (mem) { // restores from local storage if set, otherwise uses defaults
  61.         save.loadFromArray(mem);
  62.     } else {
  63.         save.loadFromArray(defaults);
  64.     }
  65.  
  66.     editor.open(save);
  67.     editor.toggleAdvanced();
  68.  
  69.     window.addEventListener("beforeunload", function() {
  70.         store.save(editor.saveToStorage());
  71.     });
  72.  
  73.     // load
  74.     $("#file").addEventListener("change", function(event) {
  75.         let file=event.target.files[0];
  76.  
  77.         if (file) {
  78.             let reader=new FileReader();
  79.             reader.addEventListener("load", function(event) {
  80.                 let save=new Save();
  81.                 save.loadFromBuffer(event.target.result);
  82.  
  83.                 editor.open(save);
  84.             });
  85.             reader.readAsArrayBuffer(file);
  86.         }
  87.     });
  88.     $("#download").addEventListener("click", function() {
  89.         let filename="", blob=null;
  90.  
  91.         try {
  92.             [filename, blob]=editor.saveToFile();
  93.  
  94.             let a=$("#link");
  95.             a.download=filename;
  96.             a.href=window.URL.createObjectURL(blob);
  97.             a.click();
  98.             window.URL.revokeObjectURL(blob);
  99.         } catch (err) {
  100.             displayError(err);
  101.         }
  102.     });
  103.     $("#advanced").addEventListener("click", function() {
  104.         editor.toggleAdvanced();
  105.     });
  106.     // save
  107.     $("#reset").addEventListener("click", function() {
  108.         let save=new Save();
  109.         save.loadFromArray(defaults);
  110.         editor.open(save);
  111.     });
  112.  
  113.     for (let element of $$(".game")) {
  114.         element.addEventListener("click", function() {
  115.             editor.setGame(Number.parseInt(element.value));
  116.         });
  117.     }
  118.  
  119.     for (let element of $$(".write")) {
  120.         element.addEventListener("click", function() {
  121.             editor.setWrite(Number.parseInt(this.value), this.checked);
  122.             editor.saveSinglePlayer();
  123.         });
  124.     }
  125.  
  126.     for (let element of $$(".slot")) {
  127.         element.addEventListener("click", function() {
  128.             editor.setSlot(Number.parseInt(element.value));
  129.         });
  130.     }
  131.  
  132.     for (let element of $$(".stage")) {
  133.         element.addEventListener("click", function() {
  134.             editor.setStage(Number.parseInt(element.value));
  135.         });
  136.     }
  137.  
  138.     for (let element of $$(".ring")) {
  139.         element.addEventListener("click", function() {
  140.             this.classList.toggle("active");
  141.             editor.saveSinglePlayer();
  142.         });
  143.     }
  144.  
  145.     for (let element of $$(".arrow")) {
  146.         element.addEventListener("click", function() {
  147.             let id=this.parentNode.parentNode.parentNode.parentNode.id;
  148.             let select=$(`#${id} .zone`);
  149.  
  150.             let modifier=Number.parseInt(this.value);
  151.             let value=Number.parseInt(select.value)+modifier;
  152.  
  153.             if (value>=0&&value<select.length) {
  154.                 select.value=value;
  155.             }
  156.  
  157.             editor.saveSinglePlayer();
  158.         });
  159.     }
  160.  
  161.     for (let element of $$("#singlePlayer .character")) {
  162.         element.addEventListener("click", function() {
  163.             let id=this.parentNode.parentNode.parentNode.parentNode.id;
  164.             editor.selectActive(`#${id} .character`, this.value);
  165.             editor.saveSinglePlayer();
  166.         });
  167.     }
  168.  
  169.     for (let element of $$("#singlePlayer .new, #singlePlayer .clear")) {
  170.         element.addEventListener("click", function() { // click event
  171.             editor.saveSinglePlayer();
  172.         });
  173.     }
  174.  
  175.     for (let element of $$("#singlePlayer input[type=\"number\"], .zone")) {
  176.         element.addEventListener("input", function() { // input event
  177.             editor.saveSinglePlayer();
  178.         });
  179.     }
  180.  
  181.     for (let element of $$("#s3 .emerald")) {
  182.         element.addEventListener("click", function() {
  183.             let chaos=this.classList.contains("chaos");
  184.             this.classList.toggle("chaos", !chaos);
  185.             this.classList.toggle("empty",  chaos);
  186.  
  187.             editor.saveSinglePlayer();
  188.         });
  189.     }
  190.  
  191.     for (let element of $$("#s3k .emerald")) {
  192.         element.addEventListener("click", function() {
  193.             // rotates through emerald states
  194.             if (this.classList.contains("empty")) {
  195.                 this.classList.add("chaos");
  196.                 this.classList.remove("empty");
  197.             } else if (this.classList.contains("chaos")) {
  198.                 this.classList.add("palace");
  199.                 this.classList.remove("chaos");
  200.             } else if (this.classList.contains("palace")) {
  201.                 this.classList.add("super");
  202.                 this.classList.remove("palace");
  203.             } else if (this.classList.contains("super")) {
  204.                 this.classList.add("empty");
  205.                 this.classList.remove("super");
  206.             }
  207.  
  208.             editor.saveSinglePlayer();
  209.         });
  210.     }
  211.  
  212.     for (let element of $$("#competition .character")) {
  213.         element.addEventListener("click", function() {
  214.             let n=this.parentNode.parentNode.id.replace(/[^0-9]+/, "");
  215.             editor.selectActive(`#row${n} .character`, this.value);
  216.             editor.saveCompetition();
  217.         });
  218.     }
  219.  
  220.     for (let element of $$("#competition input")) { // checkboxes and numbers
  221.         element.addEventListener("input", function() {
  222.             editor.saveCompetition();
  223.         });
  224.     }
  225.  
  226.     for (let element of $$(".close")) {
  227.         element.addEventListener("click", function() {
  228.             for (let overlay of $$(".overlay")) {
  229.                 overlay.classList.remove("open");
  230.             }
  231.         });
  232.     }
  233.  
  234.     for (let element of $$(".size")) {
  235.         element.addEventListener("click", function() {
  236.             editor.toggleAdvanced();
  237.         });
  238.     }
  239.  
  240.     function displayError(message) {
  241.         $("#error").classList.add("open");
  242.         $("#error p").textContent=message;
  243.     }
  244. });
  245.  
  246. function $(selector) {
  247.     return document.querySelector(selector);
  248. }
  249.  
  250. function $$(selector) {
  251.     return Array.from(document.querySelectorAll(selector));
  252. }
  253.  
  254. /*
  255.  * Editor prototype
  256.  */
  257.  
  258. function Editor(defaults) {
  259.     this.save=null;
  260.     this.defaults=defaults;
  261.  
  262.     this.currentGame =S3K;
  263.     this.currentSlot =0;
  264.     this.currentStage=0;
  265.  
  266.     this.writeS3 =false;
  267.     this.writeS3K=true;
  268. }
  269.  
  270. Editor.prototype.open=function(save) {
  271.     this.save=save;
  272.  
  273.     if (this.save.singlePlayerS3==null) {
  274.         this.save.singlePlayerS3=this.defaults.slice(
  275.             S3_START1, S3_START1+S3_SECTION_LENGTH
  276.         );
  277.     }
  278.  
  279.     if (this.save.singlePlayerS3K==null) {
  280.         this.save.singlePlayerS3K=this.defaults.slice(
  281.             S3K_START1, S3K_START1+S3K_SECTION_LENGTH
  282.         );
  283.     }
  284.  
  285.     if (this.save.competition==null) {
  286.         this.save.competition=this.defaults.slice(
  287.             CP_START1, CP_START1+S3K_SECTION_LENGTH
  288.         );
  289.     }
  290.  
  291.     this.writeS3 =this.getChecksum(this.save.singlePlayerS3) !=0;
  292.     this.writeS3K=this.getChecksum(this.save.singlePlayerS3K)!=0;
  293.  
  294.     this.setWrite(S3,  this.writeS3);
  295.     this.setWrite(S3K, this.writeS3K);
  296.  
  297.     // sets game to Sonic 3 if no S3&K data available, otherwise sets to S3&K
  298.     this.setGame(this.writeS3K);
  299.  
  300.     this.setSlot();
  301.     this.setStage();
  302. };
  303.  
  304. Editor.prototype.getChecksum=function(section) {
  305.     return (section[section.length-2]<<8)|section[section.length-1];
  306. };
  307.  
  308. Editor.prototype.openDefaults=function(save) {
  309.     this.open(save);
  310.  
  311.     // Sonic 3 is disabled by default but will not be disabled automatically
  312.     // because default save file contains save data for both S3 and S3&K
  313.     this.setWrite(S3, false);
  314.     this.saveSinglePlayer();
  315. };
  316.  
  317. Editor.prototype.saveToFile=function() {
  318.     if (!this.writeS3&&!this.writeS3K) {
  319.         throw "Must enable Sonic 3 or Sonic 3 & Knuckles.";
  320.     }
  321.  
  322.     let file=null;
  323.  
  324.     // merges changes to file buffer
  325.     this.save.update(this.writeS3, this.writeS3K);
  326.  
  327.     if ($("#byte").checked) { // byte
  328.         file=this.save.file;
  329.     } else { // word
  330.         let fillerByte=$("#bff").checked?0xff:0;
  331.         let byteOrder=$("#little").checked;
  332.  
  333.         // using Uint8Array because the byte order of numbers saved in
  334.         // Uint16Array is architecture-dependent
  335.         file=new Uint8Array(SIZE*2);
  336.  
  337.         for (let i=0, n=0; i<file.length; i+=2, n++) {
  338.             if (byteOrder) { // little endian
  339.                 file[i]  =this.save.file[n];
  340.                 file[i+1]=fillerByte;
  341.             } else { // big endian
  342.                 file[i]  =fillerByte;
  343.                 file[i+1]=this.save.file[n];
  344.             }
  345.         }
  346.     }
  347.  
  348.     // uses upload file name if available
  349.     let filename=$("#file").value.replace(/.*(\\|\/)/, "")||"";
  350.  
  351.     if (!filename) {
  352.         filename=this.writeS3K?S3K_SAVE_NAME:S3_SAVE_NAME;
  353.     }
  354.  
  355.     return [filename, new Blob([file], {type: MIME_TYPE})];
  356. };
  357.  
  358. Editor.prototype.saveToStorage=function() {
  359.     this.save.update(this.writeS3, this.writeS3K);
  360.     return this.save.file;
  361. };
  362.  
  363. Editor.prototype.selectActive=function(selector, value) {
  364.     for (let element of $$(selector)) {
  365.         let condition=Number.parseInt(element.value)==value;
  366.         element.classList.toggle("active", condition);
  367.     }
  368. };
  369.  
  370. Editor.prototype.setGame=function(value) {
  371.     this.currentGame=value;
  372.  
  373.     $("#s3").classList.toggle("hidden", this.currentGame);
  374.     $("#s3k").classList.toggle("hidden", !this.currentGame);
  375.     this.selectActive(".game", value);
  376.  
  377.     let max=(this.currentGame?S3K_SLOTS:S3_SLOTS)-1;
  378.     this.setSlot(Math.min(this.currentSlot, max));
  379.  
  380.     for (let element of $$(".slot")) {
  381.         element.disabled=Number.parseInt(element.value)>max;
  382.     }
  383. };
  384.  
  385. Editor.prototype.setWrite=function(game, checked) {
  386.     if (game==S3) {
  387.         this.writeS3=checked;
  388.         $("#s3 .write").checked=checked;
  389.         this.toggleElements("#s3 button, #s3 tbody input", !checked);
  390.  
  391.         // loads default values if data is empty
  392.         if (this.getChecksum(this.save.singlePlayerS3)==0) {
  393.             this.save.singlePlayerS3=this.defaults.slice(
  394.                 S3_START1, S3_START1+S3_SECTION_LENGTH
  395.             );
  396.         }
  397.     } else {
  398.         this.writeS3K=checked;
  399.         $("#s3k .write").checked=checked;
  400.         this.toggleElements("#s3k button, #s3k tbody input", !checked);
  401.  
  402.         if (this.getChecksum(this.save.singlePlayerS3K)==0) {
  403.             this.save.singlePlayerS3K=this.defaults.slice(
  404.                 S3K_START1, S3K_START1+S3K_SECTION_LENGTH
  405.             );
  406.         }
  407.     }
  408. };
  409.  
  410. Editor.prototype.setSlot=function(value=0) {
  411.     this.currentSlot=value;
  412.     this.loadSinglePlayer();
  413.     this.selectActive(".slot", value);
  414. };
  415.  
  416. Editor.prototype.setStage=function(value=0) {
  417.     this.currentStage=value;
  418.     this.loadCompetition();
  419.     this.selectActive(".stage", value);
  420. };
  421.  
  422. Editor.prototype.loadSinglePlayer=function() {
  423.     let self=this;
  424.  
  425.     if (this.currentGame==S3) {
  426.         loadSinglePlayerS3();
  427.         loadTabs(this.save.getSlotCharactersS3(), S3_SLOTS, this.writeS3);
  428.     } else {
  429.         loadSinglePlayerS3K();
  430.         loadTabs(this.save.getSlotCharactersS3K(), S3K_SLOTS, this.writeS3K);
  431.     }
  432.  
  433.     function loadSinglePlayerS3() {
  434.         let slot=self.save.getSaveSlotS3(self.currentSlot);
  435.  
  436.         if (self.writeS3) {
  437.             self.toggleElements(
  438.                 "#s3 button, #s3 input[type=\"number\"]",
  439.                 slot.isNew
  440.             );
  441.         }
  442.  
  443.         $("#s3 .new").checked  =slot.isNew;
  444.         $("#s3 .clear").checked=slot.isClear;
  445.         $("#last").value       =slot.last;
  446.  
  447.         self.selectActive("#s3 .character", slot.character);
  448.         selectRings("#s3 .ring", slot.rings);
  449.         selectZone("s3", slot.zone);
  450.  
  451.         for (let element of $$("#s3 .emerald")) {
  452.             let chaos=Number.parseInt(element.value)&slot.emeralds;
  453.  
  454.             element.classList.toggle("empty", !chaos);
  455.             element.classList.toggle("chaos",  chaos);
  456.         }
  457.  
  458.         let image="";
  459.  
  460.         if (self.writeS3) {
  461.             if (slot.isNew) {
  462.                 image="new";
  463.             } else {
  464.                 if (slot.isClear) {
  465.                     // Sonic 3 shows Sonic picture regardless of character
  466.                     if (slot.numEmeralds>=7) {
  467.                         image="clear-sonic-chaos";
  468.                     } else {
  469.                         image="clear-sonic";
  470.                     }
  471.                 } else {
  472.                     image="zone-"+slot.zone.toString().padStart(2, "0");
  473.                 }
  474.             }
  475.         } else {
  476.             image="static";
  477.         }
  478.  
  479.         $("#s3 .preview").src="images/"+image+".png";
  480.     }
  481.  
  482.     function loadSinglePlayerS3K() {
  483.         let slot=self.save.getSaveSlotS3K(self.currentSlot);
  484.  
  485.         if (self.writeS3K) {
  486.             self.toggleElements(
  487.                 "#s3k button, #s3k input[type=\"number\"]",
  488.                 slot.isNew
  489.             );
  490.         }
  491.  
  492.         $("#s3k .new").checked  =slot.isNew;
  493.         $("#s3k .clear").checked=slot.isClear;
  494.         $("#lives").value       =slot.lives;
  495.         $("#continues").value   =slot.continues;
  496.  
  497.         self.selectActive("#s3k .character", slot.character);
  498.         selectRings("#s3k .ring", slot.rings);
  499.         selectZone("s3k", slot.zone);
  500.  
  501.         let numEmeralds=0;
  502.  
  503.         for (let element of $$("#s3k .emerald")) {
  504.             let offset=Number.parseInt(element.value);
  505.             let emeralds=slot.emeralds1;
  506.  
  507.             if (element.classList.contains("grey")||
  508.                 element.classList.contains("red")||
  509.                 element.classList.contains("blue")
  510.             ) {
  511.                 emeralds=slot.emeralds2;
  512.             }
  513.  
  514.             let chaos =emeralds&(1<<offset);
  515.             let palace=emeralds&(1<<offset+1);
  516.  
  517.             numEmeralds+=Number(chaos||palace);
  518.  
  519.             element.classList.toggle("empty",  !chaos&&!palace);
  520.             element.classList.toggle("chaos",   chaos&&!palace);
  521.             element.classList.toggle("palace", !chaos&& palace);
  522.             element.classList.toggle("super",   chaos&& palace);
  523.         }
  524.  
  525.         let select=$("#s3k .zone");
  526.  
  527.         for (let element of $$("#s3k .zone option")) {
  528.             let value=Number.parseInt(element.value);
  529.  
  530.             if (slot.character==TAILS) {
  531.                 element.disabled=value>TAILS_LAST_ZONE;
  532.                 clampZone(slot, select, TAILS_LAST_ZONE);
  533.             } else if (slot.character==KNUCKLES) {
  534.                 element.disabled=value>KNUCKLES_LAST_ZONE;
  535.                 clampZone(slot, select, KNUCKLES_LAST_ZONE);
  536.             } else {
  537.                 // at least 7 chaos or super emeralds collected as Sonic
  538.                 if (numEmeralds>=7) {
  539.                     element.disabled=false;
  540.                     // value can be higher for cleared games
  541.                     clampZone(slot, select, SONIC_LAST_ZONE);
  542.                 } else {
  543.                     let lastZone=SONIC_LAST_ZONE-1;
  544.                     element.disabled=value>lastZone;
  545.                     clampZone(slot, select, lastZone);
  546.                 }
  547.             }
  548.         }
  549.  
  550.         let image="";
  551.  
  552.         if (self.writeS3K) {
  553.             if (slot.isNew) {
  554.                 image="new";
  555.             } else {
  556.                 if (slot.isClear) { // clear
  557.                     if (slot.isClear==SUPER_CLEAR) { // all super emeralds
  558.                         image="clear-super";
  559.                     } else {
  560.                         if (slot.isClear==CHAOS_CLEAR) { // all chaos emeralds
  561.                             switch (slot.character) {
  562.                                 case TAILS:
  563.                                     image="clear-tails-chaos";
  564.                                     break;
  565.                                 case KNUCKLES:
  566.                                     image="clear-knuckles-chaos";
  567.                                     break;
  568.                                 default:
  569.                                     image="clear-sonic-chaos";
  570.                             }
  571.                         } else {
  572.                             switch (slot.character) {
  573.                                 case TAILS:
  574.                                     image="clear-tails";
  575.                                     break;
  576.                                 case KNUCKLES:
  577.                                     image="clear-knuckles";
  578.                                     break;
  579.                                 default:
  580.                                     image="clear-sonic";
  581.                             }
  582.                         }
  583.                     }
  584.                 } else {
  585.                     image="zone-"+slot.zone.toString().padStart(2, "0");
  586.                 }
  587.             }
  588.         } else {
  589.             image="static";
  590.         }
  591.  
  592.         $("#s3k .preview").src="images/"+image+".png";
  593.     }
  594.  
  595.     function loadTabs(characters, max, enabled) {
  596.         for (let [i, element] of $$(".slot").entries()) {
  597.             if (i>=max) {
  598.                 break;
  599.             }
  600.  
  601.             if (enabled) {
  602.                 let character=characters[i];
  603.  
  604.                 element.classList.toggle("sonic", character==SONIC);
  605.                 element.classList.toggle("tails", character==TAILS);
  606.                 element.classList.toggle("sonictails", character==SONIC_TAILS);
  607.                 element.classList.toggle("knuckles",   character==KNUCKLES);
  608.             } else {
  609.                 element.classList.remove(
  610.                     "sonic", "tails", "sonictails", "knuckles"
  611.                 );
  612.             }
  613.         }
  614.     }
  615.  
  616.     function selectRings(selector, rings) {
  617.         for (let element of $$(selector)) {
  618.             let condition=rings&(1<<Number.parseInt(element.value));
  619.             element.classList.toggle("active", condition);
  620.         }
  621.     }
  622.  
  623.     function clampZone(slot, select, lastZone) {
  624.         if (slot.zone>lastZone) {
  625.             select.value=lastZone;
  626.             slot.zone=lastZone;
  627.         }
  628.     }
  629.  
  630.     function selectZone(id, zone) {
  631.         let select=$(`#${id} .zone`);
  632.         select.value=zone;
  633.  
  634.         let clear=$(`#${id} .new`).checked||$(`#${id} .clear`).checked;
  635.         $(`#${id} .zone`).disabled=clear;
  636.         $(`#${id} .prev`).disabled=clear||select.value==0;
  637.         $(`#${id} .next`).disabled=clear||select.value==select.length-1;
  638.     }
  639. };
  640.  
  641. Editor.prototype.saveSinglePlayer=function() {
  642.     let self=this;
  643.  
  644.     if (this.currentGame==S3) {
  645.         saveSinglePlayerS3();
  646.     } else {
  647.         saveSinglePlayerS3K();
  648.     }
  649.  
  650.     function saveSinglePlayerS3() {
  651.         let emeralds=0, numEmeralds=0;
  652.  
  653.         for (let element of $$("#s3 .emerald")) {
  654.             if (element.classList.contains("chaos")) {
  655.                 emeralds+=Number.parseInt(element.value);
  656.                 numEmeralds++;
  657.             }
  658.         }
  659.  
  660.         // must check if null before attempting to use value
  661.         let character=$("#s3 .character.active");
  662.         character=character?Number.parseInt(character.value):0;
  663.  
  664.         self.save.setSaveSlotS3(self.currentSlot, {
  665.             isNew:       $("#s3 .new").checked,
  666.             isClear:     $("#s3 .clear").checked,
  667.             character:   character,
  668.             zone:        Number.parseInt($("#s3 .zone").value)||0,
  669.             last:        fillNumber("#last"),
  670.             numEmeralds: numEmeralds,
  671.             emeralds:    emeralds,
  672.             rings:       fillRings("#s3 .ring")
  673.         });
  674.         self.loadSinglePlayer();
  675.     }
  676.  
  677.     function saveSinglePlayerS3K() {
  678.         let emeralds1=0, emeralds2=0, chaosEmeralds=0, superEmeralds=0;
  679.  
  680.         for (let element of $$("#s3k .emerald")) {
  681.             let offset=Number.parseInt(element.value);
  682.             let chaos =element.classList.contains("chaos");
  683.             let palace=element.classList.contains("palace");
  684.  
  685.             if (element.classList.contains("super")) {
  686.                 chaos =true;
  687.                 palace=true;
  688.             }
  689.  
  690.             if (element.classList.contains("grey")||
  691.                 element.classList.contains("red")||
  692.                 element.classList.contains("blue")
  693.             ) {
  694.                 if (chaos||palace) {
  695.                     if (chaos) {
  696.                         emeralds2|=1<<offset;
  697.                     }
  698.  
  699.                     if (palace) {
  700.                         emeralds2|=1<<offset+1;
  701.                     }
  702.  
  703.                     chaosEmeralds++;
  704.                     superEmeralds+=Number(chaos&&palace);
  705.                 }
  706.             } else {
  707.                 if (chaos||palace) {
  708.                     if (chaos) {
  709.                         emeralds1|=1<<offset;
  710.                     }
  711.  
  712.                     if (palace) {
  713.                         emeralds1|=1<<offset+1;
  714.                     }
  715.  
  716.                     chaosEmeralds++;
  717.                     superEmeralds+=Number(chaos&&palace);
  718.                 }
  719.             }
  720.         }
  721.  
  722.         let clear=0;
  723.  
  724.         if ($("#s3k .clear").checked) {
  725.             if (superEmeralds>=7) {
  726.                 clear=SUPER_CLEAR;
  727.             } else if (chaosEmeralds>=7) {
  728.                 clear=CHAOS_CLEAR;
  729.             } else {
  730.                 clear=CLEAR;
  731.             }
  732.         }
  733.  
  734.         let character=$("#s3k .character.active");
  735.         character=character?Number.parseInt(character.value):0;
  736.  
  737.         self.save.setSaveSlotS3K(self.currentSlot, {
  738.             isNew:       $("#s3k .new").checked,
  739.             isClear:     clear,
  740.             character:   character,
  741.             numEmeralds: chaosEmeralds,
  742.             zone:        Number.parseInt($("#s3k .zone").value)||0,
  743.             rings:       fillRings("#s3k .ring"),
  744.             emeralds1:   emeralds1,
  745.             emeralds2:   emeralds2,
  746.             lives:       fillNumber("#lives"),
  747.             continues:   fillNumber("#continues")
  748.         });
  749.         self.loadSinglePlayer();
  750.     }
  751.  
  752.     function fillNumber(selector) {
  753.         let element=$(selector);
  754.         let value=Number.parseInt(element.value);
  755.         value=Math.min(element.max, value);
  756.         value=Math.max(element.min, value);
  757.  
  758.         return value;
  759.     }
  760.  
  761.     function fillRings(selector) {
  762.         let rings=0;
  763.  
  764.         for (let element of $$(selector)) {
  765.             if (element.classList.contains("active")) {
  766.                 rings|=1<<Number.parseInt(element.value);
  767.             }
  768.         }
  769.  
  770.         return rings;
  771.     }
  772. };
  773.  
  774. Editor.prototype.loadCompetition=function() {
  775.     let rows=this.save.getStage(this.currentStage);
  776.  
  777.     for (let [i, row] of rows.entries()) {
  778.         $(`#row${i} .new`).checked=row.isNew;
  779.         $(`#row${i} .min`).value  =row.min;
  780.         $(`#row${i} .sec`).value  =row.sec;
  781.         $(`#row${i} .msec`).value =row.msec;
  782.  
  783.         this.toggleElements(
  784.             `#row${i} button, #row${i} input[type="number"]`,
  785.             row.isNew
  786.         );
  787.         this.selectActive(`#row${i} .character`, row.character);
  788.     }
  789. };
  790.  
  791. Editor.prototype.saveCompetition=function() {
  792.     let rows=Array(RANKINGS).fill().map(function() {
  793.         return {};
  794.     });
  795.  
  796.     rows=fillBoolean("#competition .new", "isNew");
  797.     rows=fillNumber("#competition .min",  "min");
  798.     rows=fillNumber("#competition .sec",  "sec");
  799.     rows=fillNumber("#competition .msec", "msec");
  800.     rows=fillButtons("#competition .character", "character");
  801.  
  802.     this.save.setStage(this.currentStage, rows);
  803.     this.loadCompetition();
  804.  
  805.     function fillBoolean(selector, key) {
  806.         for (let [i, element] of $$(selector).entries()) {
  807.             rows[i][key]=element.checked;
  808.         }
  809.  
  810.         return rows;
  811.     }
  812.  
  813.     function fillNumber(selector, key) {
  814.         for (let [i, element] of $$(selector).entries()) {
  815.             let value=Number.parseInt(element.value);
  816.             value=Math.min(element.max, value);
  817.             value=Math.max(element.min, value);
  818.  
  819.             rows[i][key]=value;
  820.         }
  821.  
  822.         return rows;
  823.     }
  824.  
  825.     function fillButtons(selector, key) {
  826.         let n=0;
  827.  
  828.         for (let element of $$(selector)) {
  829.             if (element.classList.contains("active")) {
  830.                 rows[n][key]=Number.parseInt(element.value);
  831.                 n++; // only increments once per table row
  832.             }
  833.         }
  834.  
  835.         return rows;
  836.     }
  837. };
  838.  
  839. Editor.prototype.toggleAdvanced=function() {
  840.     let advanced=$("#advanced").checked;
  841.  
  842.     for (let element of $$(".advanced")) {
  843.         element.classList.toggle("hidden", !advanced);
  844.     }
  845.  
  846.     let order=$("#byte").checked;
  847.  
  848.     for (let element of $$(".filler, .order")) {
  849.         element.disabled=order;
  850.     }
  851. };
  852.  
  853. Editor.prototype.toggleElements=function(selector, condition) {
  854.     for (let element of $$(selector)) {
  855.         element.disabled=condition;
  856.     }
  857. };
  858.  
  859. /*
  860.  * Save prototype
  861.  */
  862.  
  863. function Save() {
  864.     this.file=null;
  865.  
  866.     this.singlePlayerS3 =[];
  867.     this.singlePlayerS3K=[];
  868.     this.competition    =[];
  869. }
  870.  
  871. Save.prototype.loadFromArray=function(arr) {
  872.     this.file=Uint8Array.from(arr);
  873.     this.parse();
  874. };
  875.  
  876. Save.prototype.loadFromBuffer=function(buffer) {
  877.     this.file=new Uint8Array(buffer);
  878.  
  879.     if (buffer.byteLength==SIZE*2) {
  880.         this.file=this.file.filter(function(undefined, i) {
  881.             return i%2!=0; // skips odd bytes
  882.         });
  883.     }
  884.  
  885.     this.parse();
  886. };
  887.  
  888. Save.prototype.parse=function() {
  889.     let self=this;
  890.  
  891.     this.singlePlayerS3=checkChecksums(
  892.         this.file.slice(S3_START1, S3_START1+S3_SECTION_LENGTH),
  893.         this.file.slice(S3_START2, S3_START2+S3_SECTION_LENGTH)
  894.     );
  895.     this.singlePlayerS3K=checkChecksums(
  896.         this.file.slice(S3K_START1, S3K_START1+S3K_SECTION_LENGTH),
  897.         this.file.slice(S3K_START2, S3K_START2+S3K_SECTION_LENGTH)
  898.     );
  899.     this.competition=checkChecksums(
  900.         this.file.slice(CP_START1, CP_START1+S3K_SECTION_LENGTH),
  901.         this.file.slice(CP_START2, CP_START2+S3K_SECTION_LENGTH)
  902.     );
  903.  
  904.     // all data is duplicated in the save file for integrity;
  905.     //  uses first set if checksum passes,
  906.     //  otherwise uses second set if checksum passes,
  907.     //  otherwise returns null
  908.     function checkChecksums(arr1, arr2) {
  909.         let result=[];
  910.  
  911.         if (self.verifyChecksum(arr1)) {
  912.             result=arr1;
  913.         } else {
  914.             if (self.verifyChecksum(arr2)) {
  915.                 result=arr2;
  916.             } else {
  917.                 result=null;
  918.             }
  919.         }
  920.  
  921.         return result;
  922.     }
  923. };
  924.  
  925. Save.prototype.update=function(writeS3=true, writeS3K=true) {
  926.     this.singlePlayerS3 =this.updateChecksum(this.singlePlayerS3);
  927.     this.singlePlayerS3K=this.updateChecksum(this.singlePlayerS3K);
  928.     this.competition    =this.updateChecksum(this.competition);
  929.  
  930.     let self=this;
  931.  
  932.     this.file=mergeSection(S3_START1,  this.singlePlayerS3,  writeS3);
  933.     this.file=mergeSection(S3_START2,  this.singlePlayerS3,  writeS3);
  934.     this.file=mergeSection(S3K_START1, this.singlePlayerS3K, writeS3K);
  935.     this.file=mergeSection(S3K_START2, this.singlePlayerS3K, writeS3K);
  936.     this.file=mergeSection(CP_START1,  this.competition);
  937.     this.file=mergeSection(CP_START2,  this.competition);
  938.  
  939.     function mergeSection(start, source, write=true) {
  940.         let stop=start+source.length;
  941.  
  942.         for (let i=start, n=0; i<stop; i++, n++) {
  943.             self.file[i]=write?source[n]:0;
  944.         }
  945.  
  946.         return self.file;
  947.     }
  948. };
  949.  
  950. Save.prototype.calcChecksum=function(bytes) {
  951.     const BIT_MASK=0x8810;
  952.     let checksum=0, carry=0;
  953.  
  954.     for (let i=0; i<bytes.length-2; i+=2) {
  955.         checksum^=(bytes[i]<<8)|bytes[i+1];
  956.         carry=checksum&1; // gets least significant bit before shift
  957.         checksum>>>=1;
  958.  
  959.         if (carry) {
  960.             checksum^=BIT_MASK;
  961.         }
  962.     }
  963.  
  964.     return checksum;
  965. };
  966.  
  967. Save.prototype.verifyChecksum=function(bytes) {
  968.     // saves original checksum
  969.     let original=(bytes[bytes.length-2]<<8)|bytes[bytes.length-1];
  970.     let checksum=this.calcChecksum(bytes);
  971.  
  972.     return original==checksum;
  973. };
  974.  
  975. Save.prototype.updateChecksum=function(bytes) {
  976.     let checksum=this.calcChecksum(bytes);
  977.  
  978.     // writes new checksum to last two bytes of data
  979.     bytes[bytes.length-1]= checksum&0x00ff;
  980.     bytes[bytes.length-2]=(checksum&0xff00)>>8;
  981.  
  982.     return bytes;
  983. };
  984.  
  985. Save.prototype.getSaveSlotS3=function(current) {
  986.     let pos=current*S3_SLOT_LENGTH;
  987.     let zone=this.singlePlayerS3[pos+3];
  988.  
  989.     // adjusts zones after Flying Battery to match S3&K
  990.     if (zone>4&&zone<=S3_LAST_ZONE) {
  991.         zone--;
  992.     }
  993.  
  994.     return {
  995.         isNew:       this.singlePlayerS3[pos]==NEW,
  996.         isClear:     zone>S3_LAST_ZONE,
  997.         character:   this.singlePlayerS3[pos+2],
  998.         zone:        zone,
  999.         last:        this.singlePlayerS3[pos+4],
  1000.         numEmeralds: this.singlePlayerS3[pos+5],
  1001.         emeralds:    this.singlePlayerS3[pos+6],
  1002.         rings:       this.singlePlayerS3[pos+7]
  1003.     };
  1004. };
  1005.  
  1006. Save.prototype.getSaveSlotS3K=function(current) {
  1007.     let pos=current*S3K_SLOT_LENGTH;
  1008.     let clear=this.singlePlayerS3K[pos];
  1009.  
  1010.     if (clear>SUPER_CLEAR) {
  1011.         clear=0;
  1012.     }
  1013.  
  1014.     return {
  1015.         isNew:       this.singlePlayerS3K[pos]==NEW,
  1016.         isClear:     clear,
  1017.         character:   (this.singlePlayerS3K[pos+2]&0xf0)>>4,
  1018.         numEmeralds: this.singlePlayerS3K[pos+2]&0x0f,
  1019.         zone:        this.singlePlayerS3K[pos+3],
  1020.         rings:       this.singlePlayerS3K[pos+4],
  1021.         emeralds1:   this.singlePlayerS3K[pos+6],
  1022.         emeralds2:   this.singlePlayerS3K[pos+7],
  1023.         lives:       this.singlePlayerS3K[pos+8],
  1024.         continues:   this.singlePlayerS3K[pos+9]
  1025.     };
  1026. };
  1027.  
  1028. Save.prototype.getSlotCharactersS3=function() {
  1029.     let characters=[];
  1030.  
  1031.     for (let i=0; i<S3_SLOTS; i++) {
  1032.         let pos=i*S3_SLOT_LENGTH;
  1033.  
  1034.         let isNew    =this.singlePlayerS3[pos]==NEW;
  1035.         let character=this.singlePlayerS3[pos+2];
  1036.  
  1037.         characters.push(isNew?NOBODY:character);
  1038.     }
  1039.  
  1040.     return characters;
  1041. };
  1042.  
  1043. Save.prototype.getSlotCharactersS3K=function() {
  1044.     let characters=[];
  1045.  
  1046.     for (let i=0; i<S3K_SLOTS; i++) {
  1047.         let pos=i*S3K_SLOT_LENGTH;
  1048.  
  1049.         let isNew    =this.singlePlayerS3K[pos]==NEW;
  1050.         let character=(this.singlePlayerS3K[pos+2]&0xf0)>>4;
  1051.  
  1052.         characters.push(isNew?NOBODY:character);
  1053.     }
  1054.  
  1055.     return characters;
  1056. };
  1057.  
  1058. Save.prototype.setSaveSlotS3=function(current, slot) {
  1059.     let pos=current*S3_SLOT_LENGTH;
  1060.  
  1061.     if (slot.isNew) {
  1062.         this.singlePlayerS3[pos]  =NEW;
  1063.         this.singlePlayerS3[pos+1]=0;
  1064.         this.singlePlayerS3[pos+2]=0;
  1065.         this.singlePlayerS3[pos+3]=0;
  1066.         this.singlePlayerS3[pos+4]=0;
  1067.         this.singlePlayerS3[pos+5]=0;
  1068.         this.singlePlayerS3[pos+6]=0;
  1069.         this.singlePlayerS3[pos+7]=0;
  1070.     } else {
  1071.         let zone=slot.zone;
  1072.  
  1073.         if (slot.isClear) {
  1074.             zone=S3_LAST_ZONE+1;
  1075.         } else {
  1076.             // adjust from S3&K numbering
  1077.             if (zone>=4) {
  1078.                 zone++;
  1079.             }
  1080.         }
  1081.  
  1082.         this.singlePlayerS3[pos]  =0;
  1083.         this.singlePlayerS3[pos+1]=0; // always zero
  1084.         this.singlePlayerS3[pos+2]=slot.character;
  1085.         this.singlePlayerS3[pos+3]=zone;
  1086.         this.singlePlayerS3[pos+4]=slot.last;
  1087.         this.singlePlayerS3[pos+5]=slot.numEmeralds;
  1088.         this.singlePlayerS3[pos+6]=slot.emeralds;
  1089.         this.singlePlayerS3[pos+7]=slot.rings;
  1090.     }
  1091. };
  1092.  
  1093. Save.prototype.setSaveSlotS3K=function(current, slot) {
  1094.     let pos=current*S3K_SLOT_LENGTH;
  1095.  
  1096.     if (slot.isNew) {
  1097.         this.singlePlayerS3K[pos]  =NEW;
  1098.         this.singlePlayerS3K[pos+1]=0;
  1099.         this.singlePlayerS3K[pos+2]=0;
  1100.         this.singlePlayerS3K[pos+3]=0;
  1101.         this.singlePlayerS3K[pos+4]=0;
  1102.         this.singlePlayerS3K[pos+5]=0;
  1103.         this.singlePlayerS3K[pos+6]=0;
  1104.         this.singlePlayerS3K[pos+7]=0;
  1105.         this.singlePlayerS3K[pos+8]=0;
  1106.         this.singlePlayerS3K[pos+9]=0;
  1107.     } else {
  1108.         let numEmeralds=slot.numEmeralds;
  1109.  
  1110.         if (numEmeralds>=7) { // goes back to 0 when all emeralds collected
  1111.             numEmeralds=0;
  1112.         }
  1113.  
  1114.         let zone=slot.zone;
  1115.  
  1116.         if (slot.isClear) {
  1117.             switch (slot.character) {
  1118.                 case TAILS:
  1119.                     zone=TAILS_LAST_ZONE;
  1120.                     break;
  1121.                 case KNUCKLES:
  1122.                     zone=KNUCKLES_LAST_ZONE;
  1123.                     break;
  1124.                 default:
  1125.                     if (slot.isClear==CHAOS_CLEAR||slot.isClear==SUPER_CLEAR) {
  1126.                         zone=SONIC_LAST_ZONE; // Doomsday
  1127.                     } else {
  1128.                         zone=SONIC_LAST_ZONE-1; // Death Egg
  1129.                     }
  1130.             }
  1131.  
  1132.             zone++;
  1133.         }
  1134.  
  1135.         this.singlePlayerS3K[pos]  =slot.isClear;
  1136.         this.singlePlayerS3K[pos+1]=0; // always zero
  1137.         this.singlePlayerS3K[pos+2]=slot.character<<4|numEmeralds;
  1138.         this.singlePlayerS3K[pos+3]=zone;
  1139.         this.singlePlayerS3K[pos+4]=slot.rings;
  1140.         this.singlePlayerS3K[pos+5]=0; // always zero
  1141.         this.singlePlayerS3K[pos+6]=slot.emeralds1;
  1142.         this.singlePlayerS3K[pos+7]=slot.emeralds2;
  1143.         this.singlePlayerS3K[pos+8]=slot.lives;
  1144.         this.singlePlayerS3K[pos+9]=slot.continues;
  1145.     }
  1146. };
  1147.  
  1148. Save.prototype.getStage=function(current) {
  1149.     let start=current*CP_SLOT_LENGTH*(RANKINGS+1);
  1150.     let rows=[];
  1151.  
  1152.     for (let i=0; i<RANKINGS; i++) {
  1153.         let pos=start+i*CP_SLOT_LENGTH;
  1154.  
  1155.         rows.push({
  1156.             isNew: this.competition[pos]==NEW,
  1157.             min:   this.competition[pos+1],
  1158.             sec:   this.competition[pos+2],
  1159.             msec:  this.competition[pos+3],
  1160.             character: this.competition[start+CP_SLOT_LENGTH*RANKINGS+i]
  1161.         });
  1162.     }
  1163.  
  1164.     return rows;
  1165. };
  1166.  
  1167. Save.prototype.setStage=function(current, rows) {
  1168.     let pos=current*(CP_SLOT_LENGTH*(RANKINGS+1));
  1169.     let characters=pos+CP_SLOT_LENGTH*RANKINGS; // start of characters slot
  1170.  
  1171.     for (let [i, row] of rows.entries()) {
  1172.         if (row.isNew) {
  1173.             this.competition[pos]  =NEW;
  1174.             this.competition[pos+1]=0;
  1175.             this.competition[pos+2]=0;
  1176.             this.competition[pos+3]=0;
  1177.             this.competition[pos+CP_SLOT_LENGTH*RANKINGS+i]=0;
  1178.         } else {
  1179.             this.competition[pos]  =0;
  1180.             this.competition[pos+1]=row.min;
  1181.             this.competition[pos+2]=row.sec;
  1182.             this.competition[pos+3]=row.msec;
  1183.             this.competition[characters+i]=row.character;
  1184.         }
  1185.  
  1186.         pos+=4;
  1187.     }
  1188. };
  1189.  
  1190. /*
  1191.  * Storage prototype
  1192.  */
  1193.  
  1194. function Storage(name) {
  1195.     this.name=name;
  1196. }
  1197.  
  1198. Storage.prototype.load=function() {
  1199.     try {
  1200.         let contents=localStorage.getItem(this.name);
  1201.  
  1202.         if (contents!=null) {
  1203.  
  1204.             return JSON.parse(contents);
  1205.         }
  1206.     } catch (err) {
  1207.         console.error(err);
  1208.     }
  1209. };
  1210.  
  1211. Storage.prototype.save=function(file) {
  1212.     try {
  1213.         if (file.length>0) {
  1214.             localStorage.setItem(this.name, JSON.stringify(Array.from(file)));
  1215.         } else {
  1216.             this.reset();
  1217.         }
  1218.     } catch (err) {
  1219.         console.error(err);
  1220.     }
  1221. };
  1222.  
  1223. Storage.prototype.reset=function() {
  1224.     try {
  1225.         localStorage.removeItem(this.name);
  1226.     } catch (err) {
  1227.         console.error(err);
  1228.     }
  1229. };
Advertisement
Add Comment
Please, Sign In to add comment