-
-
Save Equim-chan/875a232a2c1d31181df8b3a8704c3112 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name downloadlogs | |
// @namespace mjg | |
// @icon https://cdn.myanimelist.net/images/characters/12/75583.jpg | |
// @version 0.1.1 | |
// @description sex with akochan! | |
// @match https://mahjongsoul.game.yo-star.com/* | |
// @match https://game.mahjongsoul.com/* | |
// @match https://majsoul.union-game.com/0/* | |
// @match https://game.maj-soul.com/1/* | |
// @match https://game.maj-soul.net/1/* | |
// ==/UserScript== | |
(function() | |
{ //variables you might actually want to change | |
const KEY = 83; //key we listen for; "s" is 83 - https://keycode.info/ | |
const NAMEPREF = 1; //2 for english, 1 for sane amount of weeb, 0 for japanese | |
const VERBOSELOG = false; //dump mjs records to output - will make the file too large for tenhou.net/5 viewer | |
const PRETTY = true; //make the written log somewhat human readable | |
const SHOWFU = false; //always show fu/han for scoring - even for limit hands | |
//words that can end up in log, some are mandatory kanji in places | |
const JPNAME = 0; | |
const RONAME = 1; | |
const ENNAME = 2; | |
const RUNES = { | |
/*hand limits*/ | |
"mangan" : ["満貫", "Mangan ", "Mangan " ], | |
"haneman" : ["跳満", "Haneman ", "Haneman " ], | |
"baiman" : ["倍満", "Baiman ", "Baiman " ], | |
"sanbaiman" : ["三倍満", "Sanbaiman ", "Sanbaiman " ], | |
"yakuman" : ["役満", "Yakuman ", "Yakuman " ], | |
"kazoeyakuman" : ["数え役満", "Kazoe Yakuman ", "Counted Yakuman " ], | |
"kiriagemangan" : ["切り上げ満貫", "Kiriage Mangan ", "Rounded Mangan " ], | |
/*round enders*/ | |
"agari" : ["和了", "Agari", "Agari" ], | |
"ryuukyoku" : ["流局", "Ryuukyoku", "Exhaustive Draw" ], | |
"nagashimangan" : ["流し満貫", "Nagashi Mangan", "Mangan at Draw" ], | |
"suukaikan" : ["四開槓", "Suukaikan", "Four Kan Abortion" ], | |
"sanchahou" : ["三家和", "Sanchahou", "Three Ron Abortion" ], | |
"kyuushukyuuhai" : ["九種九牌", "Kyuushu Kyuuhai", "Nine Terminal Abortion"], | |
"suufonrenda" : ["四風連打", "Suufon Renda", "Four Wind Abortion" ], | |
"suuchariichi" : ["四家立直", "Suucha Riichi", "Four Riichi Abortion" ], | |
/*scoring*/ | |
"fu" : ["符", /*"Fu",*/"符", "Fu" ], | |
"han" : ["飜", /*"Han",*/"飜", "Han" ], | |
"points" : ["点", /*"Points",*/"点", "Points" ], | |
"all" : ["∀", "∀", "∀" ], | |
"pao" : ["包", "pao", "Responsibility" ], | |
/*rooms*/ | |
"tonpuu" : ["東喰", " East", " East" ], | |
"hanchan" : ["南喰", " South", " South" ], | |
"friendly" : ["友人戦", "Friendly", "Friendly" ], | |
"tournament" : ["大会戦", "Tounament", "Tournament" ], | |
"sanma" : ["三", "3-Player ", "3-Player " ], | |
"red" : ["赤", " Red", " Red Fives" ], | |
"nored" : ["", " Aka Nashi", " No Red Fives" ] | |
}; | |
//senkinin barai yaku - please don't change, yostar.. | |
const DAISANGEN = 37; //daisangen cfg.fan.fan.map_ index | |
const DAISUUSHI = 50; | |
const TSUMOGIRI = 60; //tenhou tsumogiri symbol | |
//global variables - don't touch | |
let ALLOW_KIRIAGE = false; //potentially allow this to be true | |
let TSUMOLOSSOFF = false; //sanma tsumo loss, is set true for sanma when tsumo loss off | |
//listen for key press, modified from anonymizer mod | |
function checkscene(scene) | |
{ | |
return scene && ((scene.Inst && scene.Inst._enable) || (scene._Inst && scene._Inst._enable)); | |
} | |
document.addEventListener("keydown", function(e) | |
{ // GameMgr.Inst.record_uuid becomes populated when we have looked at a log | |
e = e || window.event; | |
if ((KEY == e.keyCode || KEY == e.key) && GameMgr.Inst.record_uuid) | |
if (checkscene(uiscript.UI_Replay) || checkscene(uiscript.UI_Loading)) | |
downloadlog(); | |
}); | |
//pop-up window for downloading | |
function download(filename, text) | |
{ | |
let element = document.createElement("a"); | |
element.setAttribute( | |
"href", | |
"data:text/plain;charset=utf-8," + encodeURIComponent(text) | |
); | |
element.setAttribute("download", filename); | |
element.style.display = "none"; | |
document.body.appendChild(element); | |
element.click(); | |
document.body.removeChild(element); | |
return; | |
} | |
//pad a to length l with f, needed to pad log for >sanma | |
const pad_right = (a, l, f) => | |
!Array.from({length: l - a.length}) | |
.map(_ => a.push(f)) || a; | |
//take '2m' and return 2 + 10 etc. | |
function tm2t(str) | |
{ //tenhou's tile encoding: | |
// 11-19 - 1-9 man | |
// 21-29 - 1-9 pin | |
// 31-39 - 1-9 sou | |
// 41-47 - ESWN WGR | |
// 51,52,53 - aka 5 man, pin, sou | |
let num = parseInt(str[0]); | |
const tcon = { m : 1, p : 2, s : 3, z : 4 }; | |
return num ? 10 * tcon[str[1]] + num : 50 + tcon[str[1]]; | |
} | |
//return normal tile from aka, tenhou rep | |
function deaka(til) | |
{ //alternativly - use strings | |
if (5 == ~~(til/10)) | |
return 10*(til%10)+(~~(til/10)); | |
return til; | |
} | |
//return aka version of tile | |
function makeaka(til) | |
{ | |
if (5 == (til%10)) //is a five (or haku) | |
return 10*(til%10)+(~~(til/10)); | |
return til; //can't be/already is aka | |
} | |
//round up to nearest hundred iff TSUMOLOSSOFF == true otherwise return 0 | |
function tlround(x) | |
{ | |
return TSUMOLOSSOFF ? 100*Math.ceil(x/100) : 0; | |
} | |
//parse mjs hule into tenhou agari list | |
function parsehule(h, kyoku) | |
{ //tenhou log viewer requires 点, 飜) or 役満) to end strings, rest of scoring string is entirely optional | |
//who won, points from (self if tsumo), who won or if pao: who's responsible | |
let res = [h.seat, h.zimo ? h.seat : kyoku.ldseat, h.seat]; | |
let delta = []; //we need to compute the delta ourselves to handle double/triple ron | |
let points = 0; | |
let rp = (-1 != kyoku.nriichi) ? 1000 * (kyoku.nriichi + kyoku.round[2]) : 0; //riichi stick points, -1 means already taken | |
let hb = 100 * kyoku.round[1]; //base honba payment | |
//sekinin barai logic | |
let pao = false; | |
let liableseat = -1; | |
let liablefor = 0; | |
if (h.yiman) | |
{ //only worth checking yakuman hands | |
h.fans.forEach(e => | |
{ | |
if (DAISUUSHI == e.id && (-1 != kyoku.paowind)) | |
{ //daisuushi pao | |
pao = true; | |
liableseat = kyoku.paowind; | |
liablefor += e.val; //realistically can only be liable once | |
} | |
else if (DAISANGEN == e.id && (-1 != kyoku.paodrag)) | |
{ | |
pao = true; | |
liableseat = kyoku.paodrag; | |
liablefor += e.val; | |
} | |
}); | |
} | |
if (h.zimo) | |
{ //ko-oya payment for non-dealer tsumo | |
//delta = [...new Array(kyoku.nplayers)].map(()=> (-hb - h.point_zimo_xian)); | |
delta = new Array(kyoku.nplayers).fill(-hb - h.point_zimo_xian - tlround((1/2) * (h.point_zimo_xian))) | |
if (h.seat == kyoku.dealerseat) //oya tsumo | |
{ | |
delta[h.seat] = rp + (kyoku.nplayers - 1) * (hb + h.point_zimo_xian) + 2 * tlround((1/2) * (h.point_zimo_xian)); | |
points = h.point_zimo_xian + tlround((1/2) * (h.point_zimo_xian)); | |
} | |
else //ko tsumo | |
{ | |
delta[h.seat] = rp + hb + h.point_zimo_qin + (kyoku.nplayers - 2) * (hb + h.point_zimo_xian) + 2 * tlround((1/2) * (h.point_zimo_xian)); | |
delta[kyoku.dealerseat] = -hb - h.point_zimo_qin - tlround((1/2) * (h.point_zimo_xian)); | |
points = h.point_zimo_xian + "-" + h.point_zimo_qin; | |
} | |
} | |
else | |
{ //ron | |
delta = new Array(kyoku.nplayers).fill(0.) | |
delta[h.seat] = rp + (kyoku.nplayers - 1) * hb + h.point_rong; | |
delta[kyoku.ldseat] = -(kyoku.nplayers - 1) * hb - h.point_rong; | |
points = h.point_rong; | |
kyoku.nriichi = -1; //mark the sticks as taken, in case of double ron | |
} | |
//sekinin barai payments | |
// treat pao as the liable player paying back the other players - safe for multiple yakuman | |
const OYA = 0; | |
const KO = 1; | |
const RON = 2; | |
const YSCORE = [ //yakuman scoring table | |
//oya, ko, ron pays | |
[0, 16000, 48000], //oya wins | |
[16000, 8000, 32000] //ko wins | |
]; | |
if (pao) | |
{ | |
res[2] = liableseat; //this is how tenhou does it - doesn't really seem to matter to akochan or tenhou.net/5 | |
if (h.zimo) //liable player needs to payback n yakuman tsumo payments | |
{ | |
if (h.qinjia) //dealer tsumo | |
{ //should treat tsumo loss as ron, luckily all yakuman values round safely for north bisection | |
delta[liableseat] -= 2 * hb + liablefor * 2 * YSCORE[OYA][KO] + tlround((1/2) * liablefor * YSCORE[OYA][KO]); // 1? only paying back other ko | |
delta.forEach((e, i) => | |
{ | |
if (liableseat != i && h.seat != i && kyoku.nplayers >= i) | |
delta[i] += hb + liablefor * YSCORE[OYA][KO] + tlround((1/2) * liablefor * (YSCORE[OYA][KO])); | |
}); | |
if (3 == kyoku.nplayers) //dealer should get north's payment from liable | |
delta[h.seat] += (TSUMOLOSSOFF ? 0 : liablefor * YSCORE[OYA][KO]); | |
} | |
else //non-dealer tsumo | |
{ | |
delta[liableseat] -= (kyoku.nplayers - 2) * hb + liablefor * (YSCORE[KO][OYA] + YSCORE[KO][KO]) + tlround((1/2) * liablefor * YSCORE[KO][KO]); //^^same 1st, but ko | |
delta.forEach((e, i) => | |
{ | |
if (liableseat != i && h.seat != i && kyoku.nplayers >= i) | |
{ | |
if (kyoku.dealerseat == i) | |
delta[i] += hb + liablefor * YSCORE[KO][OYA] + tlround((1/2) * liablefor * YSCORE[KO][KO]); //^^same 1st ... | |
else | |
delta[i] += hb + liablefor * YSCORE[KO][KO] + tlround((1/2) * liablefor * YSCORE[KO][KO]); //^^same 1st ... | |
} | |
}); | |
} | |
} | |
else //ron | |
{ | |
//liable seat pays the deal-in seat 1/2 yakuman + full honba | |
delta[liableseat] -= (kyoku.nplayers - 1) * hb + (1/2) * liablefor * YSCORE[h.qinjia ? OYA : KO][RON]; | |
delta[kyoku.ldseat] += (kyoku.nplayers - 1) * hb + (1/2) * liablefor * YSCORE[h.qinjia ? OYA : KO][RON]; | |
} | |
} //if pao | |
//append point symbol | |
points += RUNES.points[JPNAME] + ((h.zimo && h.qinjia) ? RUNES.all[NAMEPREF]: ""); | |
//score string | |
let fuhan = h.fu + RUNES.fu[NAMEPREF] + h.count + RUNES.han[NAMEPREF]; | |
if (h.yiman) //yakuman | |
res.push((SHOWFU ? fuhan : "") + RUNES.yakuman[NAMEPREF] + points); | |
else if (13 <= h.count) //kazoe | |
res.push((SHOWFU ? fuhan : "") + RUNES.kazoeyakuman[NAMEPREF] + points); | |
else if (11 <= h.count) //sanbaiman | |
res.push((SHOWFU ? fuhan : "") + RUNES.sanbaiman[NAMEPREF] + points); | |
else if (8 <= h.count) //baiman | |
res.push((SHOWFU ? fuhan : "") + RUNES.baiman[NAMEPREF] + points); | |
else if (6 <= h.count) //haneman | |
res.push((SHOWFU ? fuhan : "") + RUNES.haneman[NAMEPREF] + points); | |
else if (5 <= h.count || (4 <= h.count && 40 <= h.fu) || (3 <= h.count && 70 <= h.fu)) //mangan | |
res.push((SHOWFU ? fuhan : "") + RUNES.mangan[NAMEPREF] + points); | |
else if (ALLOW_KIRIAGE && ((4 == h.count && 30 == h.fu) || (3 == h.count && 60 == h.fu))) //kiriage | |
res.push((SHOWFU ? fuhan : "") + RUNES.kiriagemangan[NAMEPREF] + points); | |
else //ordinary hand | |
res.push(fuhan + points); | |
h.fans.forEach(e => res.push( | |
(JPNAME == NAMEPREF ? cfg.fan.fan.map_[e.id].name_jp : cfg.fan.fan.map_[e.id].name_en) | |
+ "(" + (h.yiman ? (RUNES.yakuman[JPNAME]) : (e.val + RUNES.han[JPNAME]) )+ ")" | |
)); | |
return [pad_right(delta, 4, 0.), res]; | |
} | |
//round information, to be reset every RecordNewRound | |
let kyoku = []; | |
kyoku.init = function(leaf) | |
{ //[kyoku, honba, riichi sticks] - NOTE: 4 mult. works for sanma | |
this.nplayers = leaf.scores.length; | |
this.round = [4 * leaf.chang + leaf.ju, leaf.ben, leaf.liqibang]; | |
this.initscores = leaf.scores; pad_right(this.initscores, 4, 0); | |
this.doras = leaf.dora ? [tm2t(leaf.dora)] : leaf.doras.map(e => tm2t(e)); | |
this.draws = [[],[],[],[]]; | |
this.discards = [[],[],[],[]]; | |
this.haipais = this.draws.map( (_, i) => leaf["tiles" + i].map( f => tm2t(f))); | |
//treat the last tile in the dealer's hand as a drawn tile | |
this.poppedtile = this.haipais[leaf.ju].pop(); | |
this.draws[leaf.ju].push(this.poppedtile); | |
//information we need, but can't expect in every record | |
this.dealerseat = leaf.ju; | |
this.ldseat = -1; //who dealt the last tile | |
this.nriichi = 0; //number of current riichis - needed for scores, abort workaround | |
this.nkan = 0; //number of current kans - only for abort workaround | |
//pao rule | |
this.nowinds = new Array(4).fill(0);//counter for each players open wind pons/kans | |
this.nodrags = new Array(4).fill(0); | |
this.paowind = -1; //seat of who dealt the final wind, -1 if no one is responsible | |
this.paodrag = -1; | |
return this; | |
}; | |
//dump round informaion | |
kyoku.dump = function(uras) | |
{ //NOTE: doras,uras are the indicators | |
let entry = []; | |
entry.push(kyoku.round); | |
entry.push(kyoku.initscores); | |
entry.push(kyoku.doras); | |
entry.push(uras); | |
kyoku.haipais.forEach((f,i) => | |
{ | |
entry.push(f); | |
entry.push(kyoku.draws[i]); | |
entry.push(kyoku.discards[i]); | |
}); | |
return entry; | |
} | |
//sekinin barai tiles | |
const WINDS = ["1z", "2z", "3z", "4z"].map(e => tm2t(e)); | |
const DRAGS = ["5z", "6z", "7z", "0z"].map(e => tm2t(e)); //0z would be aka haku | |
//senkinin barai incrementer - to be called every pon, daiminkan, ankan | |
kyoku.countpao = function(tile, owner, feeder) | |
{ //owner and feeder are seats, tile should be tenhou | |
if (WINDS.includes(tile)) | |
{ | |
if(4 == ++this.nowinds[owner]) | |
this.paowind = feeder; | |
} | |
else if (DRAGS.includes(tile)) | |
{ | |
if(3 == ++this.nodrags[owner]) | |
this.paodrag = feeder; | |
} | |
return; | |
} | |
//seat1 is seat0's x | |
function relativeseating(seat0, seat1) | |
{ //0: kamicha, 1: toimen, 2: if shimocha | |
return (seat0 - seat1 + 4 - 1) % 4; | |
} | |
//convert mjs records to tenhou log | |
function generatelog(mjslog) | |
{ | |
let log = []; | |
mjslog.forEach((e, leafidx) => | |
{ | |
switch (e.constructor.name) | |
{ | |
case "RecordNewRound": | |
{ //new round | |
kyoku.init(e); | |
return; | |
} | |
case "RecordDiscardTile": | |
{ //discard - marking tsumogiri and riichi | |
let symbol = e.moqie ? TSUMOGIRI : tm2t(e.tile); | |
//we pretend that the dealer's initial 14th tile is drawn - so we need to manually check the first discard | |
if (e.seat == kyoku.dealerseat | |
&& !kyoku.discards[e.seat].length && symbol == kyoku.poppedtile) | |
symbol = TSUMOGIRI; | |
if (e.is_liqi) //riichi delcaration | |
{ | |
kyoku.nriichi++; | |
symbol = "r" + symbol; | |
} | |
kyoku.discards[e.seat].push(symbol); | |
kyoku.ldseat = e.seat; //for ron, pon etc. | |
//sometimes we get dora passed here | |
if (e.doras && e.doras.length > kyoku.doras.length) | |
kyoku.doras = e.doras.map(f => tm2t(f)); | |
return; | |
} | |
case "RecordDealTile": | |
{ //draw - after kan this gets passed the new dora | |
if (e.doras && e.doras.length > kyoku.doras.length) | |
kyoku.doras = e.doras.map(f => tm2t(f)); | |
kyoku.draws[e.seat].push(tm2t(e.tile)); | |
return; | |
} | |
case "RecordChiPengGang": | |
{ //call - chi, pon, daiminkan | |
switch (e.type) | |
{ | |
case 0: | |
{ //chii | |
kyoku.draws[e.seat].push( | |
"c" + | |
tm2t(e.tiles[2]) + | |
tm2t(e.tiles[0]) + | |
tm2t(e.tiles[1]) | |
); | |
return; | |
} | |
case 1: | |
{ //pon | |
let worktiles = e.tiles.map(f => tm2t(f)); | |
let idx = relativeseating(e.seat, kyoku.ldseat); | |
kyoku.countpao(worktiles[0], e.seat, kyoku.ldseat); | |
//pop the called tile a preprend 'p' | |
worktiles.splice(idx, 0, "p" + worktiles.pop()); | |
kyoku.draws[e.seat].push(worktiles.join("")); | |
return; | |
} | |
case 2: | |
{ /////////////////////////////////////////////////// | |
// kan naki: | |
// daiminkan: | |
// kamicha "m39393939" (0) | |
// toimen "39m393939" (1) | |
// shimocha "222222m22" (3) | |
// (writes to draws; 0 to discards) | |
// shouminkan: (same order as pon; immediate tile after k is the added tile) | |
// kamicha "k37373737" (0) | |
// toimen "31k313131" (1) | |
// shimocha "3737k3737" (2) | |
// (writes to discards) | |
// ankan: | |
// "121212a12" (3) | |
// (writes to discards) | |
/////////////////////////////////////////////////// | |
//daiminkan | |
let calltiles = e.tiles.map(f => tm2t(f)); | |
// < kamicha 0 | toimen 1 | shimocha 3 > | |
let idx = relativeseating(e.seat, kyoku.ldseat); | |
kyoku.countpao(calltiles[0], e.seat, kyoku.ldseat); | |
calltiles.splice( 2 == idx ? 3 : idx, 0, "m" + calltiles.pop()); | |
kyoku.draws[e.seat].push(calltiles.join("")); | |
//tenhou drops a 0 in discards for this | |
kyoku.discards[e.seat].push(0); | |
//register kan | |
kyoku.nkan++; | |
return; | |
} | |
default: | |
console.log( | |
"didn't know what to do with " + | |
e.constructor.name + "(" + leafidx + ")" | |
); | |
return; | |
} | |
} | |
case "RecordAnGangAddGang" : | |
{ //kan - shouminkan 'k', ankan 'a' | |
//NOTE: e.tiles here is a single tile; naki is placed in discards | |
let til = tm2t(e.tiles); | |
kyoku.ldseat = e.seat; // for chankan, no conflict as last discard has passed | |
switch (e.type) | |
{ | |
case 3: | |
{ //ankan | |
//////////////////// | |
// mjs chun ankan example record: | |
//{"seat":0,"type":3,"tiles":"7z"} | |
//////////////////// | |
kyoku.countpao(til, e.seat, -1); //count the group as visible, but don't set pao | |
//get the tiles from haipai and draws that | |
//are involved in ankan, dumb | |
//because n aka might be involved | |
let ankantiles = kyoku.haipais[e.seat].filter(t => (deaka(t) == deaka(til) ? true : false)) | |
.concat(kyoku.draws[e.seat].filter(t => (deaka(t) == deaka(til) ? true : false)) ); | |
til = ankantiles.pop(); //doesn't really matter which tile we mark ankan with - chosing last drawn | |
kyoku.discards[e.seat].push(ankantiles.join("") + "a" + til); //push naki | |
kyoku.nkan++; | |
return; | |
} | |
case 2: | |
{ //shouminkan | |
//get pon naki from .draws and swap in new symbol | |
let nakis = kyoku.draws[e.seat].filter(w => | |
{ | |
if ('string' === typeof w) //naki | |
return w.includes("p" + deaka(til)) || w.includes("p" + makeaka(til)); //pon involves same tile type | |
else | |
return false; | |
}); | |
kyoku.discards[e.seat].push(nakis[0].replace(/p/, "k" + til)); //push naki | |
kyoku.nkan++; | |
return; | |
} | |
default: | |
{ | |
console.log("didn't know what to do with " | |
+ e.constructor.name + " type: " + e.type); | |
return; | |
} | |
} | |
return; | |
} | |
case "RecordBaBei" : | |
{ //kita - this record (only) gives {seat, moqie} | |
//NOTE: tenhou doesn't mark its kita based on when they were drawn, so we won't | |
//if (e.moqie) | |
// kyoku.discards[e.seat].push("f" + TSUMOGIRI); | |
//else | |
kyoku.discards[e.seat].push("f44"); | |
return; | |
} | |
///////////////////////////////////////////////////// | |
// round enders: | |
// "RecordNoTile" - ryuukyoku | |
// "RecordHule" - agari - ron/tsumo | |
// "RecordLiuJu" - abortion | |
////////////////////////////////////////////////////// | |
case "RecordLiuJu" : | |
{ //abortion | |
let entry = kyoku.dump([]); | |
if (1 == e.type) | |
entry.push([RUNES.kyuushukyuuhai[NAMEPREF]]); //kyuushukyuhai | |
else if (2 == e.type) | |
entry.push([RUNES.suufonrenda[NAMEPREF]]); //suufon renda | |
else if (4 == kyoku.nriichi) //TODO: actually get the type code | |
entry.push([RUNES.suuchariichi[NAMEPREF]]); //4 riichi | |
else if (4 <= kyoku.nkan) //TODO: actually get type code | |
entry.push([RUNES.suukaikan[NAMEPREF]]); //4 kan, potentially false positive on 3 ron with 4 kans | |
else | |
entry.push([RUNES.sanchahou[NAMEPREF]]); //3 ron - can't actually get this in mjs | |
log.push(entry); | |
return; | |
} | |
case "RecordNoTile" : | |
{ //ryuukyoku | |
let entry = kyoku.dump([]); | |
let delta = new Array(4).fill(0.); | |
//NOTE: mjs wll not give delta_scores if everyone is (no)ten - TODO: minimize the autism | |
if (e.scores && e.scores[0] && e.scores[0].delta_scores && e.scores[0].delta_scores.length) | |
e.scores.forEach(f => f.delta_scores.forEach((g, i) => delta[i] += g)); //for the rare case of multiple nagashi, we sum the arrays | |
if (e.liujumanguan) //nagashi mangan | |
entry.push([RUNES.nagashimangan[NAMEPREF], delta]) | |
else //normal ryuukyoku | |
entry.push([RUNES.ryuukyoku[NAMEPREF], delta]); | |
log.push(entry); | |
return; | |
} | |
case "RecordHule": | |
{ //agari | |
let agari = []; | |
let ura = []; | |
e.hules.forEach( f => | |
{ | |
if (ura.length < (f.li_doras ? f.li_doras.length : 0)) //take the longest ura list - double ron with riichi + dama | |
ura = f.li_doras.map(g => tm2t(g)); | |
agari.push(parsehule(f, kyoku)); | |
}); | |
let entry = kyoku.dump(ura); | |
entry.push( [RUNES.agari[JPNAME]].concat(agari.flat()) ); //needs the japanese agari | |
log.push(entry); | |
return; | |
} | |
default: | |
console.log( | |
"didn't know what to do with " + e.constructor.name + "(" + leafidx + ")" | |
); | |
return; | |
} | |
}); | |
return log; | |
} | |
//this is the json struct that we write to file | |
function parse(record) | |
{ | |
let res = {}; | |
let ruledisp = ""; | |
let lobby = ""; //usually 0, is the custom lobby number | |
let nplayers = record.head.result.players.length; | |
let nakas = nplayers - 1; //default | |
// anon edit 1 start | |
var mjslog = []; | |
var mjsact = net.MessageWrapper.decodeMessage(record.data).actions; | |
mjsact.forEach(e => {if(e.result.length!==0)mjslog.push(net.MessageWrapper.decodeMessage(e.result))}); | |
// anon edit 1 end | |
res["ver"] = "2.3"; // mlog version number | |
res["ref"] = record.head.uuid; // game id - copy and paste into "other" on the log page to view | |
res["log"] = generatelog(mjslog); | |
//PF4 is yonma, PF3 is sanma | |
res["ratingc"] = "PF" + nplayers; | |
//rule display | |
if (3 == nplayers && JPNAME == NAMEPREF) | |
ruledisp += RUNES.sanma[JPNAME]; | |
if (record.head.config.meta.mode_id) //ranked or casual | |
ruledisp += (JPNAME == NAMEPREF) ? | |
cfg.desktop.matchmode.map_[record.head.config.meta.mode_id].room_name_jp | |
: cfg.desktop.matchmode.map_[record.head.config.meta.mode_id].room_name_en; | |
else if (record.head.config.meta.room_id) //friendly | |
{ | |
lobby = ": " + record.head.config.meta.room_id; //can set room number as lobby number | |
ruledisp += RUNES.friendly[NAMEPREF]; //"Friendly"; | |
nakas = record.head.config.mode.detail_rule.dora_count; | |
TSUMOLOSSOFF = (3 == nplayers) ? ! record.head.config.mode.detail_rule.have_zimosun : false; | |
} | |
else if (record.head.config.meta.contest_uid) //tourney | |
{ | |
lobby = ": " + record.head.config.meta.contest_uid; | |
ruledisp += RUNES.tournament[NAMEPREF]; //"Tournament"; | |
nakas = record.head.config.mode.detail_rule.dora_count; | |
TSUMOLOSSOFF = (3 == nplayers) ? ! record.head.config.mode.detail_rule.have_zimosun : false; | |
} | |
if (1 == record.head.config.mode.mode) | |
{ | |
ruledisp += RUNES.tonpuu[NAMEPREF]; //" East"; | |
} | |
else if (2 == record.head.config.mode.mode) | |
{ | |
ruledisp += RUNES.hanchan[NAMEPREF]; //" South"; | |
} | |
if (! record.head.config.meta.mode_id && ! record.head.config.mode.detail_rule.dora_count) | |
{ | |
if (JPNAME != NAMEPREF) | |
ruledisp += RUNES.nored[NAMEPREF]; | |
res["rule"] = {"disp" : ruledisp, "aka53" : 0, "aka52" : 0, "aka51": 0}; | |
} | |
else | |
{ | |
if (JPNAME == NAMEPREF) | |
ruledisp += RUNES.red[JPNAME]; | |
res["rule"] = {"disp" : ruledisp, "aka53" : 1, "aka52" : (4 == nakas ? 2 : 1), "aka51": (4 == nplayers ? 1 : 0)}; | |
} | |
res["lobby"] = 0; //tenhou custom lobby - could be tourney id or friendly room for mjs. appending to title instead to avoid 3->C etc. in tenhou.net/5 | |
// autism to fix logs with AI | |
// ranks | |
res["dan"] = new Array(4).fill(''); | |
record.head.accounts.forEach(e => | |
res["dan"][e.seat] = (JPNAME == NAMEPREF) ? | |
cfg.level_definition.level_definition.map_[e.level.id].full_name_jp | |
: cfg.level_definition.level_definition.map_[e.level.id].full_name_en | |
); | |
// level score, no real analog to rate | |
res["rate"] = new Array(4).fill(0); | |
record.head.accounts.forEach(e => res["rate"][e.seat] = e.level.score); //level score, closest thing to rate | |
// sex | |
res["sx"] = new Array(4).fill('C') | |
record.head.accounts.forEach(e => { | |
let sex = cfg.item_definition.character.map_[e.character.charid].sex; | |
res["sx"][e.seat] = ( 1 == sex ) ? "F" : (2 == sex ? "M" : "C"); | |
}); | |
// >names | |
res["name"] = new Array(4).fill('AI'); | |
record.head.accounts.forEach(e => res["name"][e.seat] = e.nickname); | |
// clean up for sanma AI | |
if (3 == nplayers) | |
{ | |
res["name"][3] = ""; | |
res["sx"][3] = ""; | |
} | |
// scores | |
let scores = record.head.result.players | |
.map(e => [e.seat, e.part_point_1, e.total_point / 1000]); | |
res["sc"] = new Array(8).fill(0); | |
scores.forEach((e, i) => {res["sc"][2 * e[0]] = e[1]; res["sc"][2 * e[0] + 1] = e[2];}); | |
//optional title - why not give the room and put the timestamp here; 1000 for unix to .js timestamp convention | |
res["title"] = [ ruledisp + lobby, | |
(new Date(record.head.end_time * 1000)).toLocaleString() | |
]; | |
//optionally dump mjs records NOTE: this will likely make the file too large for tenhou.net/5 viewer | |
if (VERBOSELOG) | |
{ | |
res["mjshead"] = record.head; | |
res["mjslog"] = mjslog; | |
res["mjsrecordtypes"] = mjslog.map(e => e.constructor.name); | |
} | |
return res; | |
} | |
function downloadlog() | |
{ | |
app.NetAgent.sendReq2Lobby( | |
"Lobby", | |
"fetchGameRecord", | |
{ game_uuid: GameMgr.Inst.record_uuid, client_version_string: GameMgr.Inst.getClientVersion()}, // anon edit 2 | |
function(i, record) { | |
let results = parse(record); | |
download( | |
//default filename | |
((new Date(record.head.end_time * 1000)).toLocaleDateString() + "_" + results["rule"]["disp"] + ".json").replace(/[ \/]/g,"_"), | |
PRETTY ? | |
JSON.stringify(results, null, " ") | |
.replace(/\n \s+/g, " ") //bring up log array items | |
.replace(/], \[/g,"],\n [") //bump nested lists back down | |
.replace(/\n\s+]/g," ]") //bring up isolated right brackets | |
.replace(/\n\s+},\n/g," },\n") //ditto for non-final curly brackets | |
: JSON.stringify(results) | |
); | |
} | |
); | |
} | |
})(); | |
// vim: ts=4 et |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment