Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
M
MajsoulPlusMods
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Packages
Packages
List
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issues
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
森野さくら
MajsoulPlusMods
Commits
2915981e
Commit
2915981e
authored
Jan 02, 2021
by
TanakaKotoha
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
dededededededededededede
parent
5d134e2e
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
616 additions
and
220 deletions
+616
-220
extension/downloadlogs/extension.json
extension/downloadlogs/extension.json
+1
-1
extension/downloadlogs/script.js
extension/downloadlogs/script.js
+570
-219
extension/downloadlogs/viewlog.cmd
extension/downloadlogs/viewlog.cmd
+20
-0
extension/downloadlogs/viewlog.sh
extension/downloadlogs/viewlog.sh
+25
-0
No files found.
extension/downloadlogs/extension.json
View file @
2915981e
{
"id"
:
"downloadlogs"
,
"version"
:
"1.
0.2
"
,
"version"
:
"1.
1.0
"
,
"name"
:
"按S下载牌谱"
,
"author"
:
"凉宫杏树"
,
"description"
:
"太好了,准备把牌谱拿给主人看。"
,
...
...
extension/downloadlogs/script.js
View file @
2915981e
// ==UserScript==
// @name downloadlogs
// @namespace
reddit
// @namespace
mjg
// @icon https://cdn.myanimelist.net/images/characters/12/75583.jpg
// @version 0.
0.2
// @description
download logs for akochan
// @version 0.
1.0
// @description
save mjs logs
// @include https://mahjongsoul.game.yo-star.com/
// @include https://game.mahjongsoul.com/
// @include https://majsoul.union-game.com/0/
// ==/UserScript==
(
function
()
{
//the key we listen for
//var KEY = 17; //ctrl
const
KEY
=
83
;
//"s"
const
VERBOSELOG
=
false
;
(
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
var
ALLOW_KIRIAGE
=
false
;
//potentially allow this to be true
var
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
)
{
function
checkscene
(
scene
)
{
return
scene
&&
((
scene
.
Inst
&&
scene
.
Inst
.
_enable
)
||
(
scene
.
_Inst
&&
scene
.
_Inst
.
_enable
));
}
// GameMgr.Inst.record_uuid becomes populated when we have looked at a log
document
.
addEventListener
(
"
keydown
"
,
function
(
e
)
{
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
))
...
...
@@ -31,7 +80,8 @@
});
//pop-up window for downloading
function
download
(
filename
,
text
)
{
function
download
(
filename
,
text
)
{
var
element
=
document
.
createElement
(
"
a
"
);
element
.
setAttribute
(
"
href
"
,
...
...
@@ -42,182 +92,345 @@
document
.
body
.
appendChild
(
element
);
element
.
click
();
document
.
body
.
removeChild
(
element
);
return
;
}
// tenhou's tile encoding:
//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 - red 5 man, pin, sou
function
tm2t
(
str
)
{
//take '2m' and return 2 + 10 etc.
// 51,52,53 - aka 5 man, pin, sou
var
num
=
parseInt
(
str
[
0
]);
const
pad
=
{
m
:
1
,
p
:
2
,
s
:
3
,
z
:
4
};
return
num
?
10
*
pad
[
str
[
1
]]
+
num
:
50
+
pad
[
str
[
1
]];
const
tcon
=
{
m
:
1
,
p
:
2
,
s
:
3
,
z
:
4
};
return
num
?
10
*
tcon
[
str
[
1
]]
+
num
:
50
+
tcon
[
str
[
1
]];
}
//round information, to be reset every RecordNewRound
var
actiontable
=
[];
actiontable
.
init
=
function
(
haipais
)
{
this
.
draws
=
[];
this
.
discards
=
[];
this
.
haipais
=
[];
haipais
.
forEach
(
e
=>
{
this
.
draws
.
push
([]);
this
.
discards
.
push
([]);
this
.
haipais
.
push
(
e
);
//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
var
res
=
[
h
.
seat
,
h
.
zimo
?
h
.
seat
:
kyoku
.
ldseat
,
h
.
seat
];
var
delta
=
[];
//we need to compute the delta ourselves to handle double/triple ron
var
points
=
0
;
var
rp
=
(
-
1
!=
kyoku
.
nriichi
)
?
1000
*
(
kyoku
.
nriichi
+
kyoku
.
round
[
2
])
:
0
;
//riichi stick points, -1 means already taken
var
hb
=
100
*
kyoku
.
round
[
1
];
//base honba payment
//sekinin barai logic
var
pao
=
false
;
var
liableseat
=
-
1
;
var
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
]));
});
this
.
ponedfrom
=
[];
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
var
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
var
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
.
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
;
};
//general form of how we dump round informaion
//NOTE: doras,uras are the indicators
actiontable
.
dump
=
function
(
uras
)
{
//dump round informaion
kyoku
.
dump
=
function
(
uras
)
{
//NOTE: doras,uras are the indicators
var
entry
=
[];
entry
.
push
(
actiontable
.
round
);
entry
.
push
(
actiontable
.
initscores
);
entry
.
push
(
actiontable
.
doras
);
entry
.
push
(
kyoku
.
round
);
entry
.
push
(
kyoku
.
initscores
);
entry
.
push
(
kyoku
.
doras
);
entry
.
push
(
uras
);
actiontable
.
haipais
.
forEach
((
f
,
i
)
=>
{
kyoku
.
haipais
.
forEach
((
f
,
i
)
=>
{
entry
.
push
(
f
);
entry
.
push
(
actiontable
.
draws
[
i
]);
entry
.
push
(
actiontable
.
discards
[
i
]);
entry
.
push
(
kyoku
.
draws
[
i
]);
entry
.
push
(
kyoku
.
discards
[
i
]);
});
return
entry
;
}
function
relativeseating
(
seat0
,
seat1
,
dim
)
{
//take two seats, return 0 if seat1 is kamicha,
// 1 if seat 1 is toimen, 2 if shimocha realative to seat0
//used in generating call symbols, extra +dim b/c .js
return
(
seat0
-
seat1
+
dim
-
1
)
%
dim
;
//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
;
}
function
parse
(
record
)
{
var
res
=
{};
res
[
"
ver
"
]
=
"
2.3
"
;
// mlog version number
res
[
"
ref
"
]
=
record
.
head
.
uuid
;
// game id
return
;
}
var
ruledisp
=
""
;
if
(
record
.
head
.
config
.
meta
.
mode_id
)
//normal room
ruledisp
=
cfg
.
desktop
.
matchmode
.
map_
[
record
.
head
.
config
.
meta
.
mode_id
].
room_name_en
;
else
if
(
record
.
head
.
config
.
meta
.
room_id
)
//friendly
ruledisp
=
"
Friendly
"
;
else
if
(
record
.
head
.
config
.
meta
.
contest_uid
)
//tourney
ruledisp
=
"
Tournament
"
;
if
(
1
==
record
.
head
.
config
.
mode
.
mode
)
ruledisp
+=
"
East
"
;
else
if
(
2
==
record
.
head
.
config
.
mode
.
mode
)
ruledisp
+=
"
South
"
;
if
(
!
record
.
head
.
config
.
meta
.
mode_id
&&
!
record
.
head
.
config
.
mode
.
detail_rule
.
dora_count
)
{
ruledisp
+=
"
Aka Nashi
"
;
res
[
"
rule
"
]
=
{
"
disp
"
:
ruledisp
,
"
aka53
"
:
0
,
"
aka52
"
:
0
,
"
aka51
"
:
0
};
//seat1 is seat0's x
function
relativeseating
(
seat0
,
seat1
)
{
//0: kamicha, 1: toimen, 2: if shimocha
return
(
seat0
-
seat1
+
4
-
1
)
%
4
;
}
else
res
[
"
rule
"
]
=
{
"
disp
"
:
ruledisp
,
"
aka53
"
:
1
,
"
aka52
"
:
1
,
"
aka51
"
:
1
};
//NOTE: this works fine for anonymous logs :^)
res
[
"
dan
"
]
=
record
.
head
.
accounts
.
map
(
e
=>
cfg
.
level_definition
.
level_definition
.
map_
[
e
.
level
.
id
].
full_name_en
);
res
[
"
title
"
]
=
[
record
.
head
.
config
.
category
,
//dummy entries
record
.
head
.
config
.
meta
.
mode_id
//
];
res
[
"
name
"
]
=
record
.
head
.
accounts
.
map
(
e
=>
e
.
nickname
);
//scores: doing points and change in rankpoints. it probably
//should be oka,uma'd points. w/e
res
[
"
sc
"
]
=
record
.
head
.
result
.
players
.
map
(
e
=>
[
e
.
part_point_1
,
e
.
grading_score
])
.
flat
();
//game record
res
[
"
mjshead
"
]
=
record
.
head
;
res
[
"
mjslog
"
]
=
net
.
MessageWrapper
.
decodeMessage
(
record
.
data
).
records
.
map
(
e
=>
net
.
MessageWrapper
.
decodeMessage
(
e
));
//make the constructor names available in json output
res
[
"
mjsrecordtypes
"
]
=
res
.
mjslog
.
map
(
e
=>
e
.
constructor
.
name
);
//convert to tenhou log
var
nplayers
=
res
.
name
.
length
;
//convert mjs records to tenhou log
function
generatelog
(
mjslog
)
{
var
log
=
[];
res
[
"
log
"
]
=
res
.
mjslog
.
forEach
((
e
,
leafidx
)
=>
{
switch
(
e
.
constructor
.
name
)
{
case
"
RecordNewRound
"
:
{
//TODO: move everything into init, or nothing..
actiontable
.
init
(
//get haipais, this way should handle >sanma
res
.
name
.
map
((
f
,
i
)
=>
e
[
"
tiles
"
+
i
].
map
(
g
=>
tm2t
(
g
)))
);
// kyoku, honba, riichi sticks
actiontable
.
round
=
[
nplayers
*
e
.
chang
+
e
.
ju
,
e
.
ben
,
e
.
liqibang
];
actiontable
.
initscores
=
e
.
scores
;
//scores at the beginning of the round
//treat the last tile in the dealer's hand as a drawn tile
actiontable
.
poppedtile
=
actiontable
.
haipais
[
e
.
ju
].
pop
();
actiontable
.
draws
[
e
.
ju
].
push
(
actiontable
.
poppedtile
);
actiontable
.
dealerseat
=
e
.
ju
;
actiontable
.
doras
=
e
.
doras
.
map
(
f
=>
tm2t
(
f
));
mjslog
.
forEach
((
e
,
leafidx
)
=>
{
switch
(
e
.
constructor
.
name
)
{
case
"
RecordNewRound
"
:
{
//new round
kyoku
.
init
(
e
);
return
;
}
case
"
RecordDiscardTile
"
:
{
//sometimes we get dora passed here
if
(
e
.
doras
&&
e
.
doras
.
length
>
actiontable
.
doras
.
length
)
actiontable
.
doras
=
e
.
doras
.
map
(
f
=>
tm2t
(
f
));
//record the discard, pre-pending 'r' with riichi
case
"
RecordDiscardTile
"
:
{
//discard - marking tsumogiri and riichi
var
symbol
=
e
.
moqie
?
TSUMOGIRI
:
tm2t
(
e
.
tile
);
if
(
e
.
seat
==
actiontable
.
dealerseat
&&
!
actiontable
.
discards
[
e
.
seat
].
length
&&
symbol
==
actiontable
.
poppedtile
)
//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
;
actiontable
.
discards
[
e
.
seat
].
push
(
e
.
is_liqi
?
"
r
"
+
symbol
:
symbol
);
actiontable
.
lastdiscardseat
=
e
.
seat
;
//for ron, pon etc.
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
"
:
{
//
after kan this gets passed the new dora
if
(
e
.
doras
&&
e
.
doras
.
length
>
actiontable
.
doras
.
length
)
actiontable
.
doras
=
e
.
doras
.
map
(
f
=>
tm2t
(
f
));
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
));
actiontable
.
draws
[
e
.
seat
].
push
(
tm2t
(
e
.
tile
));
kyoku
.
draws
[
e
.
seat
].
push
(
tm2t
(
e
.
tile
));
return
;
}
case
"
RecordChiPengGang
"
:
{
//we have a call
//TODO: clean this up/simplify
switch
(
e
.
type
)
{
case
0
:
{
//chii
actiontable
.
draws
[
e
.
seat
].
push
(
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
case
1
:
{
//pon
var
worktiles
=
e
.
tiles
.
map
(
f
=>
tm2t
(
f
));
var
idx
=
relativeseating
(
e
.
seat
,
actiontable
.
lastdiscardseat
,
nplayers
);
worktiles
.
splice
(
idx
,
0
,
"
p
"
);
actiontable
.
draws
[
e
.
seat
].
push
(
worktiles
.
join
(
""
));
//save idx for shouminkan
actiontable
.
ponedfrom
[
e
.
tiles
[
0
]]
=
idx
;
var
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
:
{
///////////////////////////////////////////////////
case
2
:
{
///////////////////////////////////////////////////
// kan naki:
// daiminkan:
// kamicha "m39393939" (0)
// toimen "39m393939" (1)
// shimocha "222222m22" (3)
// (writes to draws; 0 to discards)
// shouminkan: (same
as pon
)
// shouminkan: (same
order as pon; immediate tile after k is the added tile
)
// kamicha "k37373737" (0)
// toimen "31k313131" (1)
// shimocha "3737k3737" (2)
...
...
@@ -229,15 +442,16 @@
//daiminkan
var
calltiles
=
e
.
tiles
.
map
(
f
=>
tm2t
(
f
));
// < kamicha 0 | toimen 1 | shimocha 3 >
var
idx
=
relativeseating
(
e
.
seat
,
actiontable
.
lastdiscardseat
,
nplayers
);
calltiles
.
splice
(
2
==
idx
?
3
:
idx
,
0
,
"
m
"
);
actiontable
.
draws
[
e
.
seat
].
push
(
calltiles
.
join
(
""
));
var
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
actiontable
.
discards
[
e
.
seat
].
push
(
0
);
kyoku
.
discards
[
e
.
seat
].
push
(
0
);
//register kan
kyoku
.
nkan
++
;
return
;
}
default
:
...
...
@@ -245,125 +459,262 @@
"
didn't know what to do with
"
+
e
.
constructor
.
name
+
"
(
"
+
leafidx
+
"
)
"
);
return
;
}
}
case
"
RecordAnGangAddGang
"
:
{
//keyletter is 'k' for shouminkan, 'a' for ankan
var
callstr
;
var
offset
=
0
;
switch
(
e
.
type
)
{
case
3
:
{
//ankan
callstr
=
"
a
"
;
offset
=
3
;
//actiontable.lastdiscardseat = e.seat;
break
;
case
"
RecordAnGangAddGang
"
:
{
//kan - shouminkan 'k', ankan 'a'
//NOTE: e.tiles here is a single tile; naki is placed in discards
var
til
=
tm2t
(
e
.
tiles
);
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
var
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
callstr
=
"
k
"
;
offset
=
actiontable
.
ponedfrom
[
e
.
tiles
];
break
;
case
2
:
{
//shouminkan
//get pon naki from .draws and swap in new symbol
var
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
;
});
if
(
!
nakis
.
length
)
{
console
.
log
(
"
could not find previous pon naki for shouminkan with
"
+
e
.
tiles
);
return
;
}
default
:
{
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
;
}
}
//NOTE: e.tiles is a single tile here
var
til
=
tm2t
(
e
.
tiles
)
;
var
calltiles
=
[
til
,
til
,
til
,
til
];
// e.tiles.map(f => tm2t(f));
calltiles
.
splice
(
offset
,
0
,
callstr
);
//convient case of them matching
//relativeseating(
// e.seat,
// actiontable.lastdiscardseat,
// res.name.length, 0, callstr
);
//
NOTE: this maps to discards unlike normal calls
actiontable
.
discards
[
e
.
seat
].
push
(
calltiles
.
join
(
""
)
);
return
;
}
case
"
RecordBaBei
"
:
{
//kita - this record (only) gives {seat, moqie}
//NOTE: tenhou doesn't mark it's 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" - ryuukoku
// "RecordNoTile" - ryuuk
y
oku
// "RecordHule" - agari - ron/tsumo
// "RecordLiuJu" - abortion
//////////////////////////////////////////////////////
case
"
RecordLiuJu
"
:
{
//abortion: only have checked 9-terminal..
var
entry
=
actiontable
.
dump
([]);
//TODO: find the types for aborts other than kyushukyuhai
case
"
RecordLiuJu
"
:
{
//abortion
var
entry
=
kyoku
.
dump
([]);
if
(
1
==
e
.
type
)
entry
.
push
([
"
九種九牌
"
]);
//kyushukyuhai
else
//assuming this for now
entry
.
push
([
"
四家立直
"
]);
//4 riichi
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
"
:
{
//ryuukoku
var
entry
=
actiontable
.
dump
([]);
entry
.
push
([
"
流局
"
,
(
e
.
scores
&&
e
.
scores
[
0
]
&&
e
.
scores
[
0
].
delta_scores
&&
e
.
scores
[
0
].
delta_scores
.
length
)
?
e
.
scores
[
0
].
delta_scores
:
[...
new
Array
(
nplayers
)].
map
(()
=>
0
.)]);
//ryuukoku
case
"
RecordNoTile
"
:
{
//ryuukyoku
var
entry
=
kyoku
.
dump
([]);
var
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 is all in one list
//TODO: don't show fu for limit hands maybe?
case
"
RecordHule
"
:
{
//agari
var
agari
=
[];
var
ura
=
[];
e
.
hules
.
forEach
(
f
=>
{
if
(
ura
.
length
<
(
f
.
li_doras
?
f
.
li_doras
.
length
:
0
))
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
(
e
.
delta_scores
);
//TODO: split scores
//tenhou log viewer requires 点, 飜 to end strings
agari
.
push
([
f
.
seat
,
f
.
zimo
?
f
.
seat
:
actiontable
.
lastdiscardseat
,
f
.
seat
,
//(f.zimo ? "Tsumo "+f.point_zimo_qin +" / "+f.point_zimo_xian : "Ron "+f.point_rong)+"点",
f
.
fu
+
"
符
"
+
f
.
count
+
"
飜
"
+
f
.
point_sum
+
"
点
"
,
f
.
fans
.
map
(
g
=>
cfg
.
fan
.
fan
.
map_
[
g
.
id
].
name_en
+
"
(
"
+
g
.
val
+
"
飜)
"
)
].
flat
());
//flatten han
agari
.
push
(
parsehule
(
f
,
kyoku
));
});
var
entry
=
actiontable
.
dump
(
ura
);
entry
.
push
(
[
"
和了
"
].
concat
(
agari
)
);
var
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
;
}
});
res
[
"
log
"
]
=
log
;
//clean up the output
if
(
!
VERBOSELOG
)
return
log
;
}
//this is the json struct that we write to file
function
parse
(
record
)
{
var
res
=
{};
var
ruledisp
=
""
;
var
lobby
=
""
;
//usually 0, is the custom lobby number
var
nplayers
=
record
.
head
.
result
.
players
.
length
;
var
nakas
=
nplayers
-
1
;
//default
var
mjslog
=
net
.
MessageWrapper
.
decodeMessage
(
record
.
data
)
//move-by-move details
.
records
.
map
(
e
=>
net
.
MessageWrapper
.
decodeMessage
(
e
));
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
{
delete
res
[
"
mjslog
"
];
delete
res
[
"
mjshead
"
];
delete
res
[
"
mjsrecordtypes
"
]
;
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
res
[
"
dan
"
]
=
record
.
head
.
accounts
.
map
(
e
=>
(
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
);
pad_right
(
res
[
"
dan
"
],
4
,
""
);
res
[
"
rate
"
]
=
record
.
head
.
accounts
.
map
(
e
=>
e
.
level
.
score
);
//level score, closest thing to rate
pad_right
(
res
[
"
rate
"
],
4
,
0
);
res
[
"
sx
"
]
=
record
.
head
.
accounts
.
map
(
e
=>
cfg
.
item_definition
.
character
.
map_
[
e
.
character
.
charid
].
sex
)
.
map
(
e
=>
(
e
==
1
?
"
F
"
:
(
e
==
2
?
"
M
"
:
"
C
"
)));
//player's sex
pad_right
(
res
[
"
sx
"
],
4
,
"
C
"
);
//mjs results are sorted by placement (giving seats), tenhou sorts by seat
var
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
];});
res
[
"
name
"
]
=
record
.
head
.
accounts
.
map
(
e
=>
e
.
nickname
);
pad_right
(
res
[
"
name
"
],
4
,
""
);
//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
()
{
function
downloadlog
()
{
app
.
NetAgent
.
sendReq2Lobby
(
"
Lobby
"
,
"
fetchGameRecord
"
,
"
Lobby
"
,
"
fetchGameRecord
"
,
{
game_uuid
:
GameMgr
.
Inst
.
record_uuid
},
function
(
i
,
record
)
{
download
(
GameMgr
.
Inst
.
record_uuid
+
"
.json
"
,
JSON
.
stringify
(
parse
(
record
))
var
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
)
);
}
);
}
})();
//TODO: fix double ron scores, delta point arrays should be split - too much effort
//TODO: show limit hands with "Mangan" etc. instead of fu/han
//TODO: currently ignoring RecordBaBei - kita/dorara?
//TODO: show abortive draws properly
// vim: ts=4 et
extension/downloadlogs/viewlog.cmd
0 → 100644
View file @
2915981e
@echo
off
SETLOCAL
EnableDelayedExpansion
::gets around the windows parameter character limit by writing an .html that opens the tenhou/5 url
:: rem get unique file name
:: :uniqloop
:: set "tmpfile=%tmp%\bat~%RANDOM%.html"
:: if exist "%tmpfile%" goto :uniqloop
set
"tmpfile=
%tmp%
~viewlog.html"
<
NUL
echo
|
set
/p
=
"<head><meta http-equiv='refresh' content='0; URL=https://tenhou.net/5/?tw=2#json="
>
%tmpfile%
<
NUL
type
%
1
>>
%tmpfile%
<
NUL
echo
|
set
/p
=
"'></head>"
>>
%tmpfile%
start
%tmpfile%
::you could wait and delete the file..
::pause
::del %tmpfile%
extension/downloadlogs/viewlog.sh
0 → 100644
View file @
2915981e
#!/bin/bash
if
[[
""
==
$1
]]
;
then
echo
"
${
0
}
log.json"
echo
"opens your log with tenhou.net/5"
exit
1
fi
url
=
"https://tenhou.net/5/?tw=2#json=""
$(
cat
$1
)
"
case
"
$OSTYPE
"
in
linux
*
)
xdg-open
"
${
url
}
"
;;
darwin
*
)
open
"
${
url
}
"
;;
cygwin
*
)
cygstart
"
${
url
}
"
;;
*
)
open
"
${
url
}
"
||
start
"
${
url
}
"
;;
esac
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment