Compare commits

..

6 Commits

Author SHA1 Message Date
DraVee 58f101f51d ... 2026-05-10 04:10:04 +02:00
DraVee ea3aaa1d8a [file_sys] Check for Mods the same way on SDMC and non-SDMC
* fix getting cheats from SDMC (allow usage with aio-switch-updater)
2026-05-10 04:10:04 +02:00
DraVee 50c53a7614 chore: minor changes 2026-05-10 04:10:04 +02:00
DraVee 38bc172acf [dmnt] Fix CheatCompatibility
* now it follows Atmosphere system to only enable cheat if cheat_name.txt = build_id
* also deduplicate active update

Signed-off-by: DraVee <chimera@dravee.dev>
2026-05-10 04:10:04 +02:00
xbzk 27036f376a [fixup] fix problem introduced on fix merge conflicts
Signed-off-by: xbzk <xbzk@eden-emu.dev>
2026-05-10 04:10:04 +02:00
Maufeat 206254035b [cheat] add dmnt, indiviual cheats, etc.
* DraVee: fixed rebase conflicts
* xbzk: fixed Android after upstream changes
2026-05-10 04:10:04 +02:00
84 changed files with 3119 additions and 1819 deletions
+74 -133
View File
@@ -6,8 +6,8 @@
viewBox="0 0 512 512"
version="1.1"
id="svg7"
sodipodi:docname="1stanni.svg"
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
sodipodi:docname="base.svg.2026_01_12_14_43_47.0.svg"
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
inkscape:export-filename="base.svg.2026_01_12_14_43_47.0.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
@@ -19,34 +19,36 @@
<defs
id="defs7">
<linearGradient
id="linearGradient34"
id="linearGradient1"
inkscape:collect="always">
<stop
style="stop-color:#ffd700;stop-opacity:1;"
style="stop-color:#ff2e88;stop-opacity:0.5;"
offset="0"
id="stop34" />
id="stop3" />
<stop
style="stop-color:#ffd700;stop-opacity:0.48031053;"
offset="1"
id="stop35" />
style="stop-color:#bf42f6;stop-opacity:0.5;"
offset="0.44631511"
id="stop4" />
<stop
style="stop-color:#5da5ed;stop-opacity:0.5;"
offset="0.90088946"
id="stop2" />
</linearGradient>
<rect
x="20.999999"
y="287.30493"
width="487.07235"
height="134.69506"
id="rect22" />
<linearGradient
id="linearGradient21"
id="linearGradient138"
inkscape:collect="always">
<stop
style="stop-color:#3a0057;stop-opacity:1;"
style="stop-color:#ff2e88;stop-opacity:1;"
offset="0"
id="stop21" />
id="stop152" />
<stop
style="stop-color:#830091;stop-opacity:1;"
offset="1"
id="stop22" />
style="stop-color:#bf42f6;stop-opacity:1;"
offset="0.44971901"
id="stop137" />
<stop
style="stop-color:#5da5ed;stop-opacity:1;"
offset="0.89793283"
id="stop138" />
</linearGradient>
<linearGradient
id="swatch37"
@@ -114,6 +116,33 @@
width="521.34025"
height="248.94868"
id="rect24" />
<linearGradient
id="linearGradient11"
inkscape:collect="always">
<stop
style="stop-color:#ff2e88;stop-opacity:1;"
offset="0"
id="stop11" />
<stop
style="stop-color:#bf42f6;stop-opacity:1;"
offset="0.44971901"
id="stop154" />
<stop
style="stop-color:#5da5ed;stop-opacity:1;"
offset="0.89793283"
id="stop12" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient138"
id="linearGradient6"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.118028,0,0,1.116699,-46.314723,-42.388667)"
x1="270.39996"
y1="40.000019"
x2="270.39996"
y2="494.39996"
spreadMethod="pad" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath18">
@@ -136,6 +165,16 @@
inkscape:label="Circle"
r="191.89999" />
</clipPath>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient11"
id="linearGradient27"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-6.9401139e-5,-2.8678628)"
x1="256.00012"
y1="102.94693"
x2="256.00012"
y2="409.05307" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath128">
@@ -148,106 +187,14 @@
</clipPath>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient21"
id="linearGradient22"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="256"
y1="0"
y1="64"
x2="256"
y2="512"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient34"
id="linearGradient35"
x1="256"
y1="-0.048701428"
x2="256"
y2="512.04932"
gradientUnits="userSpaceOnUse" />
<filter
inkscape:label="Glowing Bubble"
inkscape:menu="Ridges"
inkscape:menu-tooltip="Bubble effect with refraction and glow"
x="-0.19420711"
y="-0.11239541"
width="1.3884142"
height="1.2247908"
style="color-interpolation-filters:sRGB;"
id="filter61">
<feGaussianBlur
stdDeviation="1"
result="result1"
id="feGaussianBlur56" />
<feGaussianBlur
stdDeviation="10"
result="result6"
in="result1"
id="feGaussianBlur57" />
<feComposite
operator="atop"
in="result6"
in2="result1"
result="result8"
id="feComposite57" />
<feComposite
operator="xor"
result="fbSourceGraphic"
in="result6"
in2="result8"
id="feComposite58" />
<feColorMatrix
result="fbSourceGraphicAlpha"
in="fbSourceGraphic"
values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 2 0 "
id="feColorMatrix58" />
<feGaussianBlur
result="result0"
in="fbSourceGraphicAlpha"
stdDeviation="1"
id="feGaussianBlur58" />
<feSpecularLighting
specularExponent="35"
specularConstant="1.5"
surfaceScale="-2"
lighting-color="rgb(255,255,255)"
result="result1"
in="result0"
id="feSpecularLighting58">
<feDistantLight
azimuth="230"
elevation="60"
id="feDistantLight58" />
</feSpecularLighting>
<feComposite
operator="in"
result="result2"
in="result1"
in2="fbSourceGraphicAlpha"
id="feComposite59" />
<feComposite
k3="1.2"
k2="1.1"
operator="arithmetic"
result="result4"
in="fbSourceGraphic"
in2="result2"
id="feComposite60" />
<feGaussianBlur
result="result80"
in="result4"
stdDeviation="0.5"
id="feGaussianBlur60" />
<feComposite
operator="atop"
in="result9"
in2="result80"
result="result91"
id="feComposite61" />
<feBlend
mode="multiply"
in2="result91"
id="feBlend61" />
</filter>
y2="448"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3229974,0,0,1.3214002,-82.687336,-82.290326)" />
</defs>
<sodipodi:namedview
id="namedview7"
@@ -258,29 +205,23 @@
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1"
inkscape:cx="213.49999"
inkscape:cy="248.99999"
inkscape:window-width="1600"
inkscape:window-height="849"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:zoom="1.4142136"
inkscape:cx="261.62951"
inkscape:cy="230.87036"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="1080"
inkscape:window-y="351"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<circle
style="fill:url(#linearGradient22);fill-opacity:1;stroke:none;stroke-width:8"
id="path21"
cx="256"
cy="256"
r="256" />
<path
id="path8-7"
style="display:inline;mix-blend-mode:normal;fill:url(#linearGradient35);fill-opacity:1;fill-rule:nonzero;stroke:#320081;stroke-width:4.067;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
style="display:inline;mix-blend-mode:multiply;fill:url(#linearGradient6);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2);stroke-width:3.9666;stroke-dasharray:none;stroke-opacity:0.566238;paint-order:stroke fill markers"
inkscape:label="Circle"
d="M 256,2.2792898 A 254.0155,253.71401 0 0 0 150.68475,25.115202 c 19.54414,1.070775 38.74692,5.250294 51.56848,11.647658 14.14361,7.056691 28.63804,19.185961 39.4212,29.347551 h 40.60981 c 1.03847,-0.68139 2.10297,-1.36938 3.1938,-2.05957 5.45602,-15.78533 14.79164,-43.183497 19.49612,-57.0097682 A 254.0155,253.71401 0 0 0 256,2.2792898 Z m 61.57106,7.567234 -18.26098,46.1544672 c 7.79702,-4.13918 16.35655,-7.87447 25.20671,-10.87081 23.1229,-7.828433 43.96931,-10.170904 54.94058,-10.868226 A 254.0155,253.71401 0 0 0 317.57106,9.8465238 Z m 65.39277,26.4001532 c -9.68256,4.806644 -33.05532,16.642034 -55.68217,29.863734 H 424.4677 A 254.0155,253.71401 0 0 0 382.96383,36.246677 Z M 113.90698,45.690231 A 254.0155,253.71401 0 0 0 87.532302,66.110411 H 194.2739 c -1.47402,-0.80231 -2.35141,-1.25949 -2.35141,-1.25949 l 10.4496,-11.83348 -38.40568,7.01234 c 0,1e-5 -12.21537,-4.60266 -40.17313,-12.27223 -3.45336,-0.94731 -6.75329,-1.61824 -9.8863,-2.06732 z m -36.803618,30.18635 a 254.0155,253.71401 0 0 0 -34.88372,43.090929 h 59.976738 c 18.11461,-12.04145 40.14252,-22.882149 62.31266,-24.534159 52.93006,-3.9444 70.16538,1.86342 70.16538,1.86342 0,0 -4.612,-4.8206 -14.51938,-13.36656 -2.72366,-2.34942 -6.0844,-4.77373 -9.52455,-7.05363 z m 174.472868,0 c 4.57322,4.7186 7.29716,7.83565 7.29716,7.83565 0,0 3.53501,-3.18484 9.62532,-7.83565 z m 60.27649,0 c -21.56573,15.45339 -25.4703,27.979669 -25.4703,27.979669 0,0 54.83326,-19.215729 100.70543,-0.31228 11.63986,4.79661 21.58481,10.13159 29.94832,15.42354 h 52.74419 A 254.0155,253.71401 0 0 0 434.89664,75.876581 Z M 36.250648,128.73367 A 254.0155,253.71401 0 0 0 16.372095,171.82459 H 147.45478 c 1.45695,-2.5815 3.06539,-5.08648 4.83979,-7.48982 14.23694,-19.28301 27.92088,-30.0088 36.86047,-35.6011 h -30.25323 c -5.87346,0.93472 -12.04945,1.99094 -18.28166,3.16937 -30.12936,5.69727 -81.157618,22.78945 -81.157618,22.78945 0,0 11.47125,-12.39249 29.11369,-25.95882 z m 265.630492,0 c 33.48676,11.2434 52.42799,26.78443 62.7752,43.09092 h 130.97157 a 254.0155,253.71401 0 0 0 -19.87856,-43.09092 h -44.81136 c 14.85233,11.5863 21.59948,20.9854 21.59948,20.9854 0,0 -33.5226,-12.37087 -66.0646,-20.9854 z m -45.96641,16.27007 c -1.00419,0.0106 -10.12705,0.72026 -44.98966,20.64729 -3.12132,1.78406 -6.25434,3.86182 -9.37468,6.17356 h 41.81911 c 7.17181,-17.34774 12.64083,-26.82085 12.64083,-26.82085 0,0 -0.0287,-7.1e-4 -0.0957,0 z m 14.18088,0.0465 c 0,0 -3.31228,9.32762 -7.30492,26.77438 h 51.78554 C 287.6577,146.14158 270.09561,145.0502 270.09561,145.0502 Z M 13.152456,181.59075 A 254.0155,253.71401 0 0 0 3.927651,224.68167 H 134.1447 c 0.56161,-12.72411 2.67825,-28.50188 8.61499,-43.09092 z m 176.661504,0 c -14.27121,13.10564 -27.60733,29.58761 -37.56073,43.09092 h 73.3721 c 4.47018,-16.79061 9.35068,-31.26371 13.86562,-43.09092 z m 70.85787,0 c -2.41384,11.76417 -4.9032,26.20707 -6.94831,43.09092 H 360.4832 c -8.32133,-10.88917 -20.66988,-26.17008 -36.35141,-43.09092 z m 109.17313,0 c 6.63611,15.24089 6.92441,30.5373 5.57882,43.09092 h 132.64857 a 254.0155,253.71401 0 0 0 -9.22481,-43.09092 z M 2.90181,234.44783 A 254.0155,253.71401 0 0 0 1.984498,255.9933 254.0155,253.71401 0 0 0 2.90181,277.53876 h 211.89923 c 2.25762,-15.52555 5.14325,-29.93448 8.3385,-43.09093 h -77.8863 c -6.46396,9.27617 -10.33076,15.56549 -10.33076,15.56549 0,0 -0.82623,-6.14945 -0.9354,-15.56549 z m 249.72093,0 c -1.3692,13.09684 -2.4456,27.49209 -3.02068,43.09093 h 259.49613 a 254.0155,253.71401 0 0 0 0.91731,-21.54546 254.0155,253.71401 0 0 0 -0.91731,-21.54547 H 374.02584 c -0.445,2.5469 -0.90878,4.89768 -1.32817,7.01751 0,0 -1.69726,-2.53821 -4.94056,-7.01751 z M 3.927651,287.30493 a 254.0155,253.71401 0 0 0 9.224805,43.09091 H 214.04393 c -1.29238,-15.40742 -1.57503,-30.04388 -0.41861,-43.09091 z m 245.385009,0 c -0.30355,13.54349 -0.22032,27.92598 0.36951,43.09091 h 249.16537 a 254.0155,253.71401 0 0 0 9.22481,-43.09091 z M 16.369511,340.16201 a 254.0155,253.71401 0 0 0 19.878554,43.09091 H 221.4677 c -2.69781,-14.4523 -4.96108,-29.01285 -6.4832,-43.09091 z m 233.842379,0 c 1.15864,15.47765 3.81286,29.83979 7.51679,43.09091 h 218.02325 a 254.0155,253.71401 0 0 0 19.87856,-43.09091 z M 42.217052,393.01909 a 254.0155,253.71401 0 0 0 34.88372,43.09093 H 233.09561 c -3.40902,-13.67281 -6.76794,-28.2531 -9.73902,-43.09093 z m 218.490958,0 c 5.34985,16.15926 12.22007,30.51982 19.68733,43.09093 h 154.50389 a 254.0155,253.71401 0 0 0 34.88371,-43.09093 z M 87.529722,445.87618 a 254.0155,253.71401 0 0 0 166.229968,63.8208 c -3.67805,-12.0825 -10.85464,-35.49828 -18.18088,-63.8208 z m 199.010328,0 c 17.5887,26.43772 36.99259,43.60598 47.33592,51.61309 a 254.0155,253.71401 0 0 0 90.59431,-51.61309 z" />
<path
id="path27"
style="display:inline;mix-blend-mode:multiply;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
style="display:inline;mix-blend-mode:multiply;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient27);stroke-width:3;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
d="m 318.98012,441.7375 c -9.87518,-6.73978 -64.39137,-49.0272 -67.68975,-127.81978 -3.69298,-88.21893 15.36468,-141.91029 15.36468,-141.91029 0,0 16.00378,0.99513 39.80316,26.53195 23.79939,25.53753 37.74965,46.43102 37.74965,46.43102 3.91262,-19.79992 12.84563,-66.32402 -60.72865,-87.55523 0,0 12.82326,-5.38883 39.3925,-3.81382 26.56907,1.57572 81.6822,21.93799 81.6822,21.93799 0,0 -14.79766,-20.63773 -49.47063,-34.94295 -34.67291,-14.30533 -76.1182,0.23644 -76.1182,0.23644 0,0 3.86959,-12.43127 27.22669,-26.38478 23.35718,-13.9537 49.27409,-26.501533 49.27409,-26.501533 0,0 -21.97854,-0.26548 -47.67725,8.44535 -6.68948,2.267506 -13.15863,5.094213 -19.05208,8.226563 l 16.05803,-40.634103 -4.4617,-1.89059 -5.1305,-0.95965 c 0,0 -11.24072,33.12428 -16.92051,49.576513 -12.13137,7.68489 -20.11005,14.87735 -20.11005,14.87735 0,0 -21.90573,-25.09227 -42.79668,-35.527803 -26.03412,-13.00525 -86.88249,-13.90359 -94.0044,10.401173 0,0 13.56804,-7.884703 34.70032,-2.080917 21.13214,5.803997 30.3644,9.287307 30.3644,9.287307 l 29.02989,-5.30681 -7.89811,8.95527 c 0,0 13.8496,7.21324 21.33822,13.68063 7.48859,6.46722 10.9757,10.11472 10.9757,10.11472 0,0 -13.02739,-4.39388 -53.03507,-1.40893 -40.00771,2.98473 -79.40016,45.60209 -79.40016,45.60209 0,0 38.57037,-12.93531 61.34393,-17.24677 22.77354,-4.31126 44.52166,-6.46757 44.52166,-6.46757 0,0 -17.23298,5.97003 -35.69792,31.00932 -18.46522,25.03987 -13.13146,64.83866 -13.13146,64.83866 0,0 29.33874,-47.7577 57.44675,-63.84249 28.10798,-16.08527 34.0799,-15.6238 34.0799,-15.6238 0,0 -22.56785,39.13486 -31.39017,101.98268 -8.03005,57.2039 26.77689,163.75449 31.1572,178.89699"
sodipodi:nodetypes="cscsccscscscsccccccscscccscscscscscsc"
inkscape:label="MainOutline"

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 256 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

After

Width:  |  Height:  |  Size: 335 KiB

-289
View File
@@ -1,289 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="512"
height="512"
fill="none"
viewBox="0 0 512 512"
version="1.1"
id="svg7"
sodipodi:docname="1stanni.svg"
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
inkscape:export-filename="base.svg.2026_01_12_14_43_47.0.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs7">
<linearGradient
id="linearGradient34"
inkscape:collect="always">
<stop
style="stop-color:#ffd700;stop-opacity:1;"
offset="0"
id="stop34" />
<stop
style="stop-color:#ffd700;stop-opacity:0.48031053;"
offset="1"
id="stop35" />
</linearGradient>
<rect
x="20.999999"
y="287.30493"
width="487.07235"
height="134.69506"
id="rect22" />
<linearGradient
id="linearGradient21"
inkscape:collect="always">
<stop
style="stop-color:#3a0057;stop-opacity:1;"
offset="0"
id="stop21" />
<stop
style="stop-color:#830091;stop-opacity:1;"
offset="1"
id="stop22" />
</linearGradient>
<linearGradient
id="swatch37"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop37" />
</linearGradient>
<linearGradient
id="swatch28"
inkscape:swatch="solid">
<stop
style="stop-color:#252525;stop-opacity:1;"
offset="0"
id="stop28" />
</linearGradient>
<linearGradient
id="swatch27"
inkscape:swatch="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop27" />
</linearGradient>
<linearGradient
id="swatch15"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop16" />
</linearGradient>
<linearGradient
id="linearGradient14"
inkscape:swatch="gradient">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop14" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop15" />
</linearGradient>
<linearGradient
id="swatch9"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop10" />
</linearGradient>
<linearGradient
id="swatch8"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop9" />
</linearGradient>
<rect
x="22.627417"
y="402.76802"
width="521.34025"
height="248.94868"
id="rect24" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath18">
<circle
style="opacity:1;mix-blend-mode:normal;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.8382;stroke-opacity:0.566238;paint-order:stroke fill markers"
id="circle18"
cx="-246.8315"
cy="246.8338"
inkscape:label="Circle"
r="191.89999" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath22">
<circle
style="opacity:1;mix-blend-mode:normal;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.8382;stroke-opacity:0.566238;paint-order:stroke fill markers"
id="circle22"
cx="256"
cy="256"
inkscape:label="Circle"
r="191.89999" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath128">
<circle
style="fill:none;fill-opacity:1;stroke:#03ffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
id="circle128"
cx="256"
cy="256"
r="192" />
</clipPath>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient21"
id="linearGradient22"
x1="256"
y1="0"
x2="256"
y2="512"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient34"
id="linearGradient35"
x1="256"
y1="-0.048701428"
x2="256"
y2="512.04932"
gradientUnits="userSpaceOnUse" />
<filter
inkscape:label="Glowing Bubble"
inkscape:menu="Ridges"
inkscape:menu-tooltip="Bubble effect with refraction and glow"
x="-0.19420711"
y="-0.11239541"
width="1.3884142"
height="1.2247908"
style="color-interpolation-filters:sRGB;"
id="filter61">
<feGaussianBlur
stdDeviation="1"
result="result1"
id="feGaussianBlur56" />
<feGaussianBlur
stdDeviation="10"
result="result6"
in="result1"
id="feGaussianBlur57" />
<feComposite
operator="atop"
in="result6"
in2="result1"
result="result8"
id="feComposite57" />
<feComposite
operator="xor"
result="fbSourceGraphic"
in="result6"
in2="result8"
id="feComposite58" />
<feColorMatrix
result="fbSourceGraphicAlpha"
in="fbSourceGraphic"
values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 2 0 "
id="feColorMatrix58" />
<feGaussianBlur
result="result0"
in="fbSourceGraphicAlpha"
stdDeviation="1"
id="feGaussianBlur58" />
<feSpecularLighting
specularExponent="35"
specularConstant="1.5"
surfaceScale="-2"
lighting-color="rgb(255,255,255)"
result="result1"
in="result0"
id="feSpecularLighting58">
<feDistantLight
azimuth="230"
elevation="60"
id="feDistantLight58" />
</feSpecularLighting>
<feComposite
operator="in"
result="result2"
in="result1"
in2="fbSourceGraphicAlpha"
id="feComposite59" />
<feComposite
k3="1.2"
k2="1.1"
operator="arithmetic"
result="result4"
in="fbSourceGraphic"
in2="result2"
id="feComposite60" />
<feGaussianBlur
result="result80"
in="result4"
stdDeviation="0.5"
id="feGaussianBlur60" />
<feComposite
operator="atop"
in="result9"
in2="result80"
result="result91"
id="feComposite61" />
<feBlend
mode="multiply"
in2="result91"
id="feBlend61" />
</filter>
</defs>
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1"
inkscape:cx="213.49999"
inkscape:cy="248.99999"
inkscape:window-width="1600"
inkscape:window-height="849"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<circle
style="fill:url(#linearGradient22);fill-opacity:1;stroke:none;stroke-width:8"
id="path21"
cx="256"
cy="256"
r="256" />
<path
id="path8-7"
style="display:inline;mix-blend-mode:normal;fill:url(#linearGradient35);fill-opacity:1;fill-rule:nonzero;stroke:#320081;stroke-width:4.067;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
inkscape:label="Circle"
d="M 256,2.2792898 A 254.0155,253.71401 0 0 0 150.68475,25.115202 c 19.54414,1.070775 38.74692,5.250294 51.56848,11.647658 14.14361,7.056691 28.63804,19.185961 39.4212,29.347551 h 40.60981 c 1.03847,-0.68139 2.10297,-1.36938 3.1938,-2.05957 5.45602,-15.78533 14.79164,-43.183497 19.49612,-57.0097682 A 254.0155,253.71401 0 0 0 256,2.2792898 Z m 61.57106,7.567234 -18.26098,46.1544672 c 7.79702,-4.13918 16.35655,-7.87447 25.20671,-10.87081 23.1229,-7.828433 43.96931,-10.170904 54.94058,-10.868226 A 254.0155,253.71401 0 0 0 317.57106,9.8465238 Z m 65.39277,26.4001532 c -9.68256,4.806644 -33.05532,16.642034 -55.68217,29.863734 H 424.4677 A 254.0155,253.71401 0 0 0 382.96383,36.246677 Z M 113.90698,45.690231 A 254.0155,253.71401 0 0 0 87.532302,66.110411 H 194.2739 c -1.47402,-0.80231 -2.35141,-1.25949 -2.35141,-1.25949 l 10.4496,-11.83348 -38.40568,7.01234 c 0,1e-5 -12.21537,-4.60266 -40.17313,-12.27223 -3.45336,-0.94731 -6.75329,-1.61824 -9.8863,-2.06732 z m -36.803618,30.18635 a 254.0155,253.71401 0 0 0 -34.88372,43.090929 h 59.976738 c 18.11461,-12.04145 40.14252,-22.882149 62.31266,-24.534159 52.93006,-3.9444 70.16538,1.86342 70.16538,1.86342 0,0 -4.612,-4.8206 -14.51938,-13.36656 -2.72366,-2.34942 -6.0844,-4.77373 -9.52455,-7.05363 z m 174.472868,0 c 4.57322,4.7186 7.29716,7.83565 7.29716,7.83565 0,0 3.53501,-3.18484 9.62532,-7.83565 z m 60.27649,0 c -21.56573,15.45339 -25.4703,27.979669 -25.4703,27.979669 0,0 54.83326,-19.215729 100.70543,-0.31228 11.63986,4.79661 21.58481,10.13159 29.94832,15.42354 h 52.74419 A 254.0155,253.71401 0 0 0 434.89664,75.876581 Z M 36.250648,128.73367 A 254.0155,253.71401 0 0 0 16.372095,171.82459 H 147.45478 c 1.45695,-2.5815 3.06539,-5.08648 4.83979,-7.48982 14.23694,-19.28301 27.92088,-30.0088 36.86047,-35.6011 h -30.25323 c -5.87346,0.93472 -12.04945,1.99094 -18.28166,3.16937 -30.12936,5.69727 -81.157618,22.78945 -81.157618,22.78945 0,0 11.47125,-12.39249 29.11369,-25.95882 z m 265.630492,0 c 33.48676,11.2434 52.42799,26.78443 62.7752,43.09092 h 130.97157 a 254.0155,253.71401 0 0 0 -19.87856,-43.09092 h -44.81136 c 14.85233,11.5863 21.59948,20.9854 21.59948,20.9854 0,0 -33.5226,-12.37087 -66.0646,-20.9854 z m -45.96641,16.27007 c -1.00419,0.0106 -10.12705,0.72026 -44.98966,20.64729 -3.12132,1.78406 -6.25434,3.86182 -9.37468,6.17356 h 41.81911 c 7.17181,-17.34774 12.64083,-26.82085 12.64083,-26.82085 0,0 -0.0287,-7.1e-4 -0.0957,0 z m 14.18088,0.0465 c 0,0 -3.31228,9.32762 -7.30492,26.77438 h 51.78554 C 287.6577,146.14158 270.09561,145.0502 270.09561,145.0502 Z M 13.152456,181.59075 A 254.0155,253.71401 0 0 0 3.927651,224.68167 H 134.1447 c 0.56161,-12.72411 2.67825,-28.50188 8.61499,-43.09092 z m 176.661504,0 c -14.27121,13.10564 -27.60733,29.58761 -37.56073,43.09092 h 73.3721 c 4.47018,-16.79061 9.35068,-31.26371 13.86562,-43.09092 z m 70.85787,0 c -2.41384,11.76417 -4.9032,26.20707 -6.94831,43.09092 H 360.4832 c -8.32133,-10.88917 -20.66988,-26.17008 -36.35141,-43.09092 z m 109.17313,0 c 6.63611,15.24089 6.92441,30.5373 5.57882,43.09092 h 132.64857 a 254.0155,253.71401 0 0 0 -9.22481,-43.09092 z M 2.90181,234.44783 A 254.0155,253.71401 0 0 0 1.984498,255.9933 254.0155,253.71401 0 0 0 2.90181,277.53876 h 211.89923 c 2.25762,-15.52555 5.14325,-29.93448 8.3385,-43.09093 h -77.8863 c -6.46396,9.27617 -10.33076,15.56549 -10.33076,15.56549 0,0 -0.82623,-6.14945 -0.9354,-15.56549 z m 249.72093,0 c -1.3692,13.09684 -2.4456,27.49209 -3.02068,43.09093 h 259.49613 a 254.0155,253.71401 0 0 0 0.91731,-21.54546 254.0155,253.71401 0 0 0 -0.91731,-21.54547 H 374.02584 c -0.445,2.5469 -0.90878,4.89768 -1.32817,7.01751 0,0 -1.69726,-2.53821 -4.94056,-7.01751 z M 3.927651,287.30493 a 254.0155,253.71401 0 0 0 9.224805,43.09091 H 214.04393 c -1.29238,-15.40742 -1.57503,-30.04388 -0.41861,-43.09091 z m 245.385009,0 c -0.30355,13.54349 -0.22032,27.92598 0.36951,43.09091 h 249.16537 a 254.0155,253.71401 0 0 0 9.22481,-43.09091 z M 16.369511,340.16201 a 254.0155,253.71401 0 0 0 19.878554,43.09091 H 221.4677 c -2.69781,-14.4523 -4.96108,-29.01285 -6.4832,-43.09091 z m 233.842379,0 c 1.15864,15.47765 3.81286,29.83979 7.51679,43.09091 h 218.02325 a 254.0155,253.71401 0 0 0 19.87856,-43.09091 z M 42.217052,393.01909 a 254.0155,253.71401 0 0 0 34.88372,43.09093 H 233.09561 c -3.40902,-13.67281 -6.76794,-28.2531 -9.73902,-43.09093 z m 218.490958,0 c 5.34985,16.15926 12.22007,30.51982 19.68733,43.09093 h 154.50389 a 254.0155,253.71401 0 0 0 34.88371,-43.09093 z M 87.529722,445.87618 a 254.0155,253.71401 0 0 0 166.229968,63.8208 c -3.67805,-12.0825 -10.85464,-35.49828 -18.18088,-63.8208 z m 199.010328,0 c 17.5887,26.43772 36.99259,43.60598 47.33592,51.61309 a 254.0155,253.71401 0 0 0 90.59431,-51.61309 z" />
<path
id="path27"
style="display:inline;mix-blend-mode:multiply;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
d="m 318.98012,441.7375 c -9.87518,-6.73978 -64.39137,-49.0272 -67.68975,-127.81978 -3.69298,-88.21893 15.36468,-141.91029 15.36468,-141.91029 0,0 16.00378,0.99513 39.80316,26.53195 23.79939,25.53753 37.74965,46.43102 37.74965,46.43102 3.91262,-19.79992 12.84563,-66.32402 -60.72865,-87.55523 0,0 12.82326,-5.38883 39.3925,-3.81382 26.56907,1.57572 81.6822,21.93799 81.6822,21.93799 0,0 -14.79766,-20.63773 -49.47063,-34.94295 -34.67291,-14.30533 -76.1182,0.23644 -76.1182,0.23644 0,0 3.86959,-12.43127 27.22669,-26.38478 23.35718,-13.9537 49.27409,-26.501533 49.27409,-26.501533 0,0 -21.97854,-0.26548 -47.67725,8.44535 -6.68948,2.267506 -13.15863,5.094213 -19.05208,8.226563 l 16.05803,-40.634103 -4.4617,-1.89059 -5.1305,-0.95965 c 0,0 -11.24072,33.12428 -16.92051,49.576513 -12.13137,7.68489 -20.11005,14.87735 -20.11005,14.87735 0,0 -21.90573,-25.09227 -42.79668,-35.527803 -26.03412,-13.00525 -86.88249,-13.90359 -94.0044,10.401173 0,0 13.56804,-7.884703 34.70032,-2.080917 21.13214,5.803997 30.3644,9.287307 30.3644,9.287307 l 29.02989,-5.30681 -7.89811,8.95527 c 0,0 13.8496,7.21324 21.33822,13.68063 7.48859,6.46722 10.9757,10.11472 10.9757,10.11472 0,0 -13.02739,-4.39388 -53.03507,-1.40893 -40.00771,2.98473 -79.40016,45.60209 -79.40016,45.60209 0,0 38.57037,-12.93531 61.34393,-17.24677 22.77354,-4.31126 44.52166,-6.46757 44.52166,-6.46757 0,0 -17.23298,5.97003 -35.69792,31.00932 -18.46522,25.03987 -13.13146,64.83866 -13.13146,64.83866 0,0 29.33874,-47.7577 57.44675,-63.84249 28.10798,-16.08527 34.0799,-15.6238 34.0799,-15.6238 0,0 -22.56785,39.13486 -31.39017,101.98268 -8.03005,57.2039 26.77689,163.75449 31.1572,178.89699"
sodipodi:nodetypes="cscsccscscscsccccccscscccscscscscscsc"
inkscape:label="MainOutline"
clip-path="url(#clipPath128)"
transform="matrix(1.3229974,0,0,1.3214002,-82.687282,-82.278451)" />
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

-1
View File
@@ -1 +0,0 @@
#ffd700
+78 -96
View File
@@ -119,12 +119,12 @@ li.checked::marker { content: &quot;\2612&quot;; }
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="327"/>
<source>%1 has been banned</source>
<translation>%1 foi banido</translation>
<translation>%1 foi banido(a)</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="330"/>
<source>%1 has been unbanned</source>
<translation>%1 foi desbanido</translation>
<translation>%1 foi desbanido(a)</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="446"/>
@@ -782,14 +782,13 @@ Desabiltar essa opção só serve para propósitos de depuração.</translation>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="168"/>
<source>Use asynchronous GPU emulation</source>
<translation>Usar emulação assíncrona de GPU</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="169"/>
<source>Uses an extra CPU thread for rendering.
This option should always remain enabled.</source>
<translation>Usa uma thread de CPU extra para renderização.
Esta opção deve estar sempre habilitada.</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="170"/>
@@ -2802,32 +2801,32 @@ Quando um programa tenta abrir o applet, ele é imediatamente fechado.</translat
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="526"/>
<source>Bitmask for quick development toggles</source>
<translation>Bitmask para rápida alternativa de desenvolvimento</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="529"/>
<source>Set debug knobs (bitmask)</source>
<translation>Definir knobs de depuração (bitmask)</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="532"/>
<source>16-bit debug knob set for quick development toggles</source>
<translation>Knob de depuração 16-bit definido para rápida alternativa de desenvolvimento</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="535"/>
<source> (bitmask)</source>
<translation>(bitmask)</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="538"/>
<source>Debug Knobs: </source>
<translation>Knobs de depuração:</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="572"/>
<source>Unit Serial:</source>
<translation>Número de série:</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="601"/>
@@ -2842,7 +2841,7 @@ Quando um programa tenta abrir o applet, ele é imediatamente fechado.</translat
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="611"/>
<source>Flush log output on each line</source>
<translation>Liberar o log a cada linha</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="618"/>
@@ -2857,7 +2856,7 @@ Quando um programa tenta abrir o applet, ele é imediatamente fechado.</translat
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="632"/>
<source>Censor username in logs</source>
<translation>Censurar nome de usuário nos logs</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="668"/>
@@ -2961,7 +2960,7 @@ Quando um programa tenta abrir o applet, ele é imediatamente fechado.</translat
<message>
<location filename="../../src/yuzu/configuration/configure_dialog.cpp" line="77"/>
<source>GraphicsExtra</source>
<translation>Gráficos Extra</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_dialog.cpp" line="78"/>
@@ -3041,7 +3040,7 @@ Quando um programa tenta abrir o applet, ele é imediatamente fechado.</translat
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="65"/>
<source>Save Data</source>
<translation>Salvar dados</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="101"/>
@@ -3117,7 +3116,7 @@ Quando um programa tenta abrir o applet, ele é imediatamente fechado.</translat
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="111"/>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="159"/>
<source>Select Save Data Directory...</source>
<translation>Selecionar o diretório de salvamento</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="114"/>
@@ -3137,22 +3136,22 @@ Quando um programa tenta abrir o applet, ele é imediatamente fechado.</translat
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="148"/>
<source>Save Data Directory</source>
<translation>Salvar diretório de salvamento</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="149"/>
<source>Choose an action for the save data directory:</source>
<translation>Escolha uma ação para o diretório de salvamento:</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="151"/>
<source>Set Custom Path</source>
<translation>Definir caminho personalizado</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="152"/>
<source>Reset to NAND</source>
<translation>Resetar a NAND</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="208"/>
@@ -3163,13 +3162,7 @@ New: %2
Would you like to migrate saves from the old location?
WARNING: This will overwrite any conflicting saves in the new location!</source>
<translation>Salvamento existe tanto no antigo quanto nas novas locações.
Antigo: %1
Novo: %2
Você gostaria de migrar seu salvamento do antigo local?
ATENÇÃO: Isso irá sobrescrever qualquer salvamento conflitante no novo local!</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="216"/>
@@ -3177,57 +3170,51 @@ ATENÇÃO: Isso irá sobrescrever qualquer salvamento conflitante no novo local!
From: %1
To: %2</source>
<translation>Você gostaria de migrar seus salvamentos para o novo local?
De: %1
Para: %2</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="224"/>
<source>Migrate Save Data</source>
<translation>Migrar salvamento</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="231"/>
<source>Migrating save data...</source>
<translation>Migrando salvamento...</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="231"/>
<source>Cancel</source>
<translation>Cancelar</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="239"/>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="252"/>
<source>Migration Failed</source>
<translation>Migração Falhou</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="240"/>
<source>Failed to create destination directory.</source>
<translation>Falha ao criar diretório de destino.</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="253"/>
<source>Failed to migrate save data:
%1</source>
<translation>Falha ao migrar salvamento:
1%</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="258"/>
<source>Migration Complete</source>
<translation>Migração Completa</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="259"/>
<source>Save data has been migrated successfully.
Would you like to delete the old save data?</source>
<translation>Salvamento foi migrado com sucesso
Gostaria de deletar o salvamento antigo?</translation>
<translation type="unfinished"/>
</message>
</context>
<context>
@@ -3376,27 +3363,27 @@ Gostaria de deletar o salvamento antigo?</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.ui" line="14"/>
<source>Form</source>
<translation>Formar</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.ui" line="17"/>
<source>Extras</source>
<translation>Extras</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.ui" line="23"/>
<source>Hacks</source>
<translation>Hacks</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.ui" line="29"/>
<source>Changing these options from their default may cause issues. Novitii cavete!</source>
<translation>Alterar essas opções de seus padrões pode causar problemas. È una trappola, Bino!</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.ui" line="62"/>
<source>Vulkan Extensions</source>
<translation>Extensões Vulkan</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.cpp" line="49"/>
@@ -3407,7 +3394,7 @@ Gostaria de deletar o salvamento antigo?</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.cpp" line="68"/>
<source>Extended Dynamic State is disabled on macOS due to MoltenVK compatibility issues that cause black screens.</source>
<translation>Estado Dinâmico Extendido é desativado no MacOS, devido a problemas de compatibilidade com o MoltenVK, que causa telas pretas.</translation>
<translation type="unfinished"/>
</message>
</context>
<context>
@@ -4722,7 +4709,7 @@ Os valores atuais são %1% e %2% respectivamente.</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_network.ui" line="41"/>
<source>Enable Airplane Mode</source>
<translation>Habilitar Modo Avião</translation>
<translation type="unfinished"/>
</message>
</context>
<context>
@@ -4805,7 +4792,7 @@ Os valores atuais são %1% e %2% respectivamente.</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.cpp" line="82"/>
<source>Ext. Graphics</source>
<translation>Gráficos Ext.</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.cpp" line="83"/>
@@ -4820,7 +4807,7 @@ Os valores atuais são %1% e %2% respectivamente.</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.cpp" line="85"/>
<source>Network</source>
<translation>Rede</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.cpp" line="86"/>
@@ -4848,12 +4835,12 @@ Os valores atuais são %1% e %2% respectivamente.</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.ui" line="23"/>
<source>Import Mod from ZIP</source>
<translation>Importar Mod a partir de ZIP</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.ui" line="30"/>
<source>Import Mod from Folder</source>
<translation>Importar Mod a partir da Pasta</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="56"/>
@@ -4868,92 +4855,87 @@ Os valores atuais são %1% e %2% respectivamente.</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="153"/>
<source>Mod Install Succeeded</source>
<translation>Instalação de Mod Concluída</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="154"/>
<source>Successfully installed all mods.</source>
<translation>Todos os Mods foram instalados com sucesso.</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="163"/>
<source>Mod Install Failed</source>
<translation>Instalação de Mod Falhou</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="164"/>
<source>Failed to install the following mods:
%1
Check the log for details.</source>
<translation>Falha ao instalar os seguintes Mods:
%1
Cheque o log para mais detalhes.</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="183"/>
<source>Mod Folder</source>
<translation>Pasta de Mods</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="194"/>
<source>Zipped Mod Location</source>
<translation>Local de Mod Empacotado</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="196"/>
<source>Zipped Archives (*.zip)</source>
<translation>Arquivos Empacotados (*.zip)</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="214"/>
<source>Invalid Selection</source>
<translation>Seleção inválida</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="215"/>
<source>Only mods, cheats, and patches can be deleted.
To delete NAND-installed updates, right-click the game in the game list and click Remove -&gt; Remove Installed Update.</source>
<translation>Apenas Mods, trapaças e correções podem ser deletadas.
Para deletar atualizações instaladas na NAND, pressione o botão direito no jogo na lista e clique em Remover -&gt; Remova Atualização Instalada.</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="221"/>
<source>You are about to delete the following installed mods:
</source>
<translation>Você está a prestes de deletar os seguintes Mods instalados:
</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="227"/>
<source>
Once deleted, these can NOT be recovered. Are you 100% sure you want to delete them?</source>
<translation>
Uma vez deletado, esses NÃO podem ser recuperados. Você tem 100% de certeza que quer deletar eles?</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="232"/>
<source>Delete add-on(s)?</source>
<translation>Deletar add-on(s)?</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="243"/>
<source>Successfully deleted</source>
<translation>Deletado com sucesso</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="244"/>
<source>Successfully deleted all selected mods.</source>
<translation>Todos os Mods selecionados foram deletados com sucesso.</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="267"/>
<source>&amp;Delete</source>
<translation>&amp;Deletar</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="274"/>
<source>&amp;Open in File Manager</source>
<translation>&amp;Abrir Gerenciador de Arquivos</translation>
<translation type="unfinished"/>
</message>
</context>
<context>
@@ -5039,27 +5021,27 @@ Uma vez deletado, esses NÃO podem ser recuperados. Você tem 100% de certeza qu
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="217"/>
<source>Error saving user image</source>
<translation>Erro ao salvar imagem de usuário</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="218"/>
<source>Unable to save image to file</source>
<translation>Incapaz de salvar imagem para arquivo</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="230"/>
<source>&amp;Edit</source>
<translation>&amp;Editar</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="231"/>
<source>&amp;Delete</source>
<translation>&amp;Deletar</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="291"/>
<source>Edit User</source>
<translation>Editar Usuário</translation>
<translation type="unfinished"/>
</message>
</context>
<context>
@@ -5244,7 +5226,7 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.ui" line="17"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Reads controller input from scripts in the same format as TAS-nx scripts.&lt;br/&gt;For a more detailed explanation, please consult the user handbook.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt; as entradas do controle a partir de scripts no mesmo formato que o TAS-nx.&lt;br/&gt;Para uma explicação mais detalhada, por favor consulte o manual do usuário.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.ui" line="27"/>
@@ -5279,7 +5261,7 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.ui" line="84"/>
<source>Show recording dialog</source>
<translation>Mostrar gravações de diálogo</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.ui" line="98"/>
@@ -5412,7 +5394,7 @@ Arrasta os pontos para mudar a posição, ou dá duplo-clique nas células da ta
<message>
<location filename="../../src/yuzu/configuration/configure_touchscreen_advanced.ui" line="26"/>
<source>Warning: The settings in this page affect the inner workings of Eden&apos;s emulated touchscreen. Changing them may result in undesirable behavior, such as the touchscreen partially or not working. You should only use this page if you know what you are doing.</source>
<translation>Atenção: As configurações desta página podem mudar o funcionamento interno do touchscreen emulado do Eden. Mudar elas pode causar problemas indesejados, como o touchscreen parando de funcionar parcialmente e até mesmo por completo. Você deverá apenas usar essa página se você sabe o que está fazendo.</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touchscreen_advanced.ui" line="52"/>
@@ -5708,7 +5690,7 @@ Arrasta os pontos para mudar a posição, ou dá duplo-clique nas células da ta
<message>
<location filename="../../src/yuzu/configuration/configure_web.ui" line="25"/>
<source>Eden Web Service</source>
<translation>Serviço Web do Eden</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_web.ui" line="33"/>
@@ -5723,7 +5705,7 @@ Arrasta os pontos para mudar a posição, ou dá duplo-clique nas células da ta
<message>
<location filename="../../src/yuzu/configuration/configure_web.ui" line="105"/>
<source>Generate</source>
<translation>Gerar</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_web.ui" line="130"/>
@@ -5745,19 +5727,19 @@ Arrasta os pontos para mudar a posição, ou dá duplo-clique nas células da ta
<location filename="../../src/yuzu/configuration/configure_web.cpp" line="118"/>
<source>All Good</source>
<comment>Tooltip</comment>
<translation>Tudo certo</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_web.cpp" line="113"/>
<source>Must be between 4-20 characters</source>
<comment>Tooltip</comment>
<translation>Deve conter entre 4-20 caracteres</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_web.cpp" line="122"/>
<source>Must be 48 characters, and lowercase a-z</source>
<comment>Tooltip</comment>
<translation>Deve ser em 48 caracteres, e letras minúsculas de a-z</translation>
<translation type="unfinished"/>
</message>
</context>
<context>
@@ -5778,12 +5760,12 @@ Arrasta os pontos para mudar a posição, ou dá duplo-clique nas células da ta
<message>
<location filename="../../src/yuzu/data_dialog.ui" line="26"/>
<source>Data Manager</source>
<translation>Gestor de Dados</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/data_dialog.ui" line="48"/>
<source>Deleting ANY data is IRREVERSABLE!</source>
<translation>Deletar QUAISQUER dados é IRREVERSÍVEL!</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/data_dialog.cpp" line="31"/>
@@ -5793,12 +5775,12 @@ Arrasta os pontos para mudar a posição, ou dá duplo-clique nas células da ta
<message>
<location filename="../../src/yuzu/data_dialog.cpp" line="32"/>
<source>User NAND</source>
<translation>NAND de Usuário</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/data_dialog.cpp" line="33"/>
<source>System NAND</source>
<translation>NAND do Sistema</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/data_dialog.cpp" line="34"/>
@@ -5808,7 +5790,7 @@ Arrasta os pontos para mudar a posição, ou dá duplo-clique nas células da ta
<message>
<location filename="../../src/yuzu/data_dialog.cpp" line="35"/>
<source>Saves</source>
<translation>Salvamentos</translation>
<translation type="unfinished"/>
</message>
</context>
<context>
@@ -5816,17 +5798,17 @@ Arrasta os pontos para mudar a posição, ou dá duplo-clique nas células da ta
<message>
<location filename="../../src/yuzu/data_widget.ui" line="14"/>
<source>Form</source>
<translation>Formar</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/data_widget.ui" line="22"/>
<source>Tooltip</source>
<translation>Dica de Ferramenta</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/data_widget.ui" line="70"/>
<source>Open with your system file manager</source>
<translation>Abrir gerenciador de arquivos do sistema</translation>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/yuzu/data_widget.ui" line="106"/>
@@ -6081,7 +6063,7 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/yuzu/bootmanager.cpp" line="1064"/>
<source>Your GPU may not support OpenGL 4.6, or you do not have the latest graphics driver.&lt;br&gt;&lt;br&gt;GL Renderer:&lt;br&gt;%1</source>
<translation>A sua GPU não tem suporte a OpenGL 4.6, talvez você não esteja usando os drivers gráficos mais atuais.&lt;br&gt;&lt;br&gt;Renderizador GL:&lt;br&gt;%1</translation>
<translation>O teu GPU pode não suportar OpenGL 4.6, ou não tem os drivers gráficos mais recentes.</translation>
</message>
<message>
<location filename="../../src/yuzu/bootmanager.cpp" line="1072"/>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 35 KiB

+1 -4
View File
@@ -159,10 +159,7 @@ else()
endif()
if (ARCHITECTURE_x86_64)
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:-mcx16>)
if (PLATFORM_LINUX OR PLATFORM_FREEBSD)
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:-mtls-dialect=gnu2>)
endif()
add_compile_options(-mcx16)
endif()
if (APPLE AND CXX_CLANG)
@@ -8,6 +8,8 @@ package org.yuzu.yuzu_emu.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
import org.yuzu.yuzu_emu.model.Patch
import org.yuzu.yuzu_emu.model.PatchType
@@ -24,13 +26,25 @@ class AddonAdapter(val addonViewModel: AddonViewModel) :
inner class AddonViewHolder(val binding: ListItemAddonBinding) :
AbstractViewHolder<Patch>(binding) {
override fun bind(model: Patch) {
binding.addonCard.setOnClickListener {
binding.addonSwitch.performClick()
val isCheat = model.isCheat()
val isIncompatible = isCheat && model.cheatCompat == Patch.CHEAT_COMPAT_INCOMPATIBLE
val indentPx = if (isCheat) {
(32 * binding.root.context.resources.displayMetrics.density).toInt()
} else {
0
}
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginStart = indentPx
}
binding.addonCard.setOnClickListener(
if (isIncompatible) null else { { binding.addonSwitch.performClick() } }
)
binding.title.text = model.name
binding.version.text = model.version
binding.addonSwitch.setOnCheckedChangeListener(null)
binding.addonSwitch.isChecked = model.enabled
binding.addonSwitch.isEnabled = !isIncompatible
binding.addonSwitch.alpha = if (isIncompatible) 0.38f else 1.0f
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
if (PatchType.from(model.type) == PatchType.Update && checked) {
addonViewModel.enableOnlyThisUpdate(model)
@@ -39,12 +53,9 @@ class AddonAdapter(val addonViewModel: AddonViewModel) :
model.enabled = checked
}
}
val canDelete = model.isRemovable
binding.deleteCard.isEnabled = canDelete
binding.buttonDelete.isEnabled = canDelete
binding.deleteCard.alpha = if (canDelete) 1f else 0.38f
val canDelete = model.isRemovable && !isCheat
binding.deleteCard.isVisible = canDelete
binding.buttonDelete.isVisible = canDelete
if (canDelete) {
val deleteAction = {
addonViewModel.setAddonToDelete(model)
@@ -67,8 +67,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
MY_PAGE_APPLET("my_page_applet_mode"),
INPUT_OVERLAY_AUTO_HIDE("input_overlay_auto_hide"),
OVERLAY_GRID_SIZE("overlay_grid_size"),
GPU_LOG_RING_BUFFER_SIZE("gpu_log_ring_buffer_size"),
ANDROID_PIPELINE_WORKERS("pipeline_worker_count")
GPU_LOG_RING_BUFFER_SIZE("gpu_log_ring_buffer_size")
;
override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)
@@ -582,16 +582,6 @@ abstract class SettingsItem(
units = "%"
)
)
put(
SliderSetting(
IntSetting.ANDROID_PIPELINE_WORKERS,
titleId = R.string.pipeline_worker_cores,
descriptionId = R.string.pipeline_worker_cores_description,
min = 4,
max = 8,
units = "cores"
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_ANTI_ALIASING,
@@ -295,7 +295,6 @@ class SettingsFragmentPresenter(
add(BooleanSetting.EMULATE_BGR565.key)
add(BooleanSetting.RESCALE_HACK.key)
add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
add(IntSetting.ANDROID_PIPELINE_WORKERS.key)
add(BooleanSetting.RENDERER_ASYNCHRONOUS_GPU_EMULATION.key)
add(BooleanSetting.RENDERER_ASYNC_PRESENTATION.key)
add(SettingsItem.GPU_UNSWIZZLE_COMBINED)
@@ -72,7 +72,7 @@ class AddonsFragment : Fragment() {
}
addonViewModel.addonList.collect(viewLifecycleOwner) {
(binding.listAddons.adapter as AddonAdapter).submitList(it)
(binding.listAddons.adapter as AddonAdapter).submitList(it.toList())
}
addonViewModel.showModInstallPicker.collect(
viewLifecycleOwner,
@@ -31,12 +31,17 @@ class AddonViewModel : ViewModel() {
val addonToDelete = _addonToDelete.asStateFlow()
var game: Game? = null
private set
private var loadedGameKey: String? = null
private val isRefreshing = AtomicBoolean(false)
private val pendingRefresh = AtomicBoolean(false)
fun onAddonsViewCreated(game: Game) {
if (this.game?.programId == game.programId && _patchList.value.isNotEmpty()) {
return
}
this.game = game
refreshAddons(commitEmpty = false)
}
@@ -66,8 +71,7 @@ class AddonViewModel : ViewModel() {
NativeLibrary.getPatchesForFile(currentGame.path, currentGame.programId)
} ?: return@launch
val patchList = patches.toMutableList()
patchList.sortBy { it.name }
val patchList = sortPatchesWithCheatsGrouped(patches.toMutableList())
// Ensure only one update is enabled
ensureSingleUpdateEnabled(patchList)
@@ -146,6 +150,7 @@ class AddonViewModel : ViewModel() {
PatchType.Update -> NativeLibrary.removeUpdate(patch.programId)
PatchType.DLC -> NativeLibrary.removeDLC(patch.programId)
PatchType.Mod -> NativeLibrary.removeMod(patch.programId, patch.name)
PatchType.Cheat -> {}
}
refreshAddons(force = true)
}
@@ -179,7 +184,7 @@ class AddonViewModel : ViewModel() {
it.name
}
} else {
it.name
it.getStorageKey()
}
}
}.toTypedArray()
@@ -201,4 +206,28 @@ class AddonViewModel : ViewModel() {
private fun gameKey(game: Game): String {
return "${game.programId}|${game.path}"
}
private fun sortPatchesWithCheatsGrouped(patches: MutableList<Patch>): MutableList<Patch> {
val individualCheats = patches.filter { it.isCheat() }
val nonCheats = patches.filter { !it.isCheat() }.sortedBy { it.name }
val cheatsByParent = individualCheats.groupBy { it.parentName }
val result = mutableListOf<Patch>()
for (patch in nonCheats) {
result.add(patch)
cheatsByParent[patch.name]?.sortedBy { it.name }?.let { childCheats ->
result.addAll(childCheats)
}
}
val knownParents = nonCheats.map { it.name }.toSet()
for ((parentName, orphanCheats) in cheatsByParent) {
if (parentName !in knownParents) {
result.addAll(orphanCheats.sortedBy { it.name })
}
}
return result
}
}
@@ -17,7 +17,9 @@ data class Patch(
val programId: String,
val titleId: String,
val numericVersion: Long = 0,
val source: Int = 0
val source: Int = 0,
val parentName: String = "", // For cheats: name of the mod folder containing them
val cheatCompat: Int = CHEAT_COMPAT_COMPATIBLE
) {
companion object {
const val SOURCE_UNKNOWN = 0
@@ -25,8 +27,31 @@ data class Patch(
const val SOURCE_SDMC = 2
const val SOURCE_EXTERNAL = 3
const val SOURCE_PACKED = 4
const val CHEAT_COMPAT_COMPATIBLE = 0
const val CHEAT_COMPAT_INCOMPATIBLE = 1
}
val isRemovable: Boolean
get() = source != SOURCE_EXTERNAL && source != SOURCE_PACKED
/**
* Returns the storage key used for saving enabled/disabled state.
* For cheats with a parent, returns "ParentName::CheatName".
*/
fun getStorageKey(): String {
return if (parentName.isNotEmpty()) {
"$parentName::$name"
} else {
name
}
}
/**
* Returns true if this patch is an individual cheat entry (not a cheat mod).
* Individual cheats have type=Cheat and a parent mod name.
*/
fun isCheat(): Boolean = type == PatchType.Cheat.int && parentName.isNotEmpty()
fun isIncompatibleCheat(): Boolean = isCheat() && cheatCompat == CHEAT_COMPAT_INCOMPATIBLE
}
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -6,7 +9,8 @@ package org.yuzu.yuzu_emu.model
enum class PatchType(val int: Int) {
Update(0),
DLC(1),
Mod(2);
Mod(2),
Cheat(3);
companion object {
fun from(int: Int): PatchType = entries.firstOrNull { it.int == int } ?: Update
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -7,5 +7,5 @@
package org.yuzu.yuzu_emu.utils
object AddonUtil {
val validAddonDirectories = listOf("cheats", "exefs", "romfs", "romfslite", "romfs_ext")
val validAddonDirectories = listOf("cheats", "exefs", "romfs", "romfslite")
}
@@ -147,13 +147,6 @@ namespace AndroidSettings {
&show_performance_overlay};
Settings::Setting<s32> pipeline_worker_count{linkage, 4, "pipeline_worker_count",
Settings::Category::Android,
Settings::Specialization::Default,
true,
true};
Settings::Setting<bool> show_input_overlay{linkage, true, "show_input_overlay",
Settings::Category::Overlay};
Settings::Setting<bool> overlay_snap_to_grid{linkage, false, "overlay_snap_to_grid",
+3 -1
View File
@@ -1406,7 +1406,9 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env
Common::Android::ToJString(env, patch.version), static_cast<jint>(patch.type),
Common::Android::ToJString(env, std::to_string(patch.program_id)),
Common::Android::ToJString(env, std::to_string(patch.title_id)),
static_cast<jlong>(patch.numeric_version), static_cast<jint>(patch.source));
static_cast<jlong>(patch.numeric_version), static_cast<jint>(patch.source),
Common::Android::ToJString(env, patch.parent_name),
static_cast<jint>(patch.cheat_compat));
env->SetObjectArrayElement(jpatchArray, i, jpatch);
++i;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 67 KiB

@@ -66,8 +66,6 @@
<string name="show_power_info_description">عرض استهلاك الطاقة الحالي والسعة المتبقية في البطارية</string>
<string name="show_shaders_building">عرض بناء التظليل</string>
<string name="show_shaders_building_description">عرض عدد التظليل التي يتم بناؤها</string>
<string name="pipeline_worker_cores">خيوط عامل خط الأنابيب</string>
<string name="pipeline_worker_cores_description">قم بإدارة عدد النوى المستخدمة لبناء خطوط أنابيب Vulkan، فالقيمة الأعلى ستحسن أداء تجميع خط الأنابيب ولكن درجات الحرارة سترتفع أيضًا.</string>
<string name="overlay_position">موضع الطبقة</string>
<string name="overlay_position_description">حدد مكان عرض الطبقة على الشاشة</string>
<string name="overlay_position_top_left">أعلى اليمين</string>
@@ -487,15 +485,15 @@
<string name="renderer_force_max_clock">إجبار السرعة القصوى (لأجهزة Adreno فقط)</string>
<string name="renderer_force_max_clock_description">يجبر وحدة معالجة الرسومات على العمل بأقصى سرعة ممكنة (سيظل يتم تطبيق القيود الحرارية).</string>
<string name="renderer_asynchronous_gpu_emulation">محاكاة غير متزامنة لوحدة معالجة الرسومات</string>
<string name="renderer_asynchronous_gpu_emulation_description">يمكن لهذه الحيلة أن تزيد الأداء عن طريق تشغيل محاكاة وحدة معالجة الرسومات بشكل غير متزامن على حساب مشاكل الرسومات وزيادة معدلات الأعطال بسبب العمليات المتعلقة بالتوقيت.</string>
<string name="renderer_asynchronous_gpu_emulation_description">يُشغّل هذا الخيار محاكاة وحدة معالجة الرسومات بشكل غير متزامن لتقليل توقف وحدة المعالجة المركزية وتحسين الإنتاجية. عطّل هذا الخيار فقط في حال واجهت مشاكل متعلقة بالتوقيت.</string>
<string name="renderer_async_presentation">عرض غير متزامن</string>
<string name="renderer_async_presentation_description">يمكن لهذه الحيلة أن تزيد من الأداء عن طريق نقل عملية العرض إلى خيط معالجة منفصل على حساب مشاكل الرسوميات.</string>
<string name="renderer_async_presentation_description">يحسّن الأداء بشكل طفيف عن طريق نقل عملية العرض إلى خيط معالجة منفصل لوحدة المعالجة المركزية.</string>
<string name="renderer_reactive_flushing">استخدم التنظيف التفاعلي</string>
<string name="renderer_reactive_flushing_description">يحسن دقة العرض في بعض الألعاب على حساب الأداء.</string>
<string name="enable_buffer_history">تمكين سجل التخزين المؤقت</string>
<string name="enable_buffer_history_description">يُتيح هذا الخيار الوصول إلى حالات التخزين المؤقت السابقة. وقد يُحسّن جودة العرض وثبات الأداء في بعض الألعاب.</string>
<string name="use_optimized_vertex_buffers">مخازن الرؤوس المُحسّنة</string>
<string name="use_optimized_vertex_buffers_description">يُتيح ربطًا مُحسَّنًا لمخازن الرؤوس لتحسين الأداء. يتطلب برامج تشغيل Mesa 26.0+ Turnip/ برامج تشغيل QCOM. قد يتعطل على برامج تشغيل Turnip القديمة (25.3 وما دون).</string>
<string name="use_optimized_vertex_buffers_description">يتيح ربط مخزن الرؤوس المُحسّن لتحسين الأداء. يتطلب برامج تشغيل Turnip/QCOM من إصدار Mesa 26.0 أو أحدث. سيؤدي إلى تعطل النظام عند استخدام برامج تشغيل Turnip الأقدم.</string>
<string name="hacks">اختراقات</string>
@@ -507,8 +505,6 @@
<string name="fix_bloom_effects_description">يقلل من ضبابية التوهج في LA/EOW (Adreno A6XX - A7XX/ Turnip)، ويزيل التوهج في Burnout. تحذير: قد يسبب تشوهات رسومية في ألعاب أخرى.</string>
<string name="emulate_bgr565">محاكاة BGR565</string>
<string name="emulate_bgr565_description">يُصلح مشاكل انعكاس الألوان في الألعاب أو ظهور تشوهات غريبة أو ظلال غريبة.</string>
<string name="rescale_hack">تفعيل ميزة إعادة التحجيم القديمة</string>
<string name="rescale_hack_description">يُمكّن هذا الخيار من التعامل مع عملية إعادة تحجيم الألعاب بطريقة تقليدية باستخدام مسار إعادة التحجيم السريع</string>
<string name="renderer_asynchronous_shaders">استخدم تظليل غير متزامن</string>
<string name="renderer_asynchronous_shaders_description">يقوم بتجميع التظليل بشكل غير متزامن. قد يقلل ذلك من التقطعات ولكنه قد يؤدي أيضًا إلى حدوث أخطاء.</string>
<string name="gpu_unswizzle_settings">إعدادات إلغاء ترتيب بيانات وحدة معالجة الرسومات</string>
@@ -527,10 +523,10 @@
<string name="extensions">إضافات</string>
<string name="dyna_state">الحالة الديناميكية الموسعة</string>
<string name="dyna_state_description">يتحكم هذا الخيار في عدد الميزات التي يمكن استخدامها في ExtendedDynamicState (EDS). كلما زادت القيمة، قلّ عدد عمليات تجميع خط الأنابيب بناءً على الحالة الديناميكية التي يدعمها برنامج التشغيل.</string>
<string name="dyna_state_description">يتحكم هذا الخيار في عدد الميزات التي يمكن استخدامها في حالة الديناميكية الموسعة. تسمح الأرقام الأعلى بمزيد من الميزات ويمكن أن تزيد من الأداء، ولكنها قد تسبب مشاكل مع بعض برامج التشغيل والأجهزة.</string>
<string name="disabled">معطل</string>
<string name="vertex_input_dynamic_state">حالة ديناميكية لإدخال الرأس</string>
<string name="vertex_input_dynamic_state_description">يتيح تمكين هذه الميزة معالجة أكثر مرونة لمدخلات الرؤوس، مما قد يقلل من وقت تجميع خط الأنابيب في vertex/buffer.</string>
<string name="vertex_input_dynamic_state_description">يتيح ميزة الحالة الديناميكية لإدخال الرأس لتحسين الجودة والأداء.</string>
<string name="sample_shading_fraction">تظليل العينة</string>
<string name="sample_shading_fraction_description">يسمح هذا الخيار بتنفيذ مُظلل الأجزاء لكل عينة في جزء متعدد العينات بدلاً من تنفيذه مرة واحدة لكل جزء. يُحسّن هذا من جودة الرسومات على حساب بعض الأداء.</string>
@@ -478,8 +478,10 @@
<string name="extensions">Rozšíření</string>
<string name="dyna_state">Úroveň EDS</string>
<string name="dyna_state_description">Určuje počet funkcí využívaných v rámci rozšířeného dynamického stavu API Vulkan (Extended Dynamic State). Vyšší hodnoty umožňují využít více funkcí a mohou zvýšit výkon, ale u některých ovladačů a výrobců grafických karet mohou způsobovat problémy s kompatibilitou.</string>
<string name="disabled">Vypnuto</string>
<string name="vertex_input_dynamic_state">Dynamický stav vstupu vrcholů (Vertex Input)</string>
<string name="vertex_input_dynamic_state_description">Aktivuje funkci dynamického stavu vstupu vrcholů (Vertex Input Dynamic State) pro lepší kvalitu a výkon.</string>
<string name="display">Zobrazení</string>
<string name="renderer_screen_layout">Orientace</string>
@@ -476,8 +476,10 @@ Wird der Handheld-Modus verwendet, verringert es die Auflösung und erhöht die
<string name="extensions">Erweiterungen</string>
<string name="dyna_state">Erweiterter dynamischer Status</string>
<string name="dyna_state_description">Steuert die Anzahl der Funktionen, die im \"Vertex Input Dynamic State\" werden können. Höhere Werte ermöglichen mehr Funktionen und können die Leistung steigern, können aber bei einigen Treibern und Anbietern zu Problemen führen.</string>
<string name="disabled">Deaktiviert</string>
<string name="vertex_input_dynamic_state">Vertex Input Dynamic State</string>
<string name="vertex_input_dynamic_state_description">Aktiviert die Funktion \"Vertex Input Dynamic State\" für bessere Qualität und Leistung.</string>
<string name="sample_shading_fraction">Sample Shading</string>
<string name="sample_shading_fraction_description">Ermöglicht es dem Fragment-Shader, in einem Multisample-Fragment pro Sample anstatt einmal pro Fragment ausgeführt zu werden. Verbessert die Grafikqualität auf Kosten der Leistung.</string>
@@ -66,8 +66,6 @@
<string name="show_power_info_description">Muestra el consumo de energía actual y la capacidad restante de la batería</string>
<string name="show_shaders_building">Mostrar construcción de sombreadores</string>
<string name="show_shaders_building_description">Muestra el número actual de sombreadores que se están construyendo</string>
<string name="pipeline_worker_cores">Hilos de trabajo de canalización</string>
<string name="pipeline_worker_cores_description">Gestiona la cantidad de núcleos usados para la construcción de canalizaciones de Vulkan; un valor más alto mejorará el rendimiento de la contrucción de canalización, pero también aumentarán las temperaturas.</string>
<string name="overlay_position">Posición de la superposición</string>
<string name="overlay_position_description">Elige dónde se muestra la superposición en la pantalla</string>
<string name="overlay_position_top_left">Superior izquierda</string>
@@ -481,14 +479,16 @@
<string name="renderer_force_max_clock">Forzar velocidad al máximo (solo Adreno)</string>
<string name="renderer_force_max_clock_description">Fuerza a la GPU a ejecutarse a la velocidad máxima de reloj posible (se seguirán aplicando restricciones térmicas).</string>
<string name="renderer_asynchronous_gpu_emulation">Emulación de GPU asíncrona</string>
<string name="renderer_asynchronous_gpu_emulation_description">Este hack puede aumentar el rendimiento ejecutando la emulación de la GPU de forma asíncrona, a costa de problemas gráficos y un aumento en la tasa de fallos debido a operaciones relacionadas con la sincronización.</string>
<string name="renderer_asynchronous_gpu_emulation_description">Ejecuta la emulación de la GPU de forma asíncrona para reducir los bloqueos de la CPU y mejorar el rendimiento. Desactiva esta opción solo si experimentas problemas de sincronización.</string>
<string name="renderer_async_presentation">Presentación asíncrona</string>
<string name="renderer_async_presentation_description">Este hack puede aumentar el rendimiento al mover la presentación a un hilo independiente de la CPU a costa de problemas gráficos.</string>
<string name="renderer_async_presentation_description">Mejora ligeramente el rendimiento al mover la presentación a un hilo independiente de la CPU.</string>
<string name="renderer_reactive_flushing">Usar limpieza reactiva</string>
<string name="renderer_reactive_flushing_description">Mejora la precisión de renderizado en algunos juegos, pero reduce el rendimiento.</string>
<string name="enable_buffer_history">Activar el historial del búfer</string>
<string name="enable_buffer_history_description">Permite el acceso al estado del búfer anterior. Esta opción puede mejorar la calidad de renderizado y la consistencia en el rendimiento de algunos juegos.</string>
<string name="use_optimized_vertex_buffers">Búferes de vértices optimizados</string>
<string name="use_optimized_vertex_buffers_description">Permite la optimización del enlace del búfer de vértices para un mejor rendimiento. Requiere controladores Mesa 26.0+ Turnip/ controladores QCOM. Causará fallos con controladores Turnip más antiguos.</string>
<string name="hacks">Hacks</string>
<string name="fast_gpu_time">Tiempo rápido de la GPU</string>
@@ -499,7 +499,6 @@
<string name="fix_bloom_effects_description">Reduce el efecto de resplandor en LA/EOW (Adreno A6XX - A7XX/ Turnip), elimina el resplandor en Burnout. Advertencia: puede causar artefactos gráficos en otros juegos.</string>
<string name="emulate_bgr565">Emular BGR565</string>
<string name="emulate_bgr565_description">Soluciona problemas con colores invertidos en juegos, artefactos o sombras extrañas.</string>
<string name="rescale_hack">Activar la pasada de reescalado heredada</string>
<string name="renderer_asynchronous_shaders">Usar sombreadores asíncronos</string>
<string name="renderer_asynchronous_shaders_description">Compila los sombreadores de forma asíncrona. Esto puede reducir los tirones, pero también puede introducir errores gráficos.</string>
<string name="gpu_unswizzle_settings">Ajustes de desentrelazado de la GPU</string>
@@ -518,8 +517,10 @@
<string name="extensions">Extensiones</string>
<string name="dyna_state">Estado dinámico extendido</string>
<string name="dyna_state_description">Controla la cantidad de funciones que se pueden usar en el Estado Dinámico Extendido. Un número mayor permite más funciones y puede aumentar el rendimiento, pero puede causar problemas con algunos controladores y proveedores.</string>
<string name="disabled">Desactivado</string>
<string name="vertex_input_dynamic_state">Estado dinámico de entrada de vértices</string>
<string name="vertex_input_dynamic_state_description">Activa la función de estado dinámico de entrada de vértices para una mejor calidad y rendimiento.</string>
<string name="sample_shading_fraction">Muestreo de sombreado</string>
<string name="sample_shading_fraction_description">Permite que el sombreador de fragmentos se ejecute por muestra en un fragmento multimuestreado, en lugar de una sola vez por fragmento. Mejora la calidad de los gráficos a coste de algo de rendimiento.</string>
@@ -483,6 +483,7 @@
<string name="dyna_state">État dynamique étendu</string>
<string name="disabled">Désactivé</string>
<string name="vertex_input_dynamic_state">État dynamique d\'entrée de sommet</string>
<string name="vertex_input_dynamic_state_description">Active la fonctionnalité d\'état dynamique des entrées de sommets pour une meilleure qualité et de meilleures performances.</string>
<string name="display">Affichage</string>
<string name="renderer_screen_layout">Orientation</string>
@@ -476,8 +476,10 @@
<string name="extensions">Rozszerzenia</string>
<string name="dyna_state">Rozszerzony stan dynamiczny</string>
<string name="dyna_state_description">Kontroluje liczbę funkcji, które mogą być używane w Extended Dynamic State. Wyższe wartości pozwalają na użycie większej liczby funkcji i mogą zwiększyć wydajność, ale mogą powodować problemy z niektórymi sterownikami i u niektórych producentów.</string>
<string name="disabled">Wyłączone</string>
<string name="vertex_input_dynamic_state">Dynamiczny stan wejścia wierzchołków</string>
<string name="vertex_input_dynamic_state_description">Włącza funkcję dynamicznego stanu wejścia wierzchołków, poprawiając jakość i wydajność.</string>
<string name="sample_shading_fraction">Cieniowanie próbkowane</string>
<string name="sample_shading_fraction_description">Pozwala uruchamiać shader fragmentów dla każdej próbki w wielopróbkowanym fragmencie zamiast raz na fragment. Poprawia jakość grafiki kosztem części wydajności.</string>
@@ -463,6 +463,7 @@
<string name="dyna_state">Extended Dynamic State</string>
<string name="disabled">Desativado</string>
<string name="vertex_input_dynamic_state">Vertex Input Dynamic State</string>
<string name="vertex_input_dynamic_state_description">Ativa o recurso de vertex input dynamic state para melhor qualidade e desempenho.</string>
<string name="display">Tela</string>
<string name="renderer_screen_layout">Orientação</string>
@@ -66,8 +66,6 @@
<string name="show_power_info_description">Показать текущее энергопотребление и оставшуюся емкость аккумулятора</string>
<string name="show_shaders_building">Показать компиляцию шейдеров</string>
<string name="show_shaders_building_description">Отображает текущее количество шейдеров, которые компилируются</string>
<string name="pipeline_worker_cores">Рабочие потоки конвейера</string>
<string name="pipeline_worker_cores_description">Позволяет настроить количество ядер, задействованных при построении конвейеров Vulkan. Чем выше значение, тем быстрее будет компиляция конвейеров, однако это приведёт к повышению температуры устройства.</string>
<string name="overlay_position">Позиция оверлея</string>
<string name="overlay_position_description">Расположение оверлея на экране</string>
<string name="overlay_position_top_left">Сверху слева</string>
@@ -483,15 +481,15 @@
<string name="renderer_force_max_clock">Принудительная максимальная тактовая частота (только для Adreno)</string>
<string name="renderer_force_max_clock_description">Заставляет ГПУ работать на максимально возможных тактовых частотах (тепловые ограничения все равно будут применяться).</string>
<string name="renderer_asynchronous_gpu_emulation">Асинхронная эмуляция ГПУ</string>
<string name="renderer_asynchronous_gpu_emulation_description">Может повысить производительность за счёт асинхронного запуска эмуляции ГПУ, но ценой появления графических ошибок и увеличения частоты вылетов из-за операций, зависящих от синхронизации.</string>
<string name="renderer_asynchronous_gpu_emulation_description">Выполняет эмуляцию ГПУ асинхронно для снижения задержек ЦП и увеличения производительности. Отключайте только при возникновении проблем с таймингами.</string>
<string name="renderer_async_presentation">Асинхронная презентация</string>
<string name="renderer_async_presentation_description">Может повысить производительность за счёт перемещения вывода кадров в отдельный поток ЦП, но ценой возникновения графических проблем.</string>
<string name="renderer_async_presentation_description">Немного улучшает производительность, перемещая презентацию в отдельный поток ЦП.</string>
<string name="renderer_reactive_flushing">Реактивная очистка</string>
<string name="renderer_reactive_flushing_description">Повышение точности рендеринга в некоторых играх за счет снижения производительности.</string>
<string name="enable_buffer_history">Включить историю буфера</string>
<string name="enable_buffer_history_description">Позволяет обращаться к предыдущим состояниям буфера. Эта опция может повысить качество рендеринга и стабильность производительности в некоторых играх.</string>
<string name="use_optimized_vertex_buffers">Оптимизированные вершинные буферы</string>
<string name="use_optimized_vertex_buffers_description">Включает оптимизированную привязку вершинного буфера для повышения производительности. Требует Mesa Turnip 26.0+ / QCOM. Приводит к вылету на старых версиях драйверов Turnip (25.3 и ниже).</string>
<string name="use_optimized_vertex_buffers_description">Включает оптимизированную привязку вершинного буфера для повышения производительности. Требует Mesa Turnip 26.0+ / QCOM драйверы. Приводит к вылету на старых версиях Turnip.</string>
<string name="hacks">Хаки</string>
@@ -503,8 +501,6 @@
<string name="fix_bloom_effects_description">Частично убирает размытие в LA/EOW (Adreno A6XX - A7XX/ Turnip), полностью отключает его в Burnout. Внимание: может вызывать графические артефакты в других играх.</string>
<string name="emulate_bgr565">Эмулировать BGR565</string>
<string name="emulate_bgr565_description">Исправляет проблемы с инвертированными цветами в играх, а также со странными артефактами или некорректными тенями.</string>
<string name="rescale_hack">Включить старый метод изменения разрешения</string>
<string name="rescale_hack_description">Включает старый метод обработки этапа перенастройки масштабирования для игр за счёт использования быстрого алгоритма перемасштабирования.</string>
<string name="renderer_asynchronous_shaders">Использовать асинхронные шейдеры</string>
<string name="renderer_asynchronous_shaders_description">Компилирует шейдеры асинхронно. Это может уменьшить подтормаживания, но также может вызвать графические артефакты.</string>
<string name="gpu_unswizzle_settings">Настройки распаковки текстур (Unswizzle)</string>
@@ -523,10 +519,10 @@
<string name="extensions">Расширения</string>
<string name="dyna_state">Расширенное динамическое состояние</string>
<string name="dyna_state_description">Управляет количеством функций, доступных для использования в расширенном динамическом состояние. Более высокое значение позволит сократить количество компиляций конвейеров за счёт поддержки динамического состояния драйвером.</string>
<string name="dyna_state_description">Управляет количеством функций, доступных в режиме «Расширенное динамическое состояние». Большее число позволяет задействовать больше функций и может повысить производительность, но способно вызывать проблемы с некоторыми драйверами и графикой.</string>
<string name="disabled">Отключено</string>
<string name="vertex_input_dynamic_state">Динамическое состояние ввода вершин</string>
<string name="vertex_input_dynamic_state_description">Включение этой функции обеспечивает более гибкую обработку входных данных вершин, что потенциально сокращает время компиляции конвейера на этапе работы с вершинами и буферами.</string>
<string name="vertex_input_dynamic_state_description">Включает функцию динамического состояния ввода вершин для повышения качества и производительности</string>
<string name="sample_shading_fraction">Сэмпловое затенение</string>
<string name="sample_shading_fraction_description">Позволяет шейдеру фрагментов выполняться для каждого сэмпла в многосэмпловом фрагменте, а не один раз на фрагмент. Улучшает качество графики ценой некоторого падения производительности.</string>
@@ -1159,6 +1155,4 @@
<string name="license_fidelityfx_fsr_description">Высококачественное масштабирование от AMD</string>
<string name="external_content">Дополнительный контент</string>
<string name="add_folders">Добавить папку</string>
<string name="percent">%1$d %%</string>
</resources>
</resources>
@@ -66,8 +66,6 @@
<string name="show_power_info_description">Показати поточне споживання енергії та залишкову ємність акумулятора</string>
<string name="show_shaders_building">Показати побудову шейдерів</string>
<string name="show_shaders_building_description">Показати поточну кількість шейдерів, які наразі компілюються</string>
<string name="pipeline_worker_cores">Потоки обробника конвеєра</string>
<string name="pipeline_worker_cores_description">Керує кількістю ядер, що використовуються для збірки конвеєрів Vulkan. Вище значення покращить продуктивність компіляції конвеєра, але також підвищить температуру.</string>
<string name="overlay_position">Позиція оверлею</string>
<string name="overlay_position_description">Обрати розташування виводу статистики на екрані</string>
<string name="overlay_position_top_left">Вгорі ліворуч</string>
@@ -483,15 +481,15 @@
<string name="renderer_force_max_clock">Максимальна тактова частота (тільки Adreno)</string>
<string name="renderer_force_max_clock_description">Змушує GPU працювати на максимальній тактовій частоті.</string>
<string name="renderer_asynchronous_gpu_emulation">Асинхронна емуляція ГП</string>
<string name="renderer_asynchronous_gpu_emulation_description">Це обхідне рішення може покращити продуктивність завдяки асинхронному виконанню емуляції ГП, але спричинить проблеми з графікою та збільшить частоту збоїв чутливих до таймінгів операцій.</string>
<string name="renderer_asynchronous_gpu_emulation_description">Емуляція ГП виконується асинхронно для зменшення затримок ЦП й покращення пропускної здатності. Вимкніть лише у випадку виникнення проблем із таймінгами.</string>
<string name="renderer_async_presentation">Асинхронне подання</string>
<string name="renderer_async_presentation_description">Це обхідне рішення може покращити продуктивність завдяки переміщенню подання на окремий потік ЦП, але спричинить проблеми з графікою.</string>
<string name="renderer_async_presentation_description">Трохи покращує продуктивність завдяки переміщенню подання на окремий потік ЦП.</string>
<string name="renderer_reactive_flushing">Реактивне очищення</string>
<string name="renderer_reactive_flushing_description">Покращує точність рендерингу в деяких іграх.</string>
<string name="enable_buffer_history">Увімкнути історію буфера</string>
<string name="enable_buffer_history_description">Вмикає доступ до попередніх станів буфера. Цей параметр може покращити якість візуалізації та стабільну продуктивність у деяких іграх.</string>
<string name="use_optimized_vertex_buffers">Оптимізовані буфери вершин</string>
<string name="use_optimized_vertex_buffers_description">Застосовує оптимізований буфер вершин, щоб покращити продуктивність. Потребує драйверів Mesa 26.0+ Turnip / QCOM. На старіших драйверах Turnip виникатиме збій (25.3 і нижче).</string>
<string name="use_optimized_vertex_buffers_description">Застосовує оптимізований буфер вершин, щоб покращити продуктивність. Потребує драйверів Mesa 26.0+ Turnip / QCOM. На старіших драйверах Turnip виникатиме збій.</string>
<string name="hacks">Обхідні рішення</string>
@@ -503,8 +501,6 @@
<string name="fix_bloom_effects_description">Зменшує розмиття світіння в LA/EOW (Adreno A6XXA7XX / Turnip), прибирає світіння в Burnout. Увага: може спричинити графічні артефакти в інших іграх.</string>
<string name="emulate_bgr565">Емулювати BGR565</string>
<string name="emulate_bgr565_description">Виправляє проблеми з інвертованими кольорами в іграх або дивними артефактами чи тінями.</string>
<string name="rescale_hack">Увімкнути застаріле масштабування</string>
<string name="rescale_hack_description">Вмикає застарілу обробку масштабування для ігор, використовуючи швидкий шлях масштабування</string>
<string name="renderer_asynchronous_shaders">Асинхронні шейдери</string>
<string name="renderer_asynchronous_shaders_description">Компілює шейдери асинхронно. Це може зменшити затримки, але також може спричинити графічні баги.</string>
<string name="gpu_unswizzle_settings">Налаштування розпакування за допомогою ГП</string>
@@ -523,10 +519,10 @@
<string name="extensions">Розширення</string>
<string name="dyna_state">Розширений динамічний стан</string>
<string name="dyna_state_description">Керує кількістю функцій, які можна використовувати в ExtendedDynamicState (EDS). Вище значення дозволить зменшити кількість компіляцій конвеєра на основі динамічного стану, підтримуваного драйвером.</string>
<string name="dyna_state_description">Керує кількістю функцій, які можна використовувати в «Розширеному динамічному стані». Вище число дозволяє більше функцій і може покращити продуктивність, але може спричинити проблеми з деякими драйверами й виробниками.</string>
<string name="disabled">Вимкнено</string>
<string name="vertex_input_dynamic_state">Динамічний стан введення вершин</string>
<string name="vertex_input_dynamic_state_description">Увімкнення цієї функції дозволить гнучкішу обробку введення вершин. Може зменшити час компіляції конвеєра для вершин/буфера.</string>
<string name="vertex_input_dynamic_state_description">Вмикає можливість динамічного стану введення вершин для кращих якості й продуктивності.</string>
<string name="sample_shading_fraction">Простий шейдинг</string>
<string name="sample_shading_fraction_description">Дозволяє виконувати фрагмент шейдера для кожного зразка в багатозразковому фрагменті замість одного разу для кожного фрагмента. Покращує якість графікі ціною втрати продуктивності.</string>
@@ -475,12 +475,16 @@
<string name="renderer_force_max_clock">强制最大时钟 (仅限 Adreno)</string>
<string name="renderer_force_max_clock_description">强制 GPU 以最大时钟运行 (仍被温控限制)。</string>
<string name="renderer_asynchronous_gpu_emulation">GPU 异步模拟</string>
<string name="renderer_asynchronous_gpu_emulation_description">异步运行 GPU 模拟,以减少 CPU 停顿并提高吞吐量。仅当遇到与时序相关的问题时才应禁用此功能。</string>
<string name="renderer_async_presentation">异步呈现</string>
<string name="renderer_async_presentation_description">通过将呈现操作移至单独的 CPU 线程来略微提升性能。</string>
<string name="renderer_reactive_flushing">启用反应性刷新</string>
<string name="renderer_reactive_flushing_description">通过牺牲性能来提高某些游戏的渲染精度。</string>
<string name="enable_buffer_history">启用缓冲区历史</string>
<string name="enable_buffer_history_description">启用对先前缓冲区状态的访问。此选项可在某些游戏中提升渲染质量并保持性能的一致性。</string>
<string name="use_optimized_vertex_buffers">优化顶点缓冲区</string>
<string name="use_optimized_vertex_buffers_description">启用经过优化的顶点缓冲区绑定以提升性能。需要 Mesa 26.0 及以上版本的 Turnip 或 QCOM 驱动程序。若使用较旧版本的 Turnip 驱动则会导致崩溃。</string>
<string name="hacks">Hacks</string>
<string name="fast_gpu_time">GPU 超频频率</string>
@@ -509,8 +513,10 @@
<string name="extensions">扩展</string>
<string name="dyna_state">扩展动态状态</string>
<string name="dyna_state_description">控制在扩展动态状态中可使用的函数数量。更高的数值允许启用更多功能,并可能提升性能,但同时也可能导致额外的图形问题。</string>
<string name="disabled">已禁用</string>
<string name="vertex_input_dynamic_state">顶点输入动态状态</string>
<string name="vertex_input_dynamic_state_description">开启顶点输入动态状态功能来获得更好的质量和性能。</string>
<string name="sample_shading_fraction">采样着色</string>
<string name="sample_shading_fraction_description">允许片段着色器在多采样片段中每个样本执行一次,而不是每个片段执行一次。以提高性能为代价改善图形质量。</string>
@@ -466,6 +466,7 @@
<string name="dyna_state">擴展動態狀態</string>
<string name="disabled">已停用</string>
<string name="vertex_input_dynamic_state">頂點輸入動態狀態</string>
<string name="vertex_input_dynamic_state_description">啟用頂點輸入動態狀態以取得更佳的品質及性能</string>
<string name="display">顯示</string>
<string name="renderer_screen_layout">方向</string>
@@ -1 +1 @@
<?xml version='1.0' encoding='utf-8'?><resources><color name='ic_launcher_background'>#ffd700</color></resources>
<?xml version='1.0' encoding='utf-8'?><resources><color name='ic_launcher_background'>#1F143C</color></resources>
@@ -71,8 +71,6 @@
<string name="show_power_info_description">Display current power draw and remaining capacity on battery</string>
<string name="show_shaders_building">Show Shaders Building</string>
<string name="show_shaders_building_description">Display current number of shaders being built</string>
<string name="pipeline_worker_cores">Pipeline Worker Threads</string>
<string name="pipeline_worker_cores_description">Manage the amount of cores used for building Vulkan pipelines, the higher value will improve pipeline compilation performance but temperatures will increase as well.</string>
<string name="overlay_position">Overlay Position</string>
<string name="overlay_position_description">Choose where the overlay is displayed on the screen</string>
<string name="overlay_position_top_left">Top Left</string>
@@ -493,15 +491,15 @@
<string name="renderer_force_max_clock">Force maximum clocks (Adreno only)</string>
<string name="renderer_force_max_clock_description">Forces the GPU to run at the maximum possible clocks (thermal constraints will still be applied).</string>
<string name="renderer_asynchronous_gpu_emulation">GPU async emulation</string>
<string name="renderer_asynchronous_gpu_emulation_description">This hack can increase performance by running GPU emulation asynchronously at the cost of graphical issues and increased crash rates by timing-related operations.</string>
<string name="renderer_asynchronous_gpu_emulation_description">Runs GPU emulation asynchronously to reduce CPU stalls and improve throughput. Disable this only if you run into timing-related issues.</string>
<string name="renderer_async_presentation">Asynchronous presentation</string>
<string name="renderer_async_presentation_description">This hack can increase performance by moving presentation to a separate CPU thread at the cost of graphical issues.</string>
<string name="renderer_async_presentation_description">Slightly improves performance by moving presentation to a separate CPU thread.</string>
<string name="renderer_reactive_flushing">Use reactive flushing</string>
<string name="renderer_reactive_flushing_description">Improves rendering accuracy in some games at the cost of performance.</string>
<string name="enable_buffer_history">Enable buffer history</string>
<string name="enable_buffer_history_description">Enables access to previous buffer states. This option may improve rendering quality and performance consistency in some games.</string>
<string name="use_optimized_vertex_buffers">Optimized Vertex Buffers</string>
<string name="use_optimized_vertex_buffers_description">Enables optimized vertex buffer binding for improved performance. Requires Mesa 26.0+ Turnip drivers/ QCOM drivers. Will crash on older Turnip drivers (25.3 and below).</string>
<string name="use_optimized_vertex_buffers_description">Enables optimized vertex buffer binding for improved performance. Requires Mesa 26.0+ Turnip drivers/ QCOM drivers. Will crash on older Turnip drivers.</string>
<string name="hacks">Hacks</string>
@@ -533,10 +531,10 @@
<string name="extensions">Extensions</string>
<string name="dyna_state">Extended Dynamic State</string>
<string name="dyna_state_description">Controls the number of features that can be used in ExtendedDynamicState (EDS). The higher value will allow to reduce the amount of pipeline compilations based on the dynamic state supported by driver.</string>
<string name="dyna_state_description">Controls the number of features that can be used in Extended Dynamic State. Higher numbers allow for more features and can increase performance, but may cause issues with some drivers and vendors.</string>
<string name="disabled">Disabled</string>
<string name="vertex_input_dynamic_state">Vertex Input Dynamic State</string>
<string name="vertex_input_dynamic_state_description">Enabling this feature allows for more flexible vertex input handling, potentially reducing pipeline compilation time in vertex/buffer.</string>
<string name="vertex_input_dynamic_state_description">Enables vertex input dynamic state feature for better quality and performance.</string>
<string name="sample_shading_fraction">Sample Shading</string>
<string name="sample_shading_fraction_description">Allows the fragment shader to execute per sample in a multi-sampled fragment instead once per fragment. Improves graphics quality at the cost of some performance.</string>
+2
View File
@@ -66,6 +66,8 @@ add_library(
fs/path_util.cpp
fs/path_util.h
hash.h
heap_tracker.cpp
heap_tracker.h
hex_util.cpp
hex_util.h
host_memory.cpp
+1 -1
View File
@@ -516,7 +516,7 @@ namespace Common::Android {
s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class));
s_patch_constructor = env->GetMethodID(
patch_class, "<init>",
"(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;JI)V");
"(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;JILjava/lang/String;)V");
s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z");
s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;");
s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;");
+282
View File
@@ -0,0 +1,282 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fstream>
#include "common/heap_tracker.h"
#include "common/logging.h"
#include "common/assert.h"
namespace Common {
namespace {
s64 GetMaxPermissibleResidentMapCount() {
// Default value.
s64 value = 65530;
// Try to read how many mappings we can make.
std::ifstream s("/proc/sys/vm/max_map_count");
s >> value;
// Print, for debug.
LOG_INFO(HW_Memory, "Current maximum map count: {}", value);
// Allow 20000 maps for other code and to account for split inaccuracy.
return std::max<s64>(value - 20000, 0);
}
} // namespace
HeapTracker::HeapTracker(Common::HostMemory& buffer)
: m_buffer(buffer), m_max_resident_map_count(GetMaxPermissibleResidentMapCount()) {}
HeapTracker::~HeapTracker() = default;
void HeapTracker::Map(size_t virtual_offset, size_t host_offset, size_t length,
MemoryPermission perm, bool is_separate_heap) {
// When mapping other memory, map pages immediately.
if (!is_separate_heap) {
m_buffer.Map(virtual_offset, host_offset, length, perm, false);
return;
}
{
// We are mapping part of a separate heap.
std::scoped_lock lk{m_lock};
auto* const map = new SeparateHeapMap{
.vaddr = virtual_offset,
.paddr = host_offset,
.size = length,
.tick = m_tick++,
.perm = perm,
.is_resident = false,
};
// Insert into mappings.
m_map_count++;
m_mappings.insert(*map);
}
// Finally, map.
this->DeferredMapSeparateHeap(virtual_offset);
}
void HeapTracker::Unmap(size_t virtual_offset, size_t size, bool is_separate_heap) {
// If this is a separate heap...
if (is_separate_heap) {
std::scoped_lock lk{m_lock};
const SeparateHeapMap key{
.vaddr = virtual_offset,
};
// Split at the boundaries of the region we are removing.
this->SplitHeapMapLocked(virtual_offset);
this->SplitHeapMapLocked(virtual_offset + size);
// Erase all mappings in range.
auto it = m_mappings.find(key);
while (it != m_mappings.end() && it->vaddr < virtual_offset + size) {
// Get underlying item.
auto* const item = std::addressof(*it);
// If resident, erase from resident map.
if (item->is_resident) {
ASSERT(--m_resident_map_count >= 0);
m_resident_mappings.erase(m_resident_mappings.iterator_to(*item));
}
// Erase from map.
ASSERT(--m_map_count >= 0);
it = m_mappings.erase(it);
// Free the item.
delete item;
}
}
// Unmap pages.
m_buffer.Unmap(virtual_offset, size, false);
}
void HeapTracker::Protect(size_t virtual_offset, size_t size, MemoryPermission perm) {
// Ensure no rebuild occurs while reprotecting.
std::shared_lock lk{m_rebuild_lock};
// Split at the boundaries of the region we are reprotecting.
this->SplitHeapMap(virtual_offset, size);
// Declare tracking variables.
const VAddr end = virtual_offset + size;
VAddr cur = virtual_offset;
while (cur < end) {
VAddr next = cur;
bool should_protect = false;
{
std::scoped_lock lk2{m_lock};
const SeparateHeapMap key{
.vaddr = next,
};
// Try to get the next mapping corresponding to this address.
const auto it = m_mappings.nfind(key);
if (it == m_mappings.end()) {
// There are no separate heap mappings remaining.
next = end;
should_protect = true;
} else if (it->vaddr == cur) {
// We are in range.
// Update permission bits.
it->perm = perm;
// Determine next address and whether we should protect.
next = cur + it->size;
should_protect = it->is_resident;
} else /* if (it->vaddr > cur) */ {
// We weren't in range, but there is a block coming up that will be.
next = it->vaddr;
should_protect = true;
}
}
// Clamp to end.
next = (std::min)(next, end);
// Reprotect, if we need to.
if (should_protect) {
m_buffer.Protect(cur, next - cur, perm);
}
// Advance.
cur = next;
}
}
bool HeapTracker::DeferredMapSeparateHeap(u8* fault_address) {
if (m_buffer.IsInVirtualRange(fault_address)) {
return this->DeferredMapSeparateHeap(fault_address - m_buffer.VirtualBasePointer());
}
return false;
}
bool HeapTracker::DeferredMapSeparateHeap(size_t virtual_offset) {
bool rebuild_required = false;
{
std::scoped_lock lk{m_lock};
// Check to ensure this was a non-resident separate heap mapping.
const auto it = this->GetNearestHeapMapLocked(virtual_offset);
if (it == m_mappings.end() || it->is_resident) {
return false;
}
// Update tick before possible rebuild.
it->tick = m_tick++;
// Check if we need to rebuild.
if (m_resident_map_count > m_max_resident_map_count) {
rebuild_required = true;
}
// Map the area.
m_buffer.Map(it->vaddr, it->paddr, it->size, it->perm, false);
// This map is now resident.
it->is_resident = true;
m_resident_map_count++;
m_resident_mappings.insert(*it);
}
if (rebuild_required) {
// A rebuild was required, so perform it now.
this->RebuildSeparateHeapAddressSpace();
}
return true;
}
void HeapTracker::RebuildSeparateHeapAddressSpace() {
std::scoped_lock lk{m_rebuild_lock, m_lock};
ASSERT(!m_resident_mappings.empty());
// Dump half of the mappings.
//
// Despite being worse in theory, this has proven to be better in practice than more
// regularly dumping a smaller amount, because it significantly reduces average case
// lock contention.
std::size_t const desired_count = (std::min)(m_resident_map_count, m_max_resident_map_count) / 2;
std::size_t const evict_count = m_resident_map_count - desired_count;
auto it = m_resident_mappings.begin();
for (size_t i = 0; i < evict_count && it != m_resident_mappings.end(); i++) {
// Unmark and unmap.
it->is_resident = false;
m_buffer.Unmap(it->vaddr, it->size, false);
// Advance.
ASSERT(--m_resident_map_count >= 0);
it = m_resident_mappings.erase(it);
}
}
void HeapTracker::SplitHeapMap(VAddr offset, size_t size) {
std::scoped_lock lk{m_lock};
this->SplitHeapMapLocked(offset);
this->SplitHeapMapLocked(offset + size);
}
void HeapTracker::SplitHeapMapLocked(VAddr offset) {
const auto it = this->GetNearestHeapMapLocked(offset);
if (it == m_mappings.end() || it->vaddr == offset) {
// Not contained or no split required.
return;
}
// Cache the original values.
auto* const left = std::addressof(*it);
const size_t orig_size = left->size;
// Adjust the left map.
const size_t left_size = offset - left->vaddr;
left->size = left_size;
// Create the new right map.
auto* const right = new SeparateHeapMap{
.vaddr = left->vaddr + left_size,
.paddr = left->paddr + left_size,
.size = orig_size - left_size,
.tick = left->tick,
.perm = left->perm,
.is_resident = left->is_resident,
};
// Insert the new right map.
m_map_count++;
m_mappings.insert(*right);
// If resident, also insert into resident map.
if (right->is_resident) {
m_resident_map_count++;
m_resident_mappings.insert(*right);
}
}
HeapTracker::AddrTree::iterator HeapTracker::GetNearestHeapMapLocked(VAddr offset) {
const SeparateHeapMap key{
.vaddr = offset,
};
return m_mappings.find(key);
}
} // namespace Common
+98
View File
@@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <mutex>
#include <set>
#include <shared_mutex>
#include "common/host_memory.h"
#include "common/intrusive_red_black_tree.h"
namespace Common {
struct SeparateHeapMap {
Common::IntrusiveRedBlackTreeNode addr_node{};
Common::IntrusiveRedBlackTreeNode tick_node{};
VAddr vaddr{};
PAddr paddr{};
size_t size{};
size_t tick{};
MemoryPermission perm{};
bool is_resident{};
};
struct SeparateHeapMapAddrComparator {
static constexpr int Compare(const SeparateHeapMap& lhs, const SeparateHeapMap& rhs) {
if (lhs.vaddr < rhs.vaddr) {
return -1;
} else if (lhs.vaddr <= (rhs.vaddr + rhs.size - 1)) {
return 0;
} else {
return 1;
}
}
};
struct SeparateHeapMapTickComparator {
static constexpr int Compare(const SeparateHeapMap& lhs, const SeparateHeapMap& rhs) {
if (lhs.tick < rhs.tick) {
return -1;
} else if (lhs.tick > rhs.tick) {
return 1;
} else {
return SeparateHeapMapAddrComparator::Compare(lhs, rhs);
}
}
};
class HeapTracker {
public:
explicit HeapTracker(Common::HostMemory& buffer);
~HeapTracker();
void Map(size_t virtual_offset, size_t host_offset, size_t length, MemoryPermission perm,
bool is_separate_heap);
void Unmap(size_t virtual_offset, size_t size, bool is_separate_heap);
void Protect(size_t virtual_offset, size_t length, MemoryPermission perm);
u8* VirtualBasePointer() {
return m_buffer.VirtualBasePointer();
}
bool DeferredMapSeparateHeap(u8* fault_address);
bool DeferredMapSeparateHeap(size_t virtual_offset);
private:
using AddrTreeTraits =
Common::IntrusiveRedBlackTreeMemberTraitsDeferredAssert<&SeparateHeapMap::addr_node>;
using AddrTree = AddrTreeTraits::TreeType<SeparateHeapMapAddrComparator>;
using TickTreeTraits =
Common::IntrusiveRedBlackTreeMemberTraitsDeferredAssert<&SeparateHeapMap::tick_node>;
using TickTree = TickTreeTraits::TreeType<SeparateHeapMapTickComparator>;
AddrTree m_mappings{};
TickTree m_resident_mappings{};
private:
void SplitHeapMap(VAddr offset, size_t size);
void SplitHeapMapLocked(VAddr offset);
AddrTree::iterator GetNearestHeapMapLocked(VAddr offset);
void RebuildSeparateHeapAddressSpace();
private:
Common::HostMemory& m_buffer;
const s64 m_max_resident_map_count;
std::shared_mutex m_rebuild_lock{};
std::mutex m_lock{};
s64 m_map_count{};
s64 m_resident_map_count{};
size_t m_tick{};
};
} // namespace Common
+5 -4
View File
@@ -572,8 +572,9 @@ public:
if (True(perms & MemoryPermission::Execute))
prot_flags |= PROT_EXEC;
#endif
int ret = mprotect(virtual_base + virtual_offset, length, prot_flags);
ASSERT_MSG(ret == 0, "mprotect: {} {}", strerror(errno), fd);
int flags = (fd >= 0 ? MAP_SHARED : MAP_PRIVATE) | MAP_FIXED;
void* ret = mmap(virtual_base + virtual_offset, length, prot_flags, flags, fd, host_offset);
ASSERT_MSG(ret != MAP_FAILED, "mmap: {} {}", strerror(errno), fd);
}
void Unmap(size_t virtual_offset, size_t length) {
@@ -587,8 +588,8 @@ public:
auto [merged_pointer, merged_size] =
free_manager.FreeBlock(virtual_base + virtual_offset, length);
int ret = mprotect(merged_pointer, merged_size, PROT_NONE);
ASSERT_MSG(ret == 0, "mmap: {}", strerror(errno));
void* ret = mmap(merged_pointer, merged_size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
ASSERT_MSG(ret != MAP_FAILED, "mmap: {}", strerror(errno));
}
void Protect(size_t virtual_offset, size_t length, bool read, bool write, bool execute) {
+3 -8
View File
@@ -388,13 +388,8 @@ struct Values {
true,
true};
SwitchableSetting<bool> use_asynchronous_gpu_emulation{linkage,
#ifdef __ANDROID__
false,
#else
true,
#endif
"use_asynchronous_gpu_emulation", Category::Renderer};
SwitchableSetting<bool> use_asynchronous_gpu_emulation{
linkage, true, "use_asynchronous_gpu_emulation", Category::Renderer};
// *nix platforms may have issues with the borderless windowed fullscreen mode.
// Default to exclusive fullscreen on these platforms for now.
SwitchableSetting<FullscreenMode, true> fullscreen_mode{linkage,
@@ -547,7 +542,7 @@ struct Values {
true};
SwitchableSetting<bool> async_presentation{linkage,
#ifdef ANDROID
false,
true,
#else
false,
#endif
+12 -5
View File
@@ -616,6 +616,18 @@ add_library(core STATIC
hle/service/caps/caps_u.h
hle/service/cmif_serialization.h
hle/service/cmif_types.h
hle/service/dmnt/cheat_interface.cpp
hle/service/dmnt/cheat_interface.h
hle/service/dmnt/cheat_parser.cpp
hle/service/dmnt/cheat_parser.h
hle/service/dmnt/cheat_process_manager.cpp
hle/service/dmnt/cheat_process_manager.h
hle/service/dmnt/cheat_virtual_machine.cpp
hle/service/dmnt/cheat_virtual_machine.h
hle/service/dmnt/dmnt.cpp
hle/service/dmnt/dmnt.h
hle/service/dmnt/dmnt_results.h
hle/service/dmnt/dmnt_types.h
hle/service/erpt/erpt.cpp
hle/service/erpt/erpt.h
hle/service/es/es.cpp
@@ -1153,11 +1165,6 @@ add_library(core STATIC
loader/xci.h
memory.cpp
memory.h
memory/cheat_engine.cpp
memory/cheat_engine.h
memory/dmnt_cheat_types.h
memory/dmnt_cheat_vm.cpp
memory/dmnt_cheat_vm.h
perf_stats.cpp
perf_stats.h
reporter.cpp
+2 -2
View File
@@ -36,7 +36,7 @@ public:
u64 MemoryRead64(u32 vaddr) override;
std::optional<u32> MemoryReadCode(u32 vaddr) override;
void InstructionSynchronizationBarrierRaised() override {
last_code_addr = u64(-1); //reset back, force refetch
last_code_addr = 0; //reset back, force refetch
}
void MemoryWrite8(u32 vaddr, u8 value) override;
void MemoryWrite16(u32 vaddr, u16 value) override;
@@ -54,7 +54,7 @@ public:
void ReturnException(u32 pc, Dynarmic::HaltReason hr);
//
Dynarmic::CodePage cached_code_page;
u64 last_code_addr = u64(-1);
u64 last_code_addr = 0;
ArmDynarmic32& m_parent;
Core::Memory::Memory& m_memory;
Kernel::KProcess* m_process{};
+2 -2
View File
@@ -45,6 +45,7 @@ Dynarmic::A64::Vector DynarmicCallbacks64::MemoryRead128(u64 vaddr) {
std::optional<u32> DynarmicCallbacks64::MemoryReadCode(u64 vaddr) {
if (!m_memory.IsValidVirtualAddressRange(vaddr, sizeof(u32)))
return std::nullopt;
// return m_memory.Read32(vaddr);
auto const aligned_vaddr = vaddr & ~Core::Memory::YUZU_PAGEMASK;
if (last_code_addr != aligned_vaddr) {
m_memory.ReadBlock(aligned_vaddr, &cached_code_page, sizeof(cached_code_page));
@@ -102,7 +103,6 @@ bool DynarmicCallbacks64::MemoryWriteExclusive128(u64 vaddr, Dynarmic::A64::Vect
}
void DynarmicCallbacks64::InstructionCacheOperationRaised(Dynarmic::A64::InstructionCacheOperation op, u64 value) {
last_code_addr = u64(-1); //invalidate cached page
switch (op) {
case Dynarmic::A64::InstructionCacheOperation::InvalidateByVAToPoU: {
static constexpr u64 ICACHE_LINE_SIZE = 64;
@@ -128,7 +128,7 @@ void DynarmicCallbacks64::ExceptionRaised(u64 pc, Dynarmic::A64::Exception excep
case Dynarmic::A64::Exception::SendEvent:
case Dynarmic::A64::Exception::SendEventLocal:
case Dynarmic::A64::Exception::Yield:
LOG_TRACE(Core_ARM, "ExceptionRaised(exception = {}, pc = {:08X}, code = {:08X}, cached = {:08X})", std::size_t(exception), pc, m_memory.Read32(pc), MemoryReadCode(pc).value_or(0));
LOG_TRACE(Core_ARM, "ExceptionRaised(exception = {}, pc = {:08X}, code = {:08X})", static_cast<std::size_t>(exception), pc, m_memory.Read32(pc));
return;
case Dynarmic::A64::Exception::NoExecuteFault:
LOG_CRITICAL(Core_ARM, "Cannot execute instruction at unmapped address {:#016x}", pc);
+2 -2
View File
@@ -43,7 +43,7 @@ public:
Dynarmic::A64::Vector MemoryRead128(u64 vaddr) override;
std::optional<u32> MemoryReadCode(u64 vaddr) override;
void InstructionSynchronizationBarrierRaised() override {
last_code_addr = u64(-1); //reset back, force refetch
last_code_addr = 0; //reset back, force refetch
}
void MemoryWrite8(u64 vaddr, u8 value) override;
void MemoryWrite16(u64 vaddr, u16 value) override;
@@ -65,7 +65,7 @@ public:
void ReturnException(u64 pc, Dynarmic::HaltReason hr);
Dynarmic::CodePage cached_code_page;
u64 last_code_addr = u64(-1);
u64 last_code_addr = 0;
ArmDynarmic64& m_parent;
Core::Memory::Memory& m_memory;
u64 m_tpidrro_el0{};
+88 -85
View File
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -16,111 +13,117 @@ using namespace Common::ELF;
namespace Core {
namespace Symbols {
struct ModuleHeaderLocation {
u32 version;
u32 header_offset;
u32 version_offset;
};
static_assert(sizeof(ModuleHeaderLocation) == 0x0C);
struct ModuleHeader {
u32 signature;
u32 dynamic_offset;
u32 bss_start_offset;
u32 bss_end_offset;
u32 exception_info_start_offset;
u32 exception_info_end_offset;
u32 module_offset;
u32 relro_start_offset;
u32 full_relro_end_offset;
u32 nx_debug_link_start_offset;
u32 nx_debug_link_end_offset;
u32 note_gnu_build_id_start_offset;
u32 note_gnu_build_id_end_offset;
};
static_assert(sizeof(ModuleHeader) == 0x34);
struct Mod32 {
using Sym = Elf32_Sym;
using Dyn = Elf32_Dyn;
};
struct Mod64 {
using Sym = Elf64_Sym;
using Dyn = Elf64_Dyn;
};
template <typename M, typename F>
static Symbols GetSymbols(F&& ReadBytes) {
const auto Read8 = [&](u64 index) {
template <typename Word, typename ELFSymbol, typename ByteReader>
static Symbols GetSymbols(ByteReader ReadBytes) {
const auto Read8{[&](u64 index) {
u8 ret;
ReadBytes(&ret, index, sizeof(u8));
return ret;
};
ModuleHeaderLocation loc{};
ReadBytes(&loc, 0, sizeof(ModuleHeaderLocation));
ModuleHeader hdr;
ReadBytes(&hdr, loc.header_offset, sizeof(ModuleHeader));
if (hdr.signature != Common::MakeMagic('M', 'O', 'D', '0')) {
}};
const auto Read32{[&](u64 index) {
u32 ret;
ReadBytes(&ret, index, sizeof(u32));
return ret;
}};
const auto ReadWord{[&](u64 index) {
Word ret;
ReadBytes(&ret, index, sizeof(Word));
return ret;
}};
const u32 mod_offset = Read32(4);
if (Read32(mod_offset) != Common::MakeMagic('M', 'O', 'D', '0')) {
return {};
}
VAddr strtab_offs{};
VAddr symtab_offs{};
u64 syment_size = sizeof(typename M::Sym);
VAddr dynamic_offset = loc.header_offset + hdr.dynamic_offset;
VAddr string_table_offset{};
VAddr symbol_table_offset{};
u64 symbol_entry_size{};
const auto dynamic_offset = Read32(mod_offset + 0x4) + mod_offset;
VAddr dynamic_index = dynamic_offset;
while (true) {
typename M::Dyn dyn;
ReadBytes(&dyn, dynamic_offset, sizeof(typename M::Dyn));
dynamic_offset += sizeof(typename M::Dyn);
if (dyn.d_tag == ElfDtNull) {
const Word tag = ReadWord(dynamic_index);
const Word value = ReadWord(dynamic_index + sizeof(Word));
dynamic_index += 2 * sizeof(Word);
if (tag == ElfDtNull) {
break;
}
if (dyn.d_tag == ElfDtStrtab) {
strtab_offs = dyn.d_un.d_ptr;
} else if (dyn.d_tag == ElfDtSymtab) {
symtab_offs = dyn.d_un.d_ptr;
} else if (dyn.d_tag == ElfDtSyment) {
syment_size = dyn.d_un.d_val;
if (tag == ElfDtStrtab) {
string_table_offset = value;
} else if (tag == ElfDtSymtab) {
symbol_table_offset = value;
} else if (tag == ElfDtSyment) {
symbol_entry_size = value;
}
}
if (strtab_offs > 0 && symtab_offs > 0) {
Symbols out;
VAddr symbol_index = symtab_offs;
while (symbol_index < strtab_offs) {
typename M::Sym symbol{};
ReadBytes(&symbol, symbol_index, sizeof(typename M::Sym));
VAddr offs = strtab_offs + symbol.st_name;
std::string name{};
for (u8 c = Read8(offs); c != 0; c = Read8(++offs))
name += char(c);
symbol_index += syment_size;
out[name] = std::make_pair(symbol.st_value, symbol.st_size);
}
return out;
if (string_table_offset == 0 || symbol_table_offset == 0 || symbol_entry_size == 0) {
return {};
}
return {};
Symbols out;
VAddr symbol_index = symbol_table_offset;
while (symbol_index < string_table_offset) {
ELFSymbol symbol{};
ReadBytes(&symbol, symbol_index, sizeof(ELFSymbol));
VAddr string_offset = string_table_offset + symbol.st_name;
std::string name;
for (u8 c = Read8(string_offset); c != 0; c = Read8(++string_offset)) {
name += static_cast<char>(c);
}
symbol_index += symbol_entry_size;
out[name] = std::make_pair(symbol.st_value, symbol.st_size);
}
return out;
}
Symbols GetSymbols(VAddr base, Core::Memory::Memory& memory, bool is_64) {
const auto f = [base, &memory](void* ptr, size_t offset, size_t size) {
memory.ReadBlock(base + offset, ptr, size);
};
return is_64 ? GetSymbols<Mod64>(f) : GetSymbols<Mod32>(f);
const auto ReadBytes{
[&](void* ptr, size_t offset, size_t size) { memory.ReadBlock(base + offset, ptr, size); }};
if (is_64) {
return GetSymbols<u64, Elf64_Sym>(ReadBytes);
} else {
return GetSymbols<u32, Elf32_Sym>(ReadBytes);
}
}
Symbols GetSymbols(std::span<const u8> data, bool is_64) {
const auto f = [data](void* ptr, size_t offset, size_t size) {
const auto ReadBytes{[&](void* ptr, size_t offset, size_t size) {
std::memcpy(ptr, data.data() + offset, size);
};
return is_64 ? GetSymbols<Mod64>(f) : GetSymbols<Mod32>(f);
}};
if (is_64) {
return GetSymbols<u64, Elf64_Sym>(ReadBytes);
} else {
return GetSymbols<u32, Elf32_Sym>(ReadBytes);
}
}
std::optional<std::string> GetSymbolName(const Symbols& symbols, VAddr addr) {
const auto it = std::find_if(symbols.cbegin(), symbols.cend(), [addr](const auto& e) {
auto const [start, size] = e.second;
auto const end = start + size;
return addr >= start && addr < end;
const auto iter = std::find_if(symbols.cbegin(), symbols.cend(), [addr](const auto& pair) {
const auto& [name, sym_info] = pair;
const auto& [start_address, size] = sym_info;
const auto end_address = start_address + size;
return addr >= start_address && addr < end_address;
});
return it != symbols.cend() ? std::optional<std::string>{it->first} : std::nullopt;
if (iter == symbols.cend()) {
return std::nullopt;
}
return iter->first;
}
} // namespace Symbols
+137 -11
View File
@@ -40,6 +40,7 @@
#include "core/hle/service/am/frontend/applets.h"
#include "core/hle/service/am/process_creation.h"
#include "core/hle/service/apm/apm_controller.h"
#include "core/hle/service/dmnt/cheat_process_manager.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/hle/service/glue/glue_manager.h"
#include "core/hle/service/glue/time/static.h"
@@ -54,7 +55,6 @@
#include "core/internal_network/network.h"
#include "core/loader/loader.h"
#include "core/memory.h"
#include "core/memory/cheat_engine.h"
#include "core/perf_stats.h"
#include "core/reporter.h"
#include "core/tools/freezer.h"
@@ -278,8 +278,17 @@ struct System::Impl {
audio_core.emplace(system);
service_manager = std::make_shared<Service::SM::ServiceManager>(kernel);
// Create cheat_manager BEFORE services, as DMNT::LoopProcess needs it
cheat_manager = std::make_unique<Service::DMNT::CheatProcessManager>(system);
services.emplace(service_manager, system, stop_event.get_token());
// Apply any pending cheats that were registered before cheat_manager was initialized
if (pending_cheats.has_pending) {
ApplyPendingCheats(system);
}
is_powered_on = true;
exit_locked = false;
exit_requested = false;
@@ -340,11 +349,6 @@ struct System::Impl {
return init_result;
}
// Initialize cheat engine
if (cheat_engine) {
cheat_engine->Initialize();
}
// Register with applet manager
// All threads are started, begin main process execution, now that we're in the clear
applet_manager.CreateAndInsertByFrontendAppletParameters(std::move(process), params);
@@ -405,7 +409,6 @@ struct System::Impl {
services.reset();
service_manager.reset();
fs_controller.Reset();
cheat_engine.reset();
core_timing.ClearPendingEvents();
app_loader.reset();
audio_core.reset();
@@ -475,7 +478,6 @@ struct System::Impl {
std::optional<Tegra::Host1x::Host1x> host1x_core;
std::optional<Core::DeviceMemory> device_memory;
std::optional<AudioCore::AudioCore> audio_core;
std::optional<Memory::CheatEngine> cheat_engine;
std::optional<Tools::Freezer> memory_freezer;
std::optional<Tools::RenderdocAPI> renderdoc_api;
@@ -495,6 +497,17 @@ struct System::Impl {
std::unique_ptr<Tegra::GPU> gpu_core;
std::stop_source stop_event;
/// Cheat Manager (DMNT)
std::unique_ptr<Service::DMNT::CheatProcessManager> cheat_manager;
/// Pending cheats to register after cheat_manager is initialized
struct PendingCheats {
std::vector<Service::DMNT::CheatEntry> list;
std::array<u8, 32> build_id{};
u64 main_region_begin{};
u64 main_region_size{};
bool has_pending{false};
} pending_cheats;
mutable std::mutex suspend_guard;
std::mutex general_channel_mutex;
std::atomic_bool is_paused{};
@@ -512,6 +525,61 @@ struct System::Impl {
general_channel_event.emplace(*general_channel_context);
}
}
void ApplyPendingCheats(System& system) {
if (!pending_cheats.has_pending || !cheat_manager) {
return;
}
LOG_DEBUG(Core, "Applying {} pending cheats", pending_cheats.list.size());
const auto result = cheat_manager->AttachToApplicationProcess(
pending_cheats.build_id, pending_cheats.main_region_begin,
pending_cheats.main_region_size);
if (result.IsError()) {
LOG_WARNING(Core, "Failed to attach cheat process: result={}", result.raw);
pending_cheats = {};
return;
}
LOG_DEBUG(Core, "Cheat process attached successfully");
for (const auto& entry : pending_cheats.list) {
if (entry.cheat_id == 0 && entry.definition.num_opcodes != 0) {
LOG_DEBUG(Core, "Setting master cheat '{}' with {} opcodes",
entry.definition.readable_name.data(), entry.definition.num_opcodes);
const auto set_result = cheat_manager->SetMasterCheat(entry.definition);
if (set_result.IsError()) {
LOG_WARNING(Core, "Failed to set master cheat: result={}", set_result.raw);
}
break;
}
}
// Add normal cheats (cheat_id != 0)
for (const auto& entry : pending_cheats.list) {
if (entry.cheat_id == 0 || entry.definition.num_opcodes == 0) {
continue;
}
u32 assigned_id = 0;
LOG_DEBUG(Core, "Adding cheat '{}' (enabled={}, {} opcodes)",
entry.definition.readable_name.data(), entry.enabled,
entry.definition.num_opcodes);
const auto add_result = cheat_manager->AddCheat(assigned_id, entry.enabled,
entry.definition);
if (add_result.IsError()) {
LOG_WARNING(Core,
"Failed to add cheat (original_id={} enabled={} name='{}'): result={}",
entry.cheat_id, entry.enabled,
entry.definition.readable_name.data(), add_result.raw);
}
}
// Clear pending cheats
pending_cheats = {};
}
};
System::System() : impl{std::make_unique<Impl>(*this)} {}
@@ -749,11 +817,61 @@ FileSys::VirtualFilesystem System::GetFilesystem() const {
return impl->virtual_filesystem;
}
void System::RegisterCheatList(const std::vector<Memory::CheatEntry>& list,
void System::RegisterCheatList(const std::vector<Service::DMNT::CheatEntry>& list,
const std::array<u8, 32>& build_id, u64 main_region_begin,
u64 main_region_size) {
impl->cheat_engine.emplace(*this, list, build_id);
impl->cheat_engine->SetMainMemoryParameters(main_region_begin, main_region_size);
// If cheat_manager is not yet initialized, cache the cheats for later
if (!impl->cheat_manager) {
impl->pending_cheats.list = list;
impl->pending_cheats.build_id = build_id;
impl->pending_cheats.main_region_begin = main_region_begin;
impl->pending_cheats.main_region_size = main_region_size;
impl->pending_cheats.has_pending = true;
LOG_INFO(Core, "Cached {} cheats for later registration", list.size());
return;
}
// Attach cheat process to the current application process
const auto result = impl->cheat_manager->AttachToApplicationProcess(build_id, main_region_begin,
main_region_size);
if (result.IsError()) {
LOG_WARNING(Core, "Failed to attach cheat process: result={}", result.raw);
return;
}
// Empty list: nothing more to do
if (list.empty()) {
return;
}
// Set master cheat if present (cheat_id == 0)
for (const auto& entry : list) {
if (entry.cheat_id == 0 && entry.definition.num_opcodes != 0) {
const auto set_result = impl->cheat_manager->SetMasterCheat(entry.definition);
if (set_result.IsError()) {
LOG_WARNING(Core, "Failed to set master cheat: result={}", set_result.raw);
}
// Only one master cheat allowed
break;
}
}
// Add normal cheats (cheat_id != 0)
for (const auto& entry : list) {
if (entry.cheat_id == 0 || entry.definition.num_opcodes == 0) {
continue;
}
u32 assigned_id = 0;
const auto add_result = impl->cheat_manager->AddCheat(assigned_id, entry.enabled,
entry.definition);
if (add_result.IsError()) {
LOG_WARNING(Core,
"Failed to add cheat (original_id={} enabled={} name='{}'): result={}",
entry.cheat_id, entry.enabled,
entry.definition.readable_name.data(), add_result.raw);
}
}
}
void System::SetFrontendAppletSet(Service::AM::Frontend::FrontendAppletSet&& set) {
@@ -893,6 +1011,14 @@ Tools::RenderdocAPI& System::GetRenderdocAPI() {
return *impl->renderdoc_api;
}
Service::DMNT::CheatProcessManager& System::GetCheatManager() {
return *impl->cheat_manager;
}
const Service::DMNT::CheatProcessManager& System::GetCheatManager() const {
return *impl->cheat_manager;
}
void System::RunServer(std::unique_ptr<Service::ServerManager>&& server_manager) {
return impl->kernel.RunServer(std::move(server_manager));
}
+10 -2
View File
@@ -17,6 +17,7 @@
#include "common/common_types.h"
#include "core/file_sys/vfs/vfs_types.h"
#include "core/hle/service/dmnt/dmnt_types.h"
#include "core/hle/service/os/event.h"
#include "core/hle/service/kernel_helpers.h"
@@ -45,10 +46,14 @@ enum class ResultStatus : u16;
} // namespace Loader
namespace Core::Memory {
struct CheatEntry;
class Memory;
} // namespace Core::Memory
namespace Service::DMNT {
class CheatProcessManager;
struct CheatEntry;
}
namespace Service {
namespace Account {
@@ -335,7 +340,7 @@ public:
[[nodiscard]] FileSys::VirtualFilesystem GetFilesystem() const;
void RegisterCheatList(const std::vector<Memory::CheatEntry>& list,
void RegisterCheatList(const std::vector<Service::DMNT::CheatEntry>& list,
const std::array<u8, 0x20>& build_id, u64 main_region_begin,
u64 main_region_size);
@@ -376,6 +381,9 @@ public:
[[nodiscard]] Tools::RenderdocAPI& GetRenderdocAPI();
[[nodiscard]] Service::DMNT::CheatProcessManager& GetCheatManager();
[[nodiscard]] const Service::DMNT::CheatProcessManager& GetCheatManager() const;
void SetExitLocked(bool locked);
bool GetExitLocked() const;
+326 -287
View File
@@ -27,12 +27,13 @@
#include "core/file_sys/vfs/vfs_cached.h"
#include "core/file_sys/vfs/vfs_layered.h"
#include "core/file_sys/vfs/vfs_vector.h"
#include "core/hle/service/dmnt/cheat_parser.h"
#include "core/hle/service/dmnt/dmnt_types.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/hle/service/ns/language.h"
#include "core/hle/service/set/settings_server.h"
#include "core/loader/loader.h"
#include "core/loader/nso.h"
#include "core/memory/cheat_engine.h"
namespace FileSys {
namespace {
@@ -64,16 +65,15 @@ std::string FormatTitleVersion(u32 version,
return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]);
}
// Returns a directory with name matching name case-insensitive. Returns nullptr if directory
// doesn't have a directory with name.
VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) {
// Returns a directory with name matching case-insensitively.
// Returns nullptr if directory doesn't contain a subdirectory with the given name.
VirtualDir FindSubdirectoryCaseless(const VirtualDir& dir, std::string_view name) {
#ifdef _WIN32
return dir->GetSubdirectory(name);
#else
const auto subdirs = dir->GetSubdirectories();
for (const auto& subdir : subdirs) {
std::string dir_name = Common::ToLower(subdir->GetName());
if (dir_name == name) {
const auto target = Common::ToLower(std::string(name));
for (const auto& subdir : dir->GetSubdirectories()) {
if (Common::ToLower(subdir->GetName()) == target) {
return subdir;
}
}
@@ -82,36 +82,36 @@ VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name)
#endif
}
std::optional<std::vector<Core::Memory::CheatEntry>> ReadCheatFileFromFolder(
std::optional<std::vector<Service::DMNT::CheatEntry>> ReadCheatFileFromFolder(
u64 title_id, const PatchManager::BuildID& build_id_, const VirtualDir& base_path, bool upper) {
const auto build_id_raw = Common::HexToString(build_id_, upper);
const auto build_id = build_id_raw.substr(0, sizeof(u64) * 2);
const auto build_id = build_id_raw.substr(0, std::min(build_id_raw.size(), sizeof(u64) * 2));
const auto file = base_path->GetFile(fmt::format("{}.txt", build_id));
if (file == nullptr) {
LOG_INFO(Common_Filesystem, "No cheats file found for title_id={:016X}, build_id={}",
title_id, build_id);
LOG_DEBUG(Common_Filesystem, "No cheats file found for title_id={:016X}, build_id={}",
title_id, build_id);
return std::nullopt;
}
std::vector<u8> data(file->GetSize());
if (file->Read(data.data(), data.size()) != data.size()) {
LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}, build_id={}",
title_id, build_id);
LOG_WARNING(Common_Filesystem,
"Failed to read cheats file for title_id={:016X}, build_id={}", title_id,
build_id);
return std::nullopt;
}
const Core::Memory::TextCheatParser parser;
const Service::DMNT::CheatParser parser;
return parser.Parse(std::string_view(reinterpret_cast<const char*>(data.data()), data.size()));
}
void AppendCommaIfNotEmpty(std::string& to, std::string_view with) {
if (to.empty()) {
to += with;
} else {
if (!to.empty()) {
to += ", ";
to += with;
}
to += with;
}
bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
@@ -130,8 +130,7 @@ std::string GetUpdateVersionStringFromSlot(const ContentProvider* provider, u64
}
auto control_nca = provider->GetEntry(update_tid, ContentRecordType::Control);
if (control_nca == nullptr ||
control_nca->GetStatus() != Loader::ResultStatus::Success) {
if (control_nca == nullptr || control_nca->GetStatus() != Loader::ResultStatus::Success) {
return {};
}
@@ -169,16 +168,11 @@ u64 PatchManager::GetTitleID() const {
return title_id;
}
VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id);
if (exefs == nullptr)
return exefs;
PatchManager::UpdateResolution PatchManager::GetActiveUpdate(ContentRecordType type) const {
UpdateResolution update_res{};
const auto& disabled = Settings::values.disabled_addons[title_id];
bool update_disabled = true;
std::optional<u32> enabled_version;
bool checked_external = false;
bool checked_manual = false;
@@ -195,8 +189,10 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
checked_external = true;
for (const auto& update_entry : update_versions) {
if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
update_disabled = false;
enabled_version = update_entry.version;
update_res.update_disabled = false;
update_res.update_active_version = update_entry.version;
update_res.update_active_raw = external_provider->GetEntryForVersion(
update_tid, type, update_entry.version);
break;
}
}
@@ -214,8 +210,10 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
checked_manual = true;
for (const auto& update_entry : manual_update_versions) {
if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
update_disabled = false;
enabled_version = update_entry.version;
update_res.update_disabled = false;
update_res.update_active_version = update_entry.version;
update_res.update_active_raw = manual_provider->GetEntryForVersion(
update_tid, type, update_entry.version);
break;
}
}
@@ -227,37 +225,41 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
// check for original NAND style
// Check NAND if: no external updates exist, OR all external updates are disabled
if (!checked_external && !checked_manual) {
// Only enable NAND update if it exists AND is not disabled
// We need to check if an update actually exists in the content provider
const bool has_nand_update = content_provider.HasEntry(update_tid, ContentRecordType::Program);
const bool nand_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
const bool sdmc_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
const bool generic_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
if (has_nand_update) {
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
const bool generic_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
if (!nand_disabled && !sdmc_disabled && !generic_disabled) {
update_disabled = false;
}
if (!nand_disabled && !sdmc_disabled && !generic_disabled) {
update_res.update_active_raw = content_provider.GetEntryRaw(update_tid, type);
update_res.update_disabled = update_res.update_active_raw == nullptr;
}
} else if (update_disabled && content_union) {
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
} else if (update_res.update_disabled && content_union) {
const bool nand_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
const bool sdmc_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
if (!nand_disabled || !sdmc_disabled) {
const auto nand_sdmc_entries = content_union->ListEntriesFilterOrigin(
std::nullopt, TitleType::Update, ContentRecordType::Program, update_tid);
std::nullopt, TitleType::Update, type, update_tid);
for (const auto& [slot, entry] : nand_sdmc_entries) {
if (slot == ContentProviderUnionSlot::UserNAND ||
slot == ContentProviderUnionSlot::SysNAND) {
if (!nand_disabled) {
update_disabled = false;
update_res.update_active_raw =
content_provider.GetEntryRaw(update_tid, type);
update_res.update_disabled = update_res.update_active_raw == nullptr;
break;
}
} else if (slot == ContentProviderUnionSlot::SDMC) {
if (!sdmc_disabled) {
update_disabled = false;
update_res.update_active_raw =
content_provider.GetEntryRaw(update_tid, type);
update_res.update_disabled = update_res.update_active_raw == nullptr;
break;
}
}
@@ -265,44 +267,36 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
}
}
// Game Updates
return update_res;
}
VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id);
if (exefs == nullptr)
return exefs;
const auto update_tid = GetUpdateTitleID(title_id);
const auto update_res = GetActiveUpdate();
std::unique_ptr<NCA> update = nullptr;
// If we have a specific enabled version from external provider, use it
if (enabled_version.has_value() && content_union) {
const auto* external_provider = content_union->GetExternalProvider();
if (external_provider) {
auto file = external_provider->GetEntryForVersion(update_tid, ContentRecordType::Program, *enabled_version);
if (file != nullptr) {
update = std::make_unique<NCA>(file);
}
}
// Also try ManualContentProvider
if (update == nullptr) {
const auto* manual_provider = static_cast<const ManualContentProvider*>(
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
if (manual_provider) {
auto file = manual_provider->GetEntryForVersion(update_tid, ContentRecordType::Program, *enabled_version);
if (file != nullptr) {
update = std::make_unique<NCA>(file);
}
}
}
if (update_res.update_active_version.has_value() && update_res.update_active_raw != nullptr) {
update = std::make_unique<NCA>(update_res.update_active_raw);
}
// Fallback to regular content provider if no external update was loaded
if (update == nullptr && !update_disabled) {
if (update == nullptr && !update_res.update_disabled) {
update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
}
if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr) {
if (!update_res.update_disabled && update != nullptr && update->GetExeFS() != nullptr) {
LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully",
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
exefs = update->GetExeFS();
}
// LayeredExeFS
const auto& disabled = Settings::values.disabled_addons[title_id];
const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(title_id);
@@ -468,7 +462,8 @@ bool PatchManager::HasNSOPatch(const BuildID& build_id_, std::string_view name)
return !CollectPatches(patch_dirs, build_id).empty();
}
std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList(const BuildID& build_id_) const {
std::vector<Service::DMNT::CheatEntry> PatchManager::CreateCheatList(
const BuildID& build_id_) const {
const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
if (load_dir == nullptr) {
LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
@@ -477,35 +472,72 @@ std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList(const BuildI
const auto& disabled = Settings::values.disabled_addons[title_id];
auto patch_dirs = load_dir->GetSubdirectories();
std::sort(patch_dirs.begin(), patch_dirs.end(), [](auto const& l, auto const& r) { return l->GetName() < r->GetName(); });
std::sort(patch_dirs.begin(), patch_dirs.end(),
[](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
// <mod dir> / <folder> / cheats / <build id>.txt
std::vector<Core::Memory::CheatEntry> out;
// Load cheats from: <mod dir>/<folder>/cheats/<build_id>.txt
std::vector<Service::DMNT::CheatEntry> out;
for (const auto& subdir : patch_dirs) {
if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) == disabled.cend()) {
if (auto cheats_dir = FindSubdirectoryCaseless(subdir, "cheats"); cheats_dir != nullptr) {
if (auto const res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true))
std::copy(res->begin(), res->end(), std::back_inserter(out));
if (auto const res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false))
std::copy(res->begin(), res->end(), std::back_inserter(out));
const auto mod_name = subdir->GetName();
// Skip entirely disabled mods
if (std::find(disabled.cbegin(), disabled.cend(), mod_name) != disabled.cend()) {
continue;
}
auto cheats_dir = FindSubdirectoryCaseless(subdir, "cheats");
if (cheats_dir == nullptr) {
continue;
}
// Try uppercase build_id first, then lowercase
std::optional<std::vector<Service::DMNT::CheatEntry>> cheat_entries;
if (auto res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true)) {
cheat_entries = std::move(res);
} else if (auto res_lower =
ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false)) {
cheat_entries = std::move(res_lower);
}
if (cheat_entries) {
for (auto& entry : *cheat_entries) {
// Check if this individual cheat is disabled
const std::string cheat_name = entry.definition.readable_name.data();
const std::string cheat_key = mod_name + "::" + cheat_name;
if (std::find(disabled.cbegin(), disabled.cend(), cheat_key) != disabled.cend()) {
// Individual cheat is disabled - mark it as disabled but still include it
entry.enabled = false;
}
out.push_back(entry);
}
}
}
// Uncareless user-friendly loading of patches (must start with 'cheat_')
// <mod dir> / <cheat file>.txt
for (auto const& f : load_dir->GetFiles()) {
auto const name = f->GetName();
if (name.starts_with("cheat_") && std::find(disabled.cbegin(), disabled.cend(), name) == disabled.cend()) {
std::vector<u8> data(f->GetSize());
if (f->Read(data.data(), data.size()) == data.size()) {
const Core::Memory::TextCheatParser parser;
auto const res = parser.Parse(std::string_view(reinterpret_cast<const char*>(data.data()), data.size()));
std::copy(res.begin(), res.end(), std::back_inserter(out));
} else {
LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}", title_id);
}
// User-friendly cheat loading from: <mod dir>/cheat_*.txt
for (const auto& file : load_dir->GetFiles()) {
const auto& name = file->GetName();
if (!name.starts_with("cheat_")) {
continue;
}
if (std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend()) {
continue;
}
std::vector<u8> data(file->GetSize());
if (file->Read(data.data(), data.size()) != static_cast<size_t>(data.size())) {
LOG_WARNING(Common_Filesystem, "Failed to read cheat file '{}' for title_id={:016X}",
name, title_id);
continue;
}
const Service::DMNT::CheatParser parser;
auto entries =
parser.Parse(std::string_view(reinterpret_cast<const char*>(data.data()), data.size()));
out.insert(out.end(), entries.begin(), entries.end());
}
return out;
}
@@ -569,11 +601,12 @@ static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType t
layers.emplace_back(std::move(extracted));
auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers));
auto layered_ext = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers_ext));
if (layered == nullptr && layered_ext == nullptr) {
if (layered == nullptr) {
return;
}
auto layered_ext = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers_ext));
auto packed = CreateRomFS(std::move(layered), std::move(layered_ext));
if (packed == nullptr) {
return;
@@ -596,105 +629,22 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
auto romfs = base_romfs;
// Game Updates
const auto update_tid = GetUpdateTitleID(title_id);
const auto& disabled = Settings::values.disabled_addons[title_id];
const auto update_res = GetActiveUpdate(type);
bool update_disabled = true;
std::optional<u32> enabled_version;
VirtualFile update_raw = nullptr;
bool checked_external = false;
bool checked_manual = false;
const auto* content_union = static_cast<const ContentProviderUnion*>(&content_provider);
if (content_union) {
// First, check ExternalContentProvider
const auto* external_provider = content_union->GetExternalProvider();
if (external_provider) {
const auto update_versions = external_provider->ListUpdateVersions(update_tid);
if (!update_versions.empty()) {
checked_external = true;
for (const auto& update_entry : update_versions) {
if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
update_disabled = false;
enabled_version = update_entry.version;
update_raw = external_provider->GetEntryForVersion(update_tid, type, update_entry.version);
break;
}
}
}
}
if (!checked_external) {
const auto* manual_provider = static_cast<const ManualContentProvider*>(
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
if (manual_provider) {
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
if (!manual_update_versions.empty()) {
checked_manual = true;
for (const auto& update_entry : manual_update_versions) {
if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
update_disabled = false;
enabled_version = update_entry.version;
update_raw = manual_provider->GetEntryForVersion(update_tid, type, update_entry.version);
break;
}
}
}
}
}
}
if (!checked_external && !checked_manual) {
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
const bool generic_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
if (!nand_disabled && !sdmc_disabled && !generic_disabled) {
update_disabled = false;
}
if (!update_disabled) {
update_raw = content_provider.GetEntryRaw(update_tid, type);
}
} else if (update_disabled && content_union) {
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
if (!nand_disabled || !sdmc_disabled) {
const auto nand_sdmc_entries = content_union->ListEntriesFilterOrigin(
std::nullopt, TitleType::Update, type, update_tid);
for (const auto& [slot, entry] : nand_sdmc_entries) {
if (slot == ContentProviderUnionSlot::UserNAND ||
slot == ContentProviderUnionSlot::SysNAND) {
if (!nand_disabled) {
update_disabled = false;
update_raw = content_provider.GetEntryRaw(update_tid, type);
break;
}
} else if (slot == ContentProviderUnionSlot::SDMC) {
if (!sdmc_disabled) {
update_disabled = false;
update_raw = content_provider.GetEntryRaw(update_tid, type);
break;
}
}
}
}
}
if (!update_disabled && update_raw != nullptr && base_nca != nullptr) {
const auto new_nca = std::make_shared<NCA>(update_raw, base_nca);
if (!update_res.update_disabled && update_res.update_active_raw != nullptr &&
base_nca != nullptr) {
const auto new_nca = std::make_shared<NCA>(update_res.update_active_raw, base_nca);
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
new_nca->GetRomFS() != nullptr) {
LOG_INFO(Loader, " RomFS: Update ({}) applied successfully",
enabled_version.has_value() ? FormatTitleVersion(*enabled_version) :
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
LOG_INFO(
Loader, " RomFS: Update ({}) applied successfully",
update_res.update_active_version.has_value()
? FormatTitleVersion(*update_res.update_active_version)
: FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
romfs = new_nca->GetRomFS();
}
} else if (!update_disabled && packed_update_raw != nullptr && base_nca != nullptr) {
} else if (!update_res.update_disabled && packed_update_raw != nullptr && base_nca != nullptr) {
const auto new_nca = std::make_shared<NCA>(packed_update_raw, base_nca);
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
new_nca->GetRomFS() != nullptr) {
@@ -711,6 +661,53 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
return romfs;
}
PatchManager::BuildID PatchManager::GetBuildID(VirtualFile update_raw) const {
BuildID build_id{};
// Get the base NCA
const auto base_nca = content_provider.GetEntry(title_id, ContentRecordType::Program);
if (base_nca == nullptr) {
return build_id;
}
// Try to get ExeFS from update first, then base
VirtualDir exefs;
const auto update_res = GetActiveUpdate();
if (!update_res.update_disabled && update_res.update_active_raw != nullptr) {
const auto update = std::make_shared<NCA>(update_res.update_active_raw, base_nca.get());
if (update->GetStatus() == Loader::ResultStatus::Success && update->GetExeFS() != nullptr) {
exefs = update->GetExeFS();
}
} else if (update_raw != nullptr) {
const auto new_nca = std::make_shared<NCA>(update_raw, base_nca.get());
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
new_nca->GetExeFS() != nullptr) {
exefs = new_nca->GetExeFS();
}
}
if (exefs == nullptr) {
exefs = base_nca->GetExeFS();
}
if (exefs == nullptr) {
return build_id;
}
// Try to read the main NSO header
const auto main_file = exefs->GetFile("main");
if (main_file == nullptr || main_file->GetSize() < sizeof(Loader::NSOHeader)) {
return build_id;
}
Loader::NSOHeader header{};
if (main_file->Read(reinterpret_cast<u8*>(&header), sizeof(header)) == sizeof(header)) {
build_id = header.build_id;
}
return build_id;
}
std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
if (title_id == 0) {
return {};
@@ -748,7 +745,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.program_id = title_id,
.title_id = update_tid,
.source = PatchSource::External,
.numeric_version = update_entry.version};
.numeric_version = update_entry.version,
.parent_name = ""};
external_update_patches.push_back(update_patch);
}
@@ -768,7 +766,6 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
const auto update_disabled =
IsVersionedExternalUpdateDisabled(disabled, update_entry.version);
Patch update_patch = {.enabled = !update_disabled,
.name = "Update",
.version = version_str,
@@ -813,17 +810,17 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
std::string source_suffix;
switch (slot) {
case ContentProviderUnionSlot::UserNAND:
case ContentProviderUnionSlot::SysNAND:
source_type = PatchSource::NAND;
source_suffix = " (NAND)";
break;
case ContentProviderUnionSlot::SDMC:
source_type = PatchSource::SDMC;
source_suffix = " (SDMC)";
break;
default:
break;
case ContentProviderUnionSlot::UserNAND:
case ContentProviderUnionSlot::SysNAND:
source_type = PatchSource::NAND;
source_suffix = " (NAND)";
break;
case ContentProviderUnionSlot::SDMC:
source_type = PatchSource::SDMC;
source_suffix = " (SDMC)";
break;
default:
break;
}
std::string version_str;
@@ -894,96 +891,137 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
}
}
// Check if we have a valid build_id for cheat enumeration
const auto build_id = GetBuildID(update_raw);
const bool has_build_id =
std::any_of(build_id.begin(), build_id.end(), [](u8 b) { return b != 0; });
// General Mods (LayeredFS and IPS)
const auto mod_dir = fs_controller.GetModificationLoadRoot(title_id);
if (mod_dir != nullptr) {
for (auto const& f : mod_dir->GetFiles())
if (auto const name = f->GetName(); name.starts_with("cheat_")) {
auto const mod_disabled = std::find(disabled.begin(), disabled.end(), name) != disabled.end();
out.push_back({
.enabled = !mod_disabled,
.name = name,
.version = "Cheats",
.type = PatchType::Mod,
.program_id = title_id,
.title_id = title_id,
.source = PatchSource::Unknown,
.location = f->GetFullPath(),
});
}
const auto sdmc_mod_dir = fs_controller.GetSDMCModificationLoadRoot(title_id);
for (const auto& mod : mod_dir->GetSubdirectories()) {
for (const auto& root : std::array{mod_dir, sdmc_mod_dir}) {
if (root == nullptr)
continue;
const bool is_sdmc = (root == sdmc_mod_dir);
const std::vector<VirtualDir> mods =
is_sdmc ? std::vector{root} : root->GetSubdirectories();
for (const auto& mod : mods) {
std::string mod_name = is_sdmc ? "SDMC" : mod->GetName();
std::string types;
bool has_cheats = false;
const auto exefs_dir = FindSubdirectoryCaseless(mod, "exefs");
if (IsDirValidAndNonEmpty(exefs_dir)) {
bool ips = false;
bool ipswitch = false;
bool layeredfs = false;
for (const auto& file : exefs_dir->GetFiles()) {
if (file->GetExtension() == "ips") {
ips = true;
} else if (file->GetExtension() == "pchtxt") {
ipswitch = true;
} else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(),
file->GetName()) != EXEFS_FILE_NAMES.end()) {
layeredfs = true;
}
}
if (ips)
AppendCommaIfNotEmpty(types, "IPS");
if (ipswitch)
AppendCommaIfNotEmpty(types, "IPSwitch");
if (layeredfs)
if (is_sdmc) {
AppendCommaIfNotEmpty(types, "LayeredExeFS");
} else {
bool ips = false, ipswitch = false, layeredfs = false;
for (const auto& file : exefs_dir->GetFiles()) {
if (file->GetExtension() == "ips")
ips = true;
else if (file->GetExtension() == "pchtxt")
ipswitch = true;
else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(),
file->GetName()) != EXEFS_FILE_NAMES.end())
layeredfs = true;
}
if (ips)
AppendCommaIfNotEmpty(types, "IPS");
if (ipswitch)
AppendCommaIfNotEmpty(types, "IPSwitch");
if (layeredfs)
AppendCommaIfNotEmpty(types, "LayeredExeFS");
}
}
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfs")) ||
IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfslite")))
AppendCommaIfNotEmpty(types, "LayeredFS");
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfs_ext")))
AppendCommaIfNotEmpty(types, "ExtLayeredFS");
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "cheats")))
const auto cheats_dir = FindSubdirectoryCaseless(mod, "cheats");
if (IsDirValidAndNonEmpty(cheats_dir)) {
has_cheats = true;
AppendCommaIfNotEmpty(types, "Cheats");
}
if (has_cheats && is_sdmc)
mod_name = "Atmosphere";
if (types.empty())
continue;
const auto mod_disabled = std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end();
const auto mod_disabled =
std::find(disabled.begin(), disabled.end(), mod_name) != disabled.end();
out.push_back({.enabled = !mod_disabled,
.name = mod->GetName(),
.name = mod_name,
.version = types,
.type = PatchType::Mod,
.program_id = title_id,
.title_id = title_id,
.source = PatchSource::Unknown,
.location = mod->GetFullPath()});
}
}
.location = mod->GetFullPath(),
.parent_name = ""});
// SDMC mod directory (RomFS LayeredFS)
const auto sdmc_mod_dir = fs_controller.GetSDMCModificationLoadRoot(title_id);
if (sdmc_mod_dir != nullptr) {
std::string types;
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "exefs")))
AppendCommaIfNotEmpty(types, "LayeredExeFS");
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs")) ||
IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfslite")))
AppendCommaIfNotEmpty(types, "LayeredFS");
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs_ext")))
AppendCommaIfNotEmpty(types, "ExtLayeredFS");
// Add individual cheats as sub-entries
if (has_cheats) {
std::vector<Service::DMNT::CheatEntry> cheat_entries;
if (has_build_id) {
if (auto res = ReadCheatFileFromFolder(title_id, build_id, cheats_dir, true))
cheat_entries = std::move(*res);
else if (auto res_lower =
ReadCheatFileFromFolder(title_id, build_id, cheats_dir, false))
cheat_entries = std::move(*res_lower);
}
if (!types.empty()) {
const auto mod_disabled =
std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end();
out.push_back({.enabled = !mod_disabled,
.name = "SDMC",
.version = types,
.type = PatchType::Mod,
.program_id = title_id,
.title_id = title_id,
.source = PatchSource::Unknown});
if (!cheat_entries.empty()) {
for (const auto& cheat : cheat_entries) {
if (cheat.cheat_id <= 1 || cheat.definition.readable_name[0] == '\0')
continue;
const std::string cheat_name = cheat.definition.readable_name.data();
const std::string cheat_key =
(is_sdmc ? "SDMC" : mod->GetName()) + "::" + cheat_name;
out.push_back({
.enabled = std::find(disabled.begin(), disabled.end(), cheat_key) ==
disabled.end(),
.name = cheat_name,
.version = types,
.type = PatchType::Cheat,
.program_id = title_id,
.title_id = title_id,
.parent_name = mod_name,
.cheat_compat = CheatCompatibility::Compatible,
});
}
} else {
std::string title;
for (const auto& cheat_file : cheats_dir->GetFiles()) {
if (!cheat_file->GetName().ends_with(".txt"))
continue;
std::vector<u8> data(cheat_file->GetSize());
if (cheat_file->Read(data.data(), data.size()) == data.size()) {
const auto entries =
Service::DMNT::CheatParser{}.Parse(std::string_view(
reinterpret_cast<const char*>(data.data()), data.size()));
if (entries.size() > 1)
title = entries[1].definition.readable_name.data();
}
break;
}
out.push_back({
.enabled = false,
.name = title.empty() ? "Incompatible cheat"
: fmt::format("Incompatible cheat: {}", title),
.version = types,
.type = PatchType::Cheat,
.program_id = title_id,
.title_id = title_id,
.parent_name = mod_name,
.cheat_compat = CheatCompatibility::Incompatible,
});
}
}
}
}
@@ -993,8 +1031,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
bool has_nand_dlc = false;
bool has_sdmc_dlc = false;
bool has_other_dlc = false;
const auto dlc_entries_with_origin =
content_union->ListEntriesFilterOrigin(std::nullopt, TitleType::AOC, ContentRecordType::Data);
const auto dlc_entries_with_origin = content_union->ListEntriesFilterOrigin(
std::nullopt, TitleType::AOC, ContentRecordType::Data);
dlc_match.reserve(dlc_entries_with_origin.size());
for (const auto& [slot, entry] : dlc_entries_with_origin) {
@@ -1025,20 +1063,20 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
}
switch (slot) {
case ContentProviderUnionSlot::External:
case ContentProviderUnionSlot::FrontendManual:
has_external_dlc = true;
break;
case ContentProviderUnionSlot::UserNAND:
case ContentProviderUnionSlot::SysNAND:
has_nand_dlc = true;
break;
case ContentProviderUnionSlot::SDMC:
has_sdmc_dlc = true;
break;
default:
has_other_dlc = true;
break;
case ContentProviderUnionSlot::External:
case ContentProviderUnionSlot::FrontendManual:
has_external_dlc = true;
break;
case ContentProviderUnionSlot::UserNAND:
case ContentProviderUnionSlot::SysNAND:
has_nand_dlc = true;
break;
case ContentProviderUnionSlot::SDMC:
has_sdmc_dlc = true;
break;
default:
has_other_dlc = true;
break;
}
dlc_match.push_back(entry);
}
@@ -1070,7 +1108,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.type = PatchType::DLC,
.program_id = title_id,
.title_id = dlc_match.back().title_id,
.source = dlc_source});
.source = dlc_source,
.parent_name = ""});
}
return out;
+25 -4
View File
@@ -10,10 +10,10 @@
#include <memory>
#include <optional>
#include <string>
#include "common/common_types.h"
#include "core/file_sys/nca_metadata.h"
#include "core/file_sys/vfs/vfs_types.h"
#include "core/memory/dmnt_cheat_types.h"
namespace Core {
class System;
@@ -23,13 +23,17 @@ namespace Service::FileSystem {
class FileSystemController;
}
namespace Service::DMNT {
struct CheatEntry;
}
namespace FileSys {
class ContentProvider;
class NCA;
class NACP;
enum class PatchType { Update, DLC, Mod };
enum class PatchType { Update, DLC, Mod, Cheat };
enum class PatchSource {
Unknown,
@@ -39,6 +43,11 @@ enum class PatchSource {
Packed,
};
enum class CheatCompatibility : u8 {
Incompatible,
Compatible,
};
struct Patch {
bool enabled;
std::string name;
@@ -49,6 +58,8 @@ struct Patch {
PatchSource source;
std::string location;
u32 numeric_version{0};
std::string parent_name;
CheatCompatibility cheat_compat{CheatCompatibility::Incompatible};
};
// A centralized class to manage patches to games.
@@ -79,7 +90,7 @@ public:
[[nodiscard]] bool HasNSOPatch(const BuildID& build_id, std::string_view name) const;
// Creates a CheatList object with all
[[nodiscard]] std::vector<Core::Memory::CheatEntry> CreateCheatList(
[[nodiscard]] std::vector<Service::DMNT::CheatEntry> CreateCheatList(
const BuildID& build_id) const;
// Currently tracked RomFS patches:
@@ -90,9 +101,11 @@ public:
VirtualFile packed_update_raw = nullptr,
bool apply_layeredfs = true) const;
// Returns a vector of patches
// Returns a vector of patches including individual cheats
[[nodiscard]] std::vector<Patch> GetPatches(VirtualFile update_raw = nullptr) const;
[[nodiscard]] BuildID GetBuildID(VirtualFile update_raw = nullptr) const;
// If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails,
// it will fallback to the Meta-type NCA of the base game. If that fails, the result will be
// std::nullopt
@@ -109,6 +122,14 @@ private:
[[nodiscard]] std::vector<VirtualFile> CollectPatches(const std::vector<VirtualDir>& patch_dirs,
const std::string& build_id) const;
struct UpdateResolution {
bool update_disabled{true};
VirtualFile update_active_raw;
std::optional<u32> update_active_version;
};
[[nodiscard]] UpdateResolution GetActiveUpdate(
ContentRecordType type = ContentRecordType::Program) const;
u64 title_id;
const Service::FileSystem::FileSystemController& fs_controller;
const ContentProvider& content_provider;
+2 -1
View File
@@ -33,7 +33,8 @@ IScreenShotApplicationService::IScreenShotApplicationService(
IScreenShotApplicationService::~IScreenShotApplicationService() = default;
Result IScreenShotApplicationService::SetShimLibraryVersion(ShimLibraryVersion library_version, ClientAppletResourceUserId aruid) {
Result IScreenShotApplicationService::SetShimLibraryVersion(ShimLibraryVersion library_version,
ClientAppletResourceUserId aruid) {
LOG_WARNING(Service_Capture, "(STUBBED) called. library_version={}, applet_resource_user_id={}",
library_version, aruid.pid);
R_SUCCEED();
@@ -0,0 +1,238 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "core/hle/service/cmif_serialization.h"
#include "core/hle/service/dmnt/cheat_interface.h"
#include "core/hle/service/dmnt/cheat_process_manager.h"
#include "core/hle/service/dmnt/dmnt_results.h"
#include "core/hle/service/dmnt/dmnt_types.h"
namespace Service::DMNT {
ICheatInterface::ICheatInterface(Core::System& system_, CheatProcessManager& manager)
: ServiceFramework{system_, "dmnt:cht"}, cheat_process_manager{manager} {
// clang-format off
static const FunctionInfo functions[] = {
{65000, C<&ICheatInterface::HasCheatProcess>, "HasCheatProcess"},
{65001, C<&ICheatInterface::GetCheatProcessEvent>, "GetCheatProcessEvent"},
{65002, C<&ICheatInterface::GetCheatProcessMetadata>, "GetCheatProcessMetadata"},
{65003, C<&ICheatInterface::ForceOpenCheatProcess>, "ForceOpenCheatProcess"},
{65004, C<&ICheatInterface::PauseCheatProcess>, "PauseCheatProcess"},
{65005, C<&ICheatInterface::ResumeCheatProcess>, "ResumeCheatProcess"},
{65006, C<&ICheatInterface::ForceCloseCheatProcess>, "ForceCloseCheatProcess"},
{65100, C<&ICheatInterface::GetCheatProcessMappingCount>, "GetCheatProcessMappingCount"},
{65101, C<&ICheatInterface::GetCheatProcessMappings>, "GetCheatProcessMappings"},
{65102, C<&ICheatInterface::ReadCheatProcessMemory>, "ReadCheatProcessMemory"},
{65103, C<&ICheatInterface::WriteCheatProcessMemory>, "WriteCheatProcessMemory"},
{65104, C<&ICheatInterface::QueryCheatProcessMemory>, "QueryCheatProcessMemory"},
{65200, C<&ICheatInterface::GetCheatCount>, "GetCheatCount"},
{65201, C<&ICheatInterface::GetCheats>, "GetCheats"},
{65202, C<&ICheatInterface::GetCheatById>, "GetCheatById"},
{65203, C<&ICheatInterface::ToggleCheat>, "ToggleCheat"},
{65204, C<&ICheatInterface::AddCheat>, "AddCheat"},
{65205, C<&ICheatInterface::RemoveCheat>, "RemoveCheat"},
{65206, C<&ICheatInterface::ReadStaticRegister>, "ReadStaticRegister"},
{65207, C<&ICheatInterface::WriteStaticRegister>, "WriteStaticRegister"},
{65208, C<&ICheatInterface::ResetStaticRegisters>, "ResetStaticRegisters"},
{65209, C<&ICheatInterface::SetMasterCheat>, "SetMasterCheat"},
{65300, C<&ICheatInterface::GetFrozenAddressCount>, "GetFrozenAddressCount"},
{65301, C<&ICheatInterface::GetFrozenAddresses>, "GetFrozenAddresses"},
{65302, C<&ICheatInterface::GetFrozenAddress>, "GetFrozenAddress"},
{65303, C<&ICheatInterface::EnableFrozenAddress>, "EnableFrozenAddress"},
{65304, C<&ICheatInterface::DisableFrozenAddress>, "DisableFrozenAddress"},
};
// clang-format on
RegisterHandlers(functions);
}
ICheatInterface::~ICheatInterface() = default;
Result ICheatInterface::HasCheatProcess(Out<bool> out_has_cheat) {
LOG_INFO(CheatEngine, "called");
*out_has_cheat = cheat_process_manager.HasCheatProcess();
R_SUCCEED();
}
Result ICheatInterface::GetCheatProcessEvent(OutCopyHandle<Kernel::KReadableEvent> out_event) {
LOG_INFO(CheatEngine, "called");
*out_event = &cheat_process_manager.GetCheatProcessEvent();
R_SUCCEED();
}
Result ICheatInterface::GetCheatProcessMetadata(Out<CheatProcessMetadata> out_metadata) {
LOG_INFO(CheatEngine, "called");
R_RETURN(cheat_process_manager.GetCheatProcessMetadata(*out_metadata));
}
Result ICheatInterface::ForceOpenCheatProcess() {
LOG_INFO(CheatEngine, "called");
R_UNLESS(R_SUCCEEDED(cheat_process_manager.ForceOpenCheatProcess()), ResultCheatNotAttached);
R_SUCCEED();
}
Result ICheatInterface::PauseCheatProcess() {
LOG_INFO(CheatEngine, "called");
R_RETURN(cheat_process_manager.PauseCheatProcess());
}
Result ICheatInterface::ResumeCheatProcess() {
LOG_INFO(CheatEngine, "called");
R_RETURN(cheat_process_manager.ResumeCheatProcess());
}
Result ICheatInterface::ForceCloseCheatProcess() {
LOG_WARNING(CheatEngine, "(STUBBED) called");
R_RETURN(cheat_process_manager.ForceCloseCheatProcess());
}
Result ICheatInterface::GetCheatProcessMappingCount(Out<u64> out_count) {
LOG_WARNING(CheatEngine, "(STUBBED) called");
R_RETURN(cheat_process_manager.GetCheatProcessMappingCount(*out_count));
}
Result ICheatInterface::GetCheatProcessMappings(
Out<u64> out_count, u64 offset,
OutArray<Kernel::Svc::MemoryInfo, BufferAttr_HipcMapAlias> out_mappings) {
LOG_INFO(CheatEngine, "called, offset={}", offset);
R_UNLESS(!out_mappings.empty(), ResultCheatNullBuffer);
R_RETURN(cheat_process_manager.GetCheatProcessMappings(*out_count, offset, out_mappings));
}
Result ICheatInterface::ReadCheatProcessMemory(u64 address, u64 size,
OutBuffer<BufferAttr_HipcMapAlias> out_buffer) {
LOG_DEBUG(CheatEngine, "called, address={}, size={}", address, size);
R_UNLESS(!out_buffer.empty(), ResultCheatNullBuffer);
R_RETURN(cheat_process_manager.ReadCheatProcessMemory(address, size, out_buffer));
}
Result ICheatInterface::WriteCheatProcessMemory(u64 address, u64 size,
InBuffer<BufferAttr_HipcMapAlias> buffer) {
LOG_DEBUG(CheatEngine, "called, address={}, size={}", address, size);
R_UNLESS(!buffer.empty(), ResultCheatNullBuffer);
R_RETURN(cheat_process_manager.WriteCheatProcessMemory(address, size, buffer));
}
Result ICheatInterface::QueryCheatProcessMemory(Out<Kernel::Svc::MemoryInfo> out_mapping,
u64 address) {
LOG_WARNING(CheatEngine, "(STUBBED) called, address={}", address);
R_RETURN(cheat_process_manager.QueryCheatProcessMemory(out_mapping, address));
}
Result ICheatInterface::GetCheatCount(Out<u64> out_count) {
LOG_INFO(CheatEngine, "called");
R_RETURN(cheat_process_manager.GetCheatCount(*out_count));
}
Result ICheatInterface::GetCheats(Out<u64> out_count, u64 offset,
OutArray<CheatEntry, BufferAttr_HipcMapAlias> out_cheats) {
LOG_INFO(CheatEngine, "called, offset={}", offset);
R_UNLESS(!out_cheats.empty(), ResultCheatNullBuffer);
R_RETURN(cheat_process_manager.GetCheats(*out_count, offset, out_cheats));
}
Result ICheatInterface::GetCheatById(OutLargeData<CheatEntry, BufferAttr_HipcMapAlias> out_cheat,
u32 cheat_id) {
LOG_INFO(CheatEngine, "called, cheat_id={}", cheat_id);
R_RETURN(cheat_process_manager.GetCheatById(out_cheat, cheat_id));
}
Result ICheatInterface::ToggleCheat(u32 cheat_id) {
LOG_INFO(CheatEngine, "called, cheat_id={}", cheat_id);
R_RETURN(cheat_process_manager.ToggleCheat(cheat_id));
}
Result ICheatInterface::AddCheat(
Out<u32> out_cheat_id, bool is_enabled,
InLargeData<CheatDefinition, BufferAttr_HipcMapAlias> cheat_definition) {
LOG_INFO(CheatEngine, "called, is_enabled={}", is_enabled);
R_RETURN(cheat_process_manager.AddCheat(*out_cheat_id, is_enabled, *cheat_definition));
}
Result ICheatInterface::RemoveCheat(u32 cheat_id) {
LOG_INFO(CheatEngine, "called, cheat_id={}", cheat_id);
R_RETURN(cheat_process_manager.RemoveCheat(cheat_id));
}
Result ICheatInterface::ReadStaticRegister(Out<u64> out_value, u8 register_index) {
LOG_DEBUG(CheatEngine, "called, register_index={}", register_index);
R_RETURN(cheat_process_manager.ReadStaticRegister(*out_value, register_index));
}
Result ICheatInterface::WriteStaticRegister(u8 register_index, u64 value) {
LOG_DEBUG(CheatEngine, "called, register_index={}, value={}", register_index, value);
R_RETURN(cheat_process_manager.WriteStaticRegister(register_index, value));
}
Result ICheatInterface::ResetStaticRegisters() {
LOG_INFO(CheatEngine, "called");
R_RETURN(cheat_process_manager.ResetStaticRegisters());
}
Result ICheatInterface::SetMasterCheat(
InLargeData<CheatDefinition, BufferAttr_HipcMapAlias> cheat_definition) {
LOG_INFO(CheatEngine, "called, name={}, num_opcodes={}", cheat_definition->readable_name.data(),
cheat_definition->num_opcodes);
R_RETURN(cheat_process_manager.SetMasterCheat(*cheat_definition));
}
Result ICheatInterface::GetFrozenAddressCount(Out<u64> out_count) {
LOG_INFO(CheatEngine, "called");
R_RETURN(cheat_process_manager.GetFrozenAddressCount(*out_count));
}
Result ICheatInterface::GetFrozenAddresses(
Out<u64> out_count, u64 offset,
OutArray<FrozenAddressEntry, BufferAttr_HipcMapAlias> out_frozen_address) {
LOG_INFO(CheatEngine, "called, offset={}", offset);
R_UNLESS(!out_frozen_address.empty(), ResultCheatNullBuffer);
R_RETURN(cheat_process_manager.GetFrozenAddresses(*out_count, offset, out_frozen_address));
}
Result ICheatInterface::GetFrozenAddress(Out<FrozenAddressEntry> out_frozen_address_entry,
u64 address) {
LOG_INFO(CheatEngine, "called, address={}", address);
R_RETURN(cheat_process_manager.GetFrozenAddress(*out_frozen_address_entry, address));
}
Result ICheatInterface::EnableFrozenAddress(Out<u64> out_value, u64 address, u64 width) {
LOG_INFO(CheatEngine, "called, address={}, width={}", address, width);
R_UNLESS(width > 0, ResultFrozenAddressInvalidWidth);
R_UNLESS(width <= sizeof(u64), ResultFrozenAddressInvalidWidth);
R_UNLESS((width & (width - 1)) == 0, ResultFrozenAddressInvalidWidth);
R_RETURN(cheat_process_manager.EnableFrozenAddress(*out_value, address, width));
}
Result ICheatInterface::DisableFrozenAddress(u64 address) {
LOG_INFO(CheatEngine, "called, address={}", address);
R_RETURN(cheat_process_manager.DisableFrozenAddress(address));
}
void ICheatInterface::InitializeCheatManager() {
LOG_INFO(CheatEngine, "called");
}
Result ICheatInterface::ReadCheatProcessMemoryUnsafe(u64 process_addr, std::span<u8> out_data,
size_t size) {
LOG_DEBUG(CheatEngine, "called, process_addr={}, size={}", process_addr, size);
R_RETURN(cheat_process_manager.ReadCheatProcessMemoryUnsafe(process_addr, &out_data, size));
}
Result ICheatInterface::WriteCheatProcessMemoryUnsafe(u64 process_addr, std::span<const u8> data,
size_t size) {
LOG_DEBUG(CheatEngine, "called, process_addr={}, size={}", process_addr, size);
R_RETURN(cheat_process_manager.WriteCheatProcessMemoryUnsafe(process_addr, &data, size));
}
Result ICheatInterface::PauseCheatProcessUnsafe() {
LOG_INFO(CheatEngine, "called");
R_RETURN(cheat_process_manager.PauseCheatProcessUnsafe());
}
Result ICheatInterface::ResumeCheatProcessUnsafe() {
LOG_INFO(CheatEngine, "called");
R_RETURN(cheat_process_manager.ResumeCheatProcessUnsafe());
}
} // namespace Service::DMNT
@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "core/hle/service/cmif_types.h"
#include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/service.h"
namespace Core {
class System;
}
namespace Kernel {
class KEvent;
class KReadableEvent;
} // namespace Kernel
namespace Kernel::Svc {
struct MemoryInfo;
}
namespace Service::DMNT {
struct CheatDefinition;
struct CheatEntry;
struct CheatProcessMetadata;
struct FrozenAddressEntry;
class CheatProcessManager;
class ICheatInterface final : public ServiceFramework<ICheatInterface> {
public:
explicit ICheatInterface(Core::System& system_, CheatProcessManager& manager);
~ICheatInterface() override;
private:
Result HasCheatProcess(Out<bool> out_has_cheat);
Result GetCheatProcessEvent(OutCopyHandle<Kernel::KReadableEvent> out_event);
Result GetCheatProcessMetadata(Out<CheatProcessMetadata> out_metadata);
Result ForceOpenCheatProcess();
Result PauseCheatProcess();
Result ResumeCheatProcess();
Result ForceCloseCheatProcess();
Result GetCheatProcessMappingCount(Out<u64> out_count);
Result GetCheatProcessMappings(
Out<u64> out_count, u64 offset,
OutArray<Kernel::Svc::MemoryInfo, BufferAttr_HipcMapAlias> out_mappings);
Result ReadCheatProcessMemory(u64 address, u64 size,
OutBuffer<BufferAttr_HipcMapAlias> out_buffer);
Result WriteCheatProcessMemory(u64 address, u64 size, InBuffer<BufferAttr_HipcMapAlias> buffer);
Result QueryCheatProcessMemory(Out<Kernel::Svc::MemoryInfo> out_mapping, u64 address);
Result GetCheatCount(Out<u64> out_count);
Result GetCheats(Out<u64> out_count, u64 offset,
OutArray<CheatEntry, BufferAttr_HipcMapAlias> out_cheats);
Result GetCheatById(OutLargeData<CheatEntry, BufferAttr_HipcMapAlias> out_cheat, u32 cheat_id);
Result ToggleCheat(u32 cheat_id);
Result AddCheat(Out<u32> out_cheat_id, bool enabled,
InLargeData<CheatDefinition, BufferAttr_HipcMapAlias> cheat_definition);
Result RemoveCheat(u32 cheat_id);
Result ReadStaticRegister(Out<u64> out_value, u8 register_index);
Result WriteStaticRegister(u8 register_index, u64 value);
Result ResetStaticRegisters();
Result SetMasterCheat(InLargeData<CheatDefinition, BufferAttr_HipcMapAlias> cheat_definition);
Result GetFrozenAddressCount(Out<u64> out_count);
Result GetFrozenAddresses(
Out<u64> out_count, u64 offset,
OutArray<FrozenAddressEntry, BufferAttr_HipcMapAlias> out_frozen_address);
Result GetFrozenAddress(Out<FrozenAddressEntry> out_frozen_address_entry, u64 address);
Result EnableFrozenAddress(Out<u64> out_value, u64 address, u64 width);
Result DisableFrozenAddress(u64 address);
private:
void InitializeCheatManager();
Result ReadCheatProcessMemoryUnsafe(u64 process_addr, std::span<u8> out_data, size_t size);
Result WriteCheatProcessMemoryUnsafe(u64 process_addr, std::span<const u8> data, size_t size);
Result PauseCheatProcessUnsafe();
Result ResumeCheatProcessUnsafe();
CheatProcessManager& cheat_process_manager;
};
} // namespace Service::DMNT
+121
View File
@@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <cctype>
#include <cstring>
#include <optional>
#include <string>
#include "core/hle/service/dmnt/cheat_parser.h"
#include "core/hle/service/dmnt/dmnt_types.h"
namespace Service::DMNT {
CheatParser::CheatParser() {}
CheatParser::~CheatParser() = default;
std::vector<CheatEntry> CheatParser::Parse(std::string_view data) const {
std::vector<CheatEntry> out(1);
std::optional<u64> current_entry;
for (std::size_t i = 0; i < data.size(); ++i) {
if (std::isspace(data[i])) {
continue;
}
if (data[i] == '{') {
current_entry = 0;
if (out[*current_entry].definition.num_opcodes > 0) {
return {};
}
std::size_t name_size{};
const auto name = ExtractName(name_size, data, i + 1, '}');
if (name.empty()) {
return {};
}
std::memcpy(out[*current_entry].definition.readable_name.data(), name.data(),
std::min<std::size_t>(out[*current_entry].definition.readable_name.size(),
name.size()));
out[*current_entry]
.definition.readable_name[out[*current_entry].definition.readable_name.size() - 1] =
'\0';
i += name_size + 1;
} else if (data[i] == '[') {
current_entry = out.size();
out.emplace_back();
std::size_t name_size{};
const auto name = ExtractName(name_size, data, i + 1, ']');
if (name.empty()) {
return {};
}
std::memcpy(out[*current_entry].definition.readable_name.data(), name.data(),
std::min<std::size_t>(out[*current_entry].definition.readable_name.size(),
name.size()));
out[*current_entry]
.definition.readable_name[out[*current_entry].definition.readable_name.size() - 1] =
'\0';
i += name_size + 1;
} else if (std::isxdigit(data[i])) {
if (!current_entry || out[*current_entry].definition.num_opcodes >=
out[*current_entry].definition.opcodes.size()) {
return {};
}
const auto hex = std::string(data.substr(i, 8));
if (!std::all_of(hex.begin(), hex.end(), ::isxdigit)) {
return {};
}
const auto value = static_cast<u32>(std::strtoul(hex.c_str(), nullptr, 0x10));
out[*current_entry].definition.opcodes[out[*current_entry].definition.num_opcodes++] =
value;
i += 7; // 7 because the for loop will increment by 1 more
} else {
return {};
}
}
out[0].enabled = out[0].definition.num_opcodes > 0;
out[0].cheat_id = 0;
for (u32 i = 1; i < out.size(); ++i) {
out[i].enabled = out[i].definition.num_opcodes > 0;
out[i].cheat_id = i;
}
return out;
}
std::string_view CheatParser::ExtractName(std::size_t& out_name_size, std::string_view data,
std::size_t start_index, char match) const {
auto end_index = start_index;
while (data[end_index] != match) {
++end_index;
if (end_index > data.size()) {
return {};
}
}
out_name_size = end_index - start_index;
// Clamp name if it's too big
if (out_name_size > sizeof(CheatDefinition::readable_name)) {
end_index = start_index + sizeof(CheatDefinition::readable_name);
}
return data.substr(start_index, end_index - start_index);
}
} // namespace Service::DMNT
+26
View File
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string_view>
#include <vector>
namespace Service::DMNT {
struct CheatEntry;
class CheatParser final {
public:
CheatParser();
~CheatParser();
std::vector<CheatEntry> Parse(std::string_view data) const;
private:
std::string_view ExtractName(std::size_t& out_name_size, std::string_view data,
std::size_t start_index, char match) const;
};
} // namespace Service::DMNT
@@ -0,0 +1,599 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "core/arm/debug.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hle/service/cmif_serialization.h"
#include "core/hle/service/dmnt/cheat_process_manager.h"
#include "core/hle/service/dmnt/cheat_virtual_machine.h"
#include "core/hle/service/dmnt/dmnt_results.h"
#include "core/hle/service/hid/hid_server.h"
#include "core/hle/service/sm/sm.h"
#include "hid_core/resource_manager.h"
#include "hid_core/resources/npad/npad.h"
namespace Service::DMNT {
constexpr auto CHEAT_ENGINE_NS = std::chrono::nanoseconds{1000000000 / 12};
CheatProcessManager::CheatProcessManager(Core::System& system_)
: system{system_}, service_context{system_, "dmnt:cht"}, core_timing{system_.CoreTiming()} {
update_event = Core::Timing::CreateEvent("CheatEngine::FrameCallback",
[this](s64 time, std::chrono::nanoseconds ns_late)
-> std::optional<std::chrono::nanoseconds> {
FrameCallback(ns_late);
return std::nullopt;
});
for (size_t i = 0; i < MaxCheatCount; i++) {
ResetCheatEntry(i);
}
cheat_vm = std::make_unique<CheatVirtualMachine>(*this);
cheat_process_event = service_context.CreateEvent("CheatProcessManager::ProcessEvent");
unsafe_break_event = service_context.CreateEvent("CheatProcessManager::ProcessEvent");
}
CheatProcessManager::~CheatProcessManager() {
service_context.CloseEvent(cheat_process_event);
service_context.CloseEvent(unsafe_break_event);
core_timing.UnscheduleEvent(update_event);
}
void CheatProcessManager::SetVirtualMachine(std::unique_ptr<CheatVirtualMachine> vm) {
if (vm) {
cheat_vm = std::move(vm);
SetNeedsReloadVm(true);
}
}
bool CheatProcessManager::HasActiveCheatProcess() {
// Note: This function *MUST* be called only with the cheat lock held.
bool has_cheat_process =
cheat_process_debug_handle != InvalidHandle &&
system.ApplicationProcess()->GetProcessId() == cheat_process_metadata.process_id;
if (!has_cheat_process) {
CloseActiveCheatProcess();
}
return has_cheat_process;
}
void CheatProcessManager::CloseActiveCheatProcess() {
if (cheat_process_debug_handle != InvalidHandle) {
broken_unsafe = false;
unsafe_break_event->Signal();
core_timing.UnscheduleEvent(update_event);
// Close resources.
cheat_process_debug_handle = InvalidHandle;
// Save cheat toggles.
if (always_save_cheat_toggles || should_save_cheat_toggles) {
// TODO: save cheat toggles
should_save_cheat_toggles = false;
}
cheat_process_metadata = {};
ResetAllCheatEntries();
{
auto it = frozen_addresses_map.begin();
while (it != frozen_addresses_map.end()) {
it = frozen_addresses_map.erase(it);
}
}
cheat_process_event->Signal();
}
}
Result CheatProcessManager::EnsureCheatProcess() {
R_UNLESS(HasActiveCheatProcess(), ResultCheatNotAttached);
R_SUCCEED();
}
void CheatProcessManager::SetNeedsReloadVm(bool reload) {
needs_reload_vm = reload;
}
void CheatProcessManager::ResetCheatEntry(size_t i) {
if (i < MaxCheatCount) {
cheat_entries[i] = {};
cheat_entries[i].cheat_id = static_cast<u32>(i);
SetNeedsReloadVm(true);
}
}
void CheatProcessManager::ResetAllCheatEntries() {
for (size_t i = 0; i < MaxCheatCount; i++) {
ResetCheatEntry(i);
}
cheat_vm->ResetStaticRegisters();
}
CheatEntry* CheatProcessManager::GetCheatEntryById(size_t i) {
if (i < MaxCheatCount) {
return cheat_entries.data() + i;
}
return nullptr;
}
CheatEntry* CheatProcessManager::GetCheatEntryByReadableName(const char* readable_name) {
for (size_t i = 1; i < MaxCheatCount; i++) {
if (std::strncmp(cheat_entries[i].definition.readable_name.data(), readable_name,
sizeof(cheat_entries[i].definition.readable_name)) == 0) {
return cheat_entries.data() + i;
}
}
return nullptr;
}
CheatEntry* CheatProcessManager::GetFreeCheatEntry() {
// Check all non-master cheats for availability.
for (size_t i = 1; i < MaxCheatCount; i++) {
if (cheat_entries[i].definition.num_opcodes == 0) {
return cheat_entries.data() + i;
}
}
return nullptr;
}
bool CheatProcessManager::HasCheatProcess() {
std::scoped_lock lk(cheat_lock);
return HasActiveCheatProcess();
}
Kernel::KReadableEvent& CheatProcessManager::GetCheatProcessEvent() const {
return cheat_process_event->GetReadableEvent();
}
Result CheatProcessManager::AttachToApplicationProcess(const std::array<u8, 0x20>& build_id,
VAddr main_region_begin,
u64 main_region_size) {
std::scoped_lock lk(cheat_lock);
{
if (this->HasActiveCheatProcess()) {
this->CloseActiveCheatProcess();
}
}
cheat_process_metadata.process_id = system.ApplicationProcess()->GetProcessId();
{
const auto& page_table = system.ApplicationProcess()->GetPageTable();
cheat_process_metadata.program_id = system.GetApplicationProcessProgramID();
cheat_process_metadata.heap_extents = {
.base = GetInteger(page_table.GetHeapRegionStart()),
.size = page_table.GetHeapRegionSize(),
};
cheat_process_metadata.aslr_extents = {
.base = GetInteger(page_table.GetAliasCodeRegionStart()),
.size = page_table.GetAliasCodeRegionSize(),
};
cheat_process_metadata.alias_extents = {
.base = GetInteger(page_table.GetAliasRegionStart()),
.size = page_table.GetAliasRegionSize(),
};
}
{
cheat_process_metadata.main_nso_extents = {
.base = main_region_begin,
.size = main_region_size,
};
cheat_process_metadata.main_nso_build_id = build_id;
}
cheat_process_debug_handle = cheat_process_metadata.process_id;
broken_unsafe = false;
unsafe_break_event->Signal();
core_timing.ScheduleLoopingEvent(CHEAT_ENGINE_NS, CHEAT_ENGINE_NS, update_event);
LOG_INFO(CheatEngine, "Cheat engine started");
// Signal to our fans.
cheat_process_event->Signal();
R_SUCCEED();
}
Result CheatProcessManager::GetCheatProcessMetadata(CheatProcessMetadata& out_metadata) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
out_metadata = cheat_process_metadata;
R_SUCCEED();
}
Result CheatProcessManager::ForceOpenCheatProcess() {
// R_RETURN(AttachToApplicationProcess(false));
R_SUCCEED();
}
Result CheatProcessManager::PauseCheatProcess() {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
R_RETURN(PauseCheatProcessUnsafe());
}
Result CheatProcessManager::PauseCheatProcessUnsafe() {
broken_unsafe = true;
unsafe_break_event->Clear();
if (system.ApplicationProcess()->IsSuspended()) {
R_SUCCEED();
}
R_RETURN(system.ApplicationProcess()->SetActivity(Kernel::Svc::ProcessActivity::Paused));
}
Result CheatProcessManager::ResumeCheatProcess() {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
R_RETURN(ResumeCheatProcessUnsafe());
}
Result CheatProcessManager::ResumeCheatProcessUnsafe() {
broken_unsafe = true;
unsafe_break_event->Clear();
if (!system.ApplicationProcess()->IsSuspended()) {
R_SUCCEED();
}
system.ApplicationProcess()->SetActivity(Kernel::Svc::ProcessActivity::Runnable);
R_SUCCEED();
}
Result CheatProcessManager::ForceCloseCheatProcess() {
CloseActiveCheatProcess();
R_SUCCEED();
}
Result CheatProcessManager::GetCheatProcessMappingCount(u64& out_count) {
std::scoped_lock lk(cheat_lock);
R_TRY(this->EnsureCheatProcess());
// TODO: Call svc::QueryDebugProcessMemory
out_count = 0;
R_SUCCEED();
}
Result CheatProcessManager::GetCheatProcessMappings(
u64& out_count, u64 offset, std::span<Kernel::Svc::MemoryInfo> out_mappings) {
std::scoped_lock lk(cheat_lock);
R_TRY(this->EnsureCheatProcess());
// TODO: Call svc::QueryDebugProcessMemory
out_count = 0;
R_SUCCEED();
}
Result CheatProcessManager::ReadCheatProcessMemory(u64 process_address, u64 size,
std::span<u8> out_data) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
R_RETURN(ReadCheatProcessMemoryUnsafe(process_address, &out_data, size));
}
Result CheatProcessManager::ReadCheatProcessMemoryUnsafe(u64 process_address, void* out_data,
size_t size) {
if (!system.ApplicationMemory().IsValidVirtualAddress(process_address)) {
std::memset(out_data, 0, size);
R_SUCCEED();
}
system.ApplicationMemory().ReadBlock(process_address, out_data, size);
R_SUCCEED();
}
Result CheatProcessManager::WriteCheatProcessMemory(u64 process_address, u64 size,
std::span<const u8> data) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
R_RETURN(WriteCheatProcessMemoryUnsafe(process_address, &data, size));
}
Result CheatProcessManager::WriteCheatProcessMemoryUnsafe(u64 process_address, const void* data,
size_t size) {
if (!system.ApplicationMemory().IsValidVirtualAddress(process_address)) {
R_SUCCEED();
}
if (system.ApplicationMemory().WriteBlock(process_address, data, size)) {
Core::InvalidateInstructionCacheRange(system.ApplicationProcess(), process_address, size);
}
R_SUCCEED();
}
Result CheatProcessManager::QueryCheatProcessMemory(Out<Kernel::Svc::MemoryInfo> mapping,
u64 address) {
std::scoped_lock lk(cheat_lock);
R_TRY(this->EnsureCheatProcess());
// TODO: Call svc::QueryDebugProcessMemory
R_SUCCEED();
}
Result CheatProcessManager::GetCheatCount(u64& out_count) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
out_count = std::count_if(cheat_entries.begin(), cheat_entries.end(),
[](const auto& entry) { return entry.definition.num_opcodes != 0; });
R_SUCCEED();
}
Result CheatProcessManager::GetCheats(u64& out_count, u64 offset,
std::span<CheatEntry> out_cheats) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
size_t count = 0, total_count = 0;
for (size_t i = 0; i < MaxCheatCount && count < out_cheats.size(); i++) {
if (cheat_entries[i].definition.num_opcodes) {
total_count++;
if (total_count > offset) {
out_cheats[count++] = cheat_entries[i];
}
}
}
out_count = count;
R_SUCCEED();
}
Result CheatProcessManager::GetCheatById(CheatEntry* out_cheat, u32 cheat_id) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
const CheatEntry* entry = GetCheatEntryById(cheat_id);
R_UNLESS(entry != nullptr, ResultCheatUnknownId);
R_UNLESS(entry->definition.num_opcodes != 0, ResultCheatUnknownId);
*out_cheat = *entry;
R_SUCCEED();
}
Result CheatProcessManager::ToggleCheat(u32 cheat_id) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
CheatEntry* entry = GetCheatEntryById(cheat_id);
R_UNLESS(entry != nullptr, ResultCheatUnknownId);
R_UNLESS(entry->definition.num_opcodes != 0, ResultCheatUnknownId);
R_UNLESS(cheat_id != 0, ResultCheatCannotDisable);
entry->enabled = !entry->enabled;
SetNeedsReloadVm(true);
R_SUCCEED();
}
Result CheatProcessManager::AddCheat(u32& out_cheat_id, bool enabled,
const CheatDefinition& cheat_definition) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
R_UNLESS(cheat_definition.num_opcodes != 0, ResultCheatInvalid);
R_UNLESS(cheat_definition.num_opcodes <= cheat_definition.opcodes.size(), ResultCheatInvalid);
CheatEntry* new_entry = GetFreeCheatEntry();
R_UNLESS(new_entry != nullptr, ResultCheatOutOfResource);
new_entry->enabled = enabled;
new_entry->definition = cheat_definition;
SetNeedsReloadVm(true);
out_cheat_id = new_entry->cheat_id;
R_SUCCEED();
}
Result CheatProcessManager::RemoveCheat(u32 cheat_id) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
R_UNLESS(cheat_id < MaxCheatCount, ResultCheatUnknownId);
ResetCheatEntry(cheat_id);
SetNeedsReloadVm(true);
R_SUCCEED();
}
Result CheatProcessManager::ReadStaticRegister(u64& out_value, u64 register_index) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
R_UNLESS(register_index < CheatVirtualMachine::NumStaticRegisters, ResultCheatInvalid);
out_value = cheat_vm->GetStaticRegister(register_index);
R_SUCCEED();
}
Result CheatProcessManager::WriteStaticRegister(u64 register_index, u64 value) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
R_UNLESS(register_index < CheatVirtualMachine::NumStaticRegisters, ResultCheatInvalid);
cheat_vm->SetStaticRegister(register_index, value);
R_SUCCEED();
}
Result CheatProcessManager::ResetStaticRegisters() {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
cheat_vm->ResetStaticRegisters();
R_SUCCEED();
}
Result CheatProcessManager::SetMasterCheat(const CheatDefinition& cheat_definition) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
R_UNLESS(cheat_definition.num_opcodes != 0, ResultCheatInvalid);
R_UNLESS(cheat_definition.num_opcodes <= cheat_definition.opcodes.size(), ResultCheatInvalid);
cheat_entries[0] = {
.enabled = true,
.definition = cheat_definition,
};
SetNeedsReloadVm(true);
R_SUCCEED();
}
Result CheatProcessManager::GetFrozenAddressCount(u64& out_count) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
out_count = std::distance(frozen_addresses_map.begin(), frozen_addresses_map.end());
R_SUCCEED();
}
Result CheatProcessManager::GetFrozenAddresses(u64& out_count, u64 offset,
std::span<FrozenAddressEntry> out_frozen_address) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
u64 total_count = 0, written_count = 0;
for (const auto& [address, value] : frozen_addresses_map) {
if (written_count >= out_frozen_address.size()) {
break;
}
if (offset <= total_count) {
out_frozen_address[written_count].address = address;
out_frozen_address[written_count].value = value;
written_count++;
}
total_count++;
}
out_count = written_count;
R_SUCCEED();
}
Result CheatProcessManager::GetFrozenAddress(FrozenAddressEntry& out_frozen_address_entry,
u64 address) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
const auto it = frozen_addresses_map.find(address);
R_UNLESS(it != frozen_addresses_map.end(), ResultFrozenAddressNotFound);
out_frozen_address_entry = {
.address = it->first,
.value = it->second,
};
R_SUCCEED();
}
Result CheatProcessManager::EnableFrozenAddress(u64& out_value, u64 address, u64 width) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
const auto it = frozen_addresses_map.find(address);
R_UNLESS(it == frozen_addresses_map.end(), ResultFrozenAddressAlreadyExists);
FrozenAddressValue value{};
value.width = static_cast<u8>(width);
R_TRY(ReadCheatProcessMemoryUnsafe(address, &value.value, width));
frozen_addresses_map.insert({address, value});
out_value = value.value;
R_SUCCEED();
}
Result CheatProcessManager::DisableFrozenAddress(u64 address) {
std::scoped_lock lk(cheat_lock);
R_TRY(EnsureCheatProcess());
const auto it = frozen_addresses_map.find(address);
R_UNLESS(it != frozen_addresses_map.end(), ResultFrozenAddressNotFound);
frozen_addresses_map.erase(it);
R_SUCCEED();
}
u64 CheatProcessManager::HidKeysDown() const {
const auto hid = system.ServiceManager().GetService<Service::HID::IHidServer>("hid");
if (hid == nullptr) {
LOG_WARNING(CheatEngine, "Attempted to read input state, but hid is not initialized!");
return 0;
}
const auto applet_resource = hid->GetResourceManager();
if (applet_resource == nullptr || applet_resource->GetNpad() == nullptr) {
LOG_WARNING(CheatEngine,
"Attempted to read input state, but applet resource is not initialized!");
return 0;
}
const auto press_state = applet_resource->GetNpad()->GetAndResetPressState();
return static_cast<u64>(press_state & Core::HID::NpadButton::All);
}
void CheatProcessManager::DebugLog(u8 id, u64 value) const {
LOG_INFO(CheatEngine, "Cheat triggered DebugLog: ID '{:01X}' Value '{:016X}'", id, value);
}
void CheatProcessManager::CommandLog(std::string_view data) const {
LOG_DEBUG(CheatEngine, "[DmntCheatVm]: {}",
data.back() == '\n' ? data.substr(0, data.size() - 1) : data);
}
void CheatProcessManager::FrameCallback(std::chrono::nanoseconds ns_late) {
std::scoped_lock lk(cheat_lock);
if (cheat_vm == nullptr) {
LOG_DEBUG(CheatEngine, "FrameCallback: VM is null");
return;
}
if (needs_reload_vm) {
LOG_INFO(CheatEngine, "Reloading cheat VM with {} entries", cheat_entries.size());
size_t enabled_count = 0;
for (const auto& entry : cheat_entries) {
if (entry.enabled && entry.definition.num_opcodes > 0) {
enabled_count++;
LOG_INFO(CheatEngine, " Cheat '{}': {} opcodes, enabled={}",
entry.definition.readable_name.data(), entry.definition.num_opcodes,
entry.enabled);
}
}
LOG_INFO(CheatEngine, "Total enabled cheats: {}", enabled_count);
cheat_vm->LoadProgram(cheat_entries);
LOG_INFO(CheatEngine, "Cheat VM loaded, program size: {}", cheat_vm->GetProgramSize());
needs_reload_vm = false;
}
if (cheat_vm->GetProgramSize() == 0) {
return;
}
cheat_vm->Execute(cheat_process_metadata);
}
} // namespace Service::DMNT
@@ -0,0 +1,126 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <map>
#include <span>
#include "core/hle/service/cmif_types.h"
#include "core/hle/service/dmnt/dmnt_types.h"
#include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/service.h"
#include "common/intrusive_red_black_tree.h"
namespace Core {
class System;
}
namespace Kernel {
class KEvent;
class KReadableEvent;
} // namespace Kernel
namespace Kernel::Svc {
struct MemoryInfo;
}
namespace Service::DMNT {
class CheatVirtualMachine;
class CheatProcessManager final {
public:
static constexpr size_t MaxCheatCount = 0x80;
static constexpr size_t MaxFrozenAddressCount = 0x80;
CheatProcessManager(Core::System& system_);
~CheatProcessManager();
void SetVirtualMachine(std::unique_ptr<CheatVirtualMachine> vm);
bool HasCheatProcess();
Kernel::KReadableEvent& GetCheatProcessEvent() const;
Result GetCheatProcessMetadata(CheatProcessMetadata& out_metadata);
Result AttachToApplicationProcess(const std::array<u8, 0x20>& build_id, VAddr main_region_begin,
u64 main_region_size);
Result ForceOpenCheatProcess();
Result PauseCheatProcess();
Result PauseCheatProcessUnsafe();
Result ResumeCheatProcess();
Result ResumeCheatProcessUnsafe();
Result ForceCloseCheatProcess();
Result GetCheatProcessMappingCount(u64& out_count);
Result GetCheatProcessMappings(u64& out_count, u64 offset,
std::span<Kernel::Svc::MemoryInfo> out_mappings);
Result ReadCheatProcessMemory(u64 process_address, u64 size, std::span<u8> out_data);
Result ReadCheatProcessMemoryUnsafe(u64 process_address, void* out_data, size_t size);
Result WriteCheatProcessMemory(u64 process_address, u64 size, std::span<const u8> data);
Result WriteCheatProcessMemoryUnsafe(u64 process_address, const void* data, size_t size);
Result QueryCheatProcessMemory(Out<Kernel::Svc::MemoryInfo> mapping, u64 address);
Result GetCheatCount(u64& out_count);
Result GetCheats(u64& out_count, u64 offset, std::span<CheatEntry> out_cheats);
Result GetCheatById(CheatEntry* out_cheat, u32 cheat_id);
Result ToggleCheat(u32 cheat_id);
Result AddCheat(u32& out_cheat_id, bool enabled, const CheatDefinition& cheat_definition);
Result RemoveCheat(u32 cheat_id);
Result ReadStaticRegister(u64& out_value, u64 register_index);
Result WriteStaticRegister(u64 register_index, u64 value);
Result ResetStaticRegisters();
Result SetMasterCheat(const CheatDefinition& cheat_definition);
Result GetFrozenAddressCount(u64& out_count);
Result GetFrozenAddresses(u64& out_count, u64 offset,
std::span<FrozenAddressEntry> out_frozen_address);
Result GetFrozenAddress(FrozenAddressEntry& out_frozen_address_entry, u64 address);
Result EnableFrozenAddress(u64& out_value, u64 address, u64 width);
Result DisableFrozenAddress(u64 address);
u64 HidKeysDown() const;
void DebugLog(u8 id, u64 value) const;
void CommandLog(std::string_view data) const;
private:
bool HasActiveCheatProcess();
void CloseActiveCheatProcess();
Result EnsureCheatProcess();
void SetNeedsReloadVm(bool reload);
void ResetCheatEntry(size_t i);
void ResetAllCheatEntries();
CheatEntry* GetCheatEntryById(size_t i);
CheatEntry* GetCheatEntryByReadableName(const char* readable_name);
CheatEntry* GetFreeCheatEntry();
void FrameCallback(std::chrono::nanoseconds ns_late);
static constexpr u64 InvalidHandle = 0;
mutable std::mutex cheat_lock;
Kernel::KEvent* unsafe_break_event;
Kernel::KEvent* cheat_process_event;
u64 cheat_process_debug_handle = InvalidHandle;
CheatProcessMetadata cheat_process_metadata = {};
bool broken_unsafe = false;
bool needs_reload_vm = false;
std::unique_ptr<CheatVirtualMachine> cheat_vm;
bool enable_cheats_by_default = true;
bool always_save_cheat_toggles = false;
bool should_save_cheat_toggles = false;
std::array<CheatEntry, MaxCheatCount> cheat_entries = {};
// TODO: Replace with IntrusiveRedBlackTree
std::map<u64, FrozenAddressValue> frozen_addresses_map = {};
Core::System& system;
KernelHelpers::ServiceContext service_context;
std::shared_ptr<Core::Timing::EventType> update_event;
Core::Timing::CoreTiming& core_timing;
};
} // namespace Service::DMNT
@@ -1,226 +1,223 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fmt/printf.h>
#include "common/assert.h"
#include "common/scope_exit.h"
#include "core/memory/dmnt_cheat_types.h"
#include "core/memory/dmnt_cheat_vm.h"
#include "core/hle/service/dmnt/cheat_process_manager.h"
#include "core/hle/service/dmnt/cheat_virtual_machine.h"
namespace Core::Memory {
namespace Service::DMNT {
CheatVirtualMachine::CheatVirtualMachine(CheatProcessManager& cheat_manager)
: manager(cheat_manager) {}
DmntCheatVm::DmntCheatVm(std::unique_ptr<Callbacks> callbacks_)
: callbacks(std::move(callbacks_)) {}
CheatVirtualMachine::~CheatVirtualMachine() = default;
DmntCheatVm::~DmntCheatVm() = default;
void DmntCheatVm::DebugLog(u32 log_id, u64 value) {
callbacks->DebugLog(static_cast<u8>(log_id), value);
void CheatVirtualMachine::DebugLog(u32 log_id, u64 value) const {
manager.DebugLog(static_cast<u8>(log_id), value);
}
void DmntCheatVm::LogOpcode(const CheatVmOpcode& opcode) {
void CheatVirtualMachine::LogOpcode(const CheatVmOpcode& opcode) const {
if (auto store_static = std::get_if<StoreStaticOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Store Static");
callbacks->CommandLog(fmt::format("Bit Width: {:X}", store_static->bit_width));
callbacks->CommandLog(
manager.CommandLog("Opcode: Store Static");
manager.CommandLog(fmt::format("Bit Width: {:X}", store_static->bit_width));
manager.CommandLog(
fmt::format("Mem Type: {:X}", static_cast<u32>(store_static->mem_type)));
callbacks->CommandLog(fmt::format("Reg Idx: {:X}", store_static->offset_register));
callbacks->CommandLog(fmt::format("Rel Addr: {:X}", store_static->rel_address));
callbacks->CommandLog(fmt::format("Value: {:X}", store_static->value.bit64));
manager.CommandLog(fmt::format("Reg Idx: {:X}", store_static->offset_register));
manager.CommandLog(fmt::format("Rel Addr: {:X}", store_static->rel_address));
manager.CommandLog(fmt::format("Value: {:X}", store_static->value.bit64));
} else if (auto begin_cond = std::get_if<BeginConditionalOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Begin Conditional");
callbacks->CommandLog(fmt::format("Bit Width: {:X}", begin_cond->bit_width));
callbacks->CommandLog(
fmt::format("Mem Type: {:X}", static_cast<u32>(begin_cond->mem_type)));
callbacks->CommandLog(
fmt::format("Cond Type: {:X}", static_cast<u32>(begin_cond->cond_type)));
callbacks->CommandLog(fmt::format("Rel Addr: {:X}", begin_cond->rel_address));
callbacks->CommandLog(fmt::format("Value: {:X}", begin_cond->value.bit64));
manager.CommandLog("Opcode: Begin Conditional");
manager.CommandLog(fmt::format("Bit Width: {:X}", begin_cond->bit_width));
manager.CommandLog(fmt::format("Mem Type: {:X}", static_cast<u32>(begin_cond->mem_type)));
manager.CommandLog(fmt::format("Cond Type: {:X}", static_cast<u32>(begin_cond->cond_type)));
manager.CommandLog(fmt::format("Rel Addr: {:X}", begin_cond->rel_address));
manager.CommandLog(fmt::format("Value: {:X}", begin_cond->value.bit64));
} else if (std::holds_alternative<EndConditionalOpcode>(opcode.opcode)) {
callbacks->CommandLog("Opcode: End Conditional");
manager.CommandLog("Opcode: End Conditional");
} else if (auto ctrl_loop = std::get_if<ControlLoopOpcode>(&opcode.opcode)) {
if (ctrl_loop->start_loop) {
callbacks->CommandLog("Opcode: Start Loop");
callbacks->CommandLog(fmt::format("Reg Idx: {:X}", ctrl_loop->reg_index));
callbacks->CommandLog(fmt::format("Num Iters: {:X}", ctrl_loop->num_iters));
manager.CommandLog("Opcode: Start Loop");
manager.CommandLog(fmt::format("Reg Idx: {:X}", ctrl_loop->reg_index));
manager.CommandLog(fmt::format("Num Iters: {:X}", ctrl_loop->num_iters));
} else {
callbacks->CommandLog("Opcode: End Loop");
callbacks->CommandLog(fmt::format("Reg Idx: {:X}", ctrl_loop->reg_index));
manager.CommandLog("Opcode: End Loop");
manager.CommandLog(fmt::format("Reg Idx: {:X}", ctrl_loop->reg_index));
}
} else if (auto ldr_static = std::get_if<LoadRegisterStaticOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Load Register Static");
callbacks->CommandLog(fmt::format("Reg Idx: {:X}", ldr_static->reg_index));
callbacks->CommandLog(fmt::format("Value: {:X}", ldr_static->value));
manager.CommandLog("Opcode: Load Register Static");
manager.CommandLog(fmt::format("Reg Idx: {:X}", ldr_static->reg_index));
manager.CommandLog(fmt::format("Value: {:X}", ldr_static->value));
} else if (auto ldr_memory = std::get_if<LoadRegisterMemoryOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Load Register Memory");
callbacks->CommandLog(fmt::format("Bit Width: {:X}", ldr_memory->bit_width));
callbacks->CommandLog(fmt::format("Reg Idx: {:X}", ldr_memory->reg_index));
callbacks->CommandLog(
fmt::format("Mem Type: {:X}", static_cast<u32>(ldr_memory->mem_type)));
callbacks->CommandLog(fmt::format("From Reg: {:d}", ldr_memory->load_from_reg));
callbacks->CommandLog(fmt::format("Rel Addr: {:X}", ldr_memory->rel_address));
manager.CommandLog("Opcode: Load Register Memory");
manager.CommandLog(fmt::format("Bit Width: {:X}", ldr_memory->bit_width));
manager.CommandLog(fmt::format("Reg Idx: {:X}", ldr_memory->reg_index));
manager.CommandLog(fmt::format("Mem Type: {:X}", static_cast<u32>(ldr_memory->mem_type)));
manager.CommandLog(fmt::format("From Reg: {:d}", ldr_memory->load_from_reg));
manager.CommandLog(fmt::format("Rel Addr: {:X}", ldr_memory->rel_address));
} else if (auto str_static = std::get_if<StoreStaticToAddressOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Store Static to Address");
callbacks->CommandLog(fmt::format("Bit Width: {:X}", str_static->bit_width));
callbacks->CommandLog(fmt::format("Reg Idx: {:X}", str_static->reg_index));
manager.CommandLog("Opcode: Store Static to Address");
manager.CommandLog(fmt::format("Bit Width: {:X}", str_static->bit_width));
manager.CommandLog(fmt::format("Reg Idx: {:X}", str_static->reg_index));
if (str_static->add_offset_reg) {
callbacks->CommandLog(fmt::format("O Reg Idx: {:X}", str_static->offset_reg_index));
manager.CommandLog(fmt::format("O Reg Idx: {:X}", str_static->offset_reg_index));
}
callbacks->CommandLog(fmt::format("Incr Reg: {:d}", str_static->increment_reg));
callbacks->CommandLog(fmt::format("Value: {:X}", str_static->value));
manager.CommandLog(fmt::format("Incr Reg: {:d}", str_static->increment_reg));
manager.CommandLog(fmt::format("Value: {:X}", str_static->value));
} else if (auto perform_math_static =
std::get_if<PerformArithmeticStaticOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Perform Static Arithmetic");
callbacks->CommandLog(fmt::format("Bit Width: {:X}", perform_math_static->bit_width));
callbacks->CommandLog(fmt::format("Reg Idx: {:X}", perform_math_static->reg_index));
callbacks->CommandLog(
manager.CommandLog("Opcode: Perform Static Arithmetic");
manager.CommandLog(fmt::format("Bit Width: {:X}", perform_math_static->bit_width));
manager.CommandLog(fmt::format("Reg Idx: {:X}", perform_math_static->reg_index));
manager.CommandLog(
fmt::format("Math Type: {:X}", static_cast<u32>(perform_math_static->math_type)));
callbacks->CommandLog(fmt::format("Value: {:X}", perform_math_static->value));
manager.CommandLog(fmt::format("Value: {:X}", perform_math_static->value));
} else if (auto begin_keypress_cond =
std::get_if<BeginKeypressConditionalOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Begin Keypress Conditional");
callbacks->CommandLog(fmt::format("Key Mask: {:X}", begin_keypress_cond->key_mask));
manager.CommandLog("Opcode: Begin Keypress Conditional");
manager.CommandLog(fmt::format("Key Mask: {:X}", begin_keypress_cond->key_mask));
} else if (auto perform_math_reg =
std::get_if<PerformArithmeticRegisterOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Perform Register Arithmetic");
callbacks->CommandLog(fmt::format("Bit Width: {:X}", perform_math_reg->bit_width));
callbacks->CommandLog(fmt::format("Dst Idx: {:X}", perform_math_reg->dst_reg_index));
callbacks->CommandLog(fmt::format("Src1 Idx: {:X}", perform_math_reg->src_reg_1_index));
manager.CommandLog("Opcode: Perform Register Arithmetic");
manager.CommandLog(fmt::format("Bit Width: {:X}", perform_math_reg->bit_width));
manager.CommandLog(fmt::format("Dst Idx: {:X}", perform_math_reg->dst_reg_index));
manager.CommandLog(fmt::format("Src1 Idx: {:X}", perform_math_reg->src_reg_1_index));
if (perform_math_reg->has_immediate) {
callbacks->CommandLog(fmt::format("Value: {:X}", perform_math_reg->value.bit64));
manager.CommandLog(fmt::format("Value: {:X}", perform_math_reg->value.bit64));
} else {
callbacks->CommandLog(
fmt::format("Src2 Idx: {:X}", perform_math_reg->src_reg_2_index));
manager.CommandLog(fmt::format("Src2 Idx: {:X}", perform_math_reg->src_reg_2_index));
}
} else if (auto str_register = std::get_if<StoreRegisterToAddressOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Store Register to Address");
callbacks->CommandLog(fmt::format("Bit Width: {:X}", str_register->bit_width));
callbacks->CommandLog(fmt::format("S Reg Idx: {:X}", str_register->str_reg_index));
callbacks->CommandLog(fmt::format("A Reg Idx: {:X}", str_register->addr_reg_index));
callbacks->CommandLog(fmt::format("Incr Reg: {:d}", str_register->increment_reg));
manager.CommandLog("Opcode: Store Register to Address");
manager.CommandLog(fmt::format("Bit Width: {:X}", str_register->bit_width));
manager.CommandLog(fmt::format("S Reg Idx: {:X}", str_register->str_reg_index));
manager.CommandLog(fmt::format("A Reg Idx: {:X}", str_register->addr_reg_index));
manager.CommandLog(fmt::format("Incr Reg: {:d}", str_register->increment_reg));
switch (str_register->ofs_type) {
case StoreRegisterOffsetType::None:
break;
case StoreRegisterOffsetType::Reg:
callbacks->CommandLog(fmt::format("O Reg Idx: {:X}", str_register->ofs_reg_index));
manager.CommandLog(fmt::format("O Reg Idx: {:X}", str_register->ofs_reg_index));
break;
case StoreRegisterOffsetType::Imm:
callbacks->CommandLog(fmt::format("Rel Addr: {:X}", str_register->rel_address));
manager.CommandLog(fmt::format("Rel Addr: {:X}", str_register->rel_address));
break;
case StoreRegisterOffsetType::MemReg:
callbacks->CommandLog(
manager.CommandLog(
fmt::format("Mem Type: {:X}", static_cast<u32>(str_register->mem_type)));
break;
case StoreRegisterOffsetType::MemImm:
case StoreRegisterOffsetType::MemImmReg:
callbacks->CommandLog(
manager.CommandLog(
fmt::format("Mem Type: {:X}", static_cast<u32>(str_register->mem_type)));
callbacks->CommandLog(fmt::format("Rel Addr: {:X}", str_register->rel_address));
manager.CommandLog(fmt::format("Rel Addr: {:X}", str_register->rel_address));
break;
}
} else if (auto begin_reg_cond = std::get_if<BeginRegisterConditionalOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Begin Register Conditional");
callbacks->CommandLog(fmt::format("Bit Width: {:X}", begin_reg_cond->bit_width));
callbacks->CommandLog(
manager.CommandLog("Opcode: Begin Register Conditional");
manager.CommandLog(fmt::format("Bit Width: {:X}", begin_reg_cond->bit_width));
manager.CommandLog(
fmt::format("Cond Type: {:X}", static_cast<u32>(begin_reg_cond->cond_type)));
callbacks->CommandLog(fmt::format("V Reg Idx: {:X}", begin_reg_cond->val_reg_index));
manager.CommandLog(fmt::format("V Reg Idx: {:X}", begin_reg_cond->val_reg_index));
switch (begin_reg_cond->comp_type) {
case CompareRegisterValueType::StaticValue:
callbacks->CommandLog("Comp Type: Static Value");
callbacks->CommandLog(fmt::format("Value: {:X}", begin_reg_cond->value.bit64));
manager.CommandLog("Comp Type: Static Value");
manager.CommandLog(fmt::format("Value: {:X}", begin_reg_cond->value.bit64));
break;
case CompareRegisterValueType::OtherRegister:
callbacks->CommandLog("Comp Type: Other Register");
callbacks->CommandLog(fmt::format("X Reg Idx: {:X}", begin_reg_cond->other_reg_index));
manager.CommandLog("Comp Type: Other Register");
manager.CommandLog(fmt::format("X Reg Idx: {:X}", begin_reg_cond->other_reg_index));
break;
case CompareRegisterValueType::MemoryRelAddr:
callbacks->CommandLog("Comp Type: Memory Relative Address");
callbacks->CommandLog(
manager.CommandLog("Comp Type: Memory Relative Address");
manager.CommandLog(
fmt::format("Mem Type: {:X}", static_cast<u32>(begin_reg_cond->mem_type)));
callbacks->CommandLog(fmt::format("Rel Addr: {:X}", begin_reg_cond->rel_address));
manager.CommandLog(fmt::format("Rel Addr: {:X}", begin_reg_cond->rel_address));
break;
case CompareRegisterValueType::MemoryOfsReg:
callbacks->CommandLog("Comp Type: Memory Offset Register");
callbacks->CommandLog(
manager.CommandLog("Comp Type: Memory Offset Register");
manager.CommandLog(
fmt::format("Mem Type: {:X}", static_cast<u32>(begin_reg_cond->mem_type)));
callbacks->CommandLog(fmt::format("O Reg Idx: {:X}", begin_reg_cond->ofs_reg_index));
manager.CommandLog(fmt::format("O Reg Idx: {:X}", begin_reg_cond->ofs_reg_index));
break;
case CompareRegisterValueType::RegisterRelAddr:
callbacks->CommandLog("Comp Type: Register Relative Address");
callbacks->CommandLog(fmt::format("A Reg Idx: {:X}", begin_reg_cond->addr_reg_index));
callbacks->CommandLog(fmt::format("Rel Addr: {:X}", begin_reg_cond->rel_address));
manager.CommandLog("Comp Type: Register Relative Address");
manager.CommandLog(fmt::format("A Reg Idx: {:X}", begin_reg_cond->addr_reg_index));
manager.CommandLog(fmt::format("Rel Addr: {:X}", begin_reg_cond->rel_address));
break;
case CompareRegisterValueType::RegisterOfsReg:
callbacks->CommandLog("Comp Type: Register Offset Register");
callbacks->CommandLog(fmt::format("A Reg Idx: {:X}", begin_reg_cond->addr_reg_index));
callbacks->CommandLog(fmt::format("O Reg Idx: {:X}", begin_reg_cond->ofs_reg_index));
manager.CommandLog("Comp Type: Register Offset Register");
manager.CommandLog(fmt::format("A Reg Idx: {:X}", begin_reg_cond->addr_reg_index));
manager.CommandLog(fmt::format("O Reg Idx: {:X}", begin_reg_cond->ofs_reg_index));
break;
}
} else if (auto save_restore_reg = std::get_if<SaveRestoreRegisterOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Save or Restore Register");
callbacks->CommandLog(fmt::format("Dst Idx: {:X}", save_restore_reg->dst_index));
callbacks->CommandLog(fmt::format("Src Idx: {:X}", save_restore_reg->src_index));
callbacks->CommandLog(
manager.CommandLog("Opcode: Save or Restore Register");
manager.CommandLog(fmt::format("Dst Idx: {:X}", save_restore_reg->dst_index));
manager.CommandLog(fmt::format("Src Idx: {:X}", save_restore_reg->src_index));
manager.CommandLog(
fmt::format("Op Type: {:d}", static_cast<u32>(save_restore_reg->op_type)));
} else if (auto save_restore_regmask =
std::get_if<SaveRestoreRegisterMaskOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Save or Restore Register Mask");
callbacks->CommandLog(
manager.CommandLog("Opcode: Save or Restore Register Mask");
manager.CommandLog(
fmt::format("Op Type: {:d}", static_cast<u32>(save_restore_regmask->op_type)));
for (std::size_t i = 0; i < NumRegisters; i++) {
callbacks->CommandLog(
manager.CommandLog(
fmt::format("Act[{:02X}]: {:d}", i, save_restore_regmask->should_operate[i]));
}
} else if (auto rw_static_reg = std::get_if<ReadWriteStaticRegisterOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Read/Write Static Register");
manager.CommandLog("Opcode: Read/Write Static Register");
if (rw_static_reg->static_idx < NumReadableStaticRegisters) {
callbacks->CommandLog("Op Type: ReadStaticRegister");
manager.CommandLog("Op Type: ReadStaticRegister");
} else {
callbacks->CommandLog("Op Type: WriteStaticRegister");
manager.CommandLog("Op Type: WriteStaticRegister");
}
callbacks->CommandLog(fmt::format("Reg Idx {:X}", rw_static_reg->idx));
callbacks->CommandLog(fmt::format("Stc Idx {:X}", rw_static_reg->static_idx));
manager.CommandLog(fmt::format("Reg Idx {:X}", rw_static_reg->idx));
manager.CommandLog(fmt::format("Stc Idx {:X}", rw_static_reg->static_idx));
} else if (auto debug_log = std::get_if<DebugLogOpcode>(&opcode.opcode)) {
callbacks->CommandLog("Opcode: Debug Log");
callbacks->CommandLog(fmt::format("Bit Width: {:X}", debug_log->bit_width));
callbacks->CommandLog(fmt::format("Log ID: {:X}", debug_log->log_id));
callbacks->CommandLog(
fmt::format("Val Type: {:X}", static_cast<u32>(debug_log->val_type)));
manager.CommandLog("Opcode: Debug Log");
manager.CommandLog(fmt::format("Bit Width: {:X}", debug_log->bit_width));
manager.CommandLog(fmt::format("Log ID: {:X}", debug_log->log_id));
manager.CommandLog(fmt::format("Val Type: {:X}", static_cast<u32>(debug_log->val_type)));
switch (debug_log->val_type) {
case DebugLogValueType::RegisterValue:
callbacks->CommandLog("Val Type: Register Value");
callbacks->CommandLog(fmt::format("X Reg Idx: {:X}", debug_log->val_reg_index));
manager.CommandLog("Val Type: Register Value");
manager.CommandLog(fmt::format("X Reg Idx: {:X}", debug_log->val_reg_index));
break;
case DebugLogValueType::MemoryRelAddr:
callbacks->CommandLog("Val Type: Memory Relative Address");
callbacks->CommandLog(
manager.CommandLog("Val Type: Memory Relative Address");
manager.CommandLog(
fmt::format("Mem Type: {:X}", static_cast<u32>(debug_log->mem_type)));
callbacks->CommandLog(fmt::format("Rel Addr: {:X}", debug_log->rel_address));
manager.CommandLog(fmt::format("Rel Addr: {:X}", debug_log->rel_address));
break;
case DebugLogValueType::MemoryOfsReg:
callbacks->CommandLog("Val Type: Memory Offset Register");
callbacks->CommandLog(
manager.CommandLog("Val Type: Memory Offset Register");
manager.CommandLog(
fmt::format("Mem Type: {:X}", static_cast<u32>(debug_log->mem_type)));
callbacks->CommandLog(fmt::format("O Reg Idx: {:X}", debug_log->ofs_reg_index));
manager.CommandLog(fmt::format("O Reg Idx: {:X}", debug_log->ofs_reg_index));
break;
case DebugLogValueType::RegisterRelAddr:
callbacks->CommandLog("Val Type: Register Relative Address");
callbacks->CommandLog(fmt::format("A Reg Idx: {:X}", debug_log->addr_reg_index));
callbacks->CommandLog(fmt::format("Rel Addr: {:X}", debug_log->rel_address));
manager.CommandLog("Val Type: Register Relative Address");
manager.CommandLog(fmt::format("A Reg Idx: {:X}", debug_log->addr_reg_index));
manager.CommandLog(fmt::format("Rel Addr: {:X}", debug_log->rel_address));
break;
case DebugLogValueType::RegisterOfsReg:
callbacks->CommandLog("Val Type: Register Offset Register");
callbacks->CommandLog(fmt::format("A Reg Idx: {:X}", debug_log->addr_reg_index));
callbacks->CommandLog(fmt::format("O Reg Idx: {:X}", debug_log->ofs_reg_index));
manager.CommandLog("Val Type: Register Offset Register");
manager.CommandLog(fmt::format("A Reg Idx: {:X}", debug_log->addr_reg_index));
manager.CommandLog(fmt::format("O Reg Idx: {:X}", debug_log->ofs_reg_index));
break;
}
} else if (auto instr = std::get_if<UnrecognizedInstruction>(&opcode.opcode)) {
callbacks->CommandLog(fmt::format("Unknown opcode: {:X}", static_cast<u32>(instr->opcode)));
manager.CommandLog(fmt::format("Unknown opcode: {:X}", static_cast<u32>(instr->opcode)));
}
}
DmntCheatVm::Callbacks::~Callbacks() = default;
bool DmntCheatVm::DecodeNextOpcode(CheatVmOpcode& out) {
bool CheatVirtualMachine::DecodeNextOpcode(CheatVmOpcode& out) {
// If we've ever seen a decode failure, return false.
bool valid = decode_success;
CheatVmOpcode opcode = {};
@@ -634,7 +631,7 @@ bool DmntCheatVm::DecodeNextOpcode(CheatVmOpcode& out) {
return valid;
}
void DmntCheatVm::SkipConditionalBlock(bool is_if) {
void CheatVirtualMachine::SkipConditionalBlock(bool is_if) {
if (condition_depth > 0) {
// We want to continue until we're out of the current block.
const std::size_t desired_depth = condition_depth - 1;
@@ -668,7 +665,7 @@ void DmntCheatVm::SkipConditionalBlock(bool is_if) {
}
}
u64 DmntCheatVm::GetVmInt(VmInt value, u32 bit_width) {
u64 CheatVirtualMachine::GetVmInt(VmInt value, u32 bit_width) {
switch (bit_width) {
case 1:
return value.bit8;
@@ -684,8 +681,8 @@ u64 DmntCheatVm::GetVmInt(VmInt value, u32 bit_width) {
}
}
u64 DmntCheatVm::GetCheatProcessAddress(const CheatProcessMetadata& metadata,
MemoryAccessType mem_type, u64 rel_address) {
u64 CheatVirtualMachine::GetCheatProcessAddress(const CheatProcessMetadata& metadata,
MemoryAccessType mem_type, u64 rel_address) {
switch (mem_type) {
case MemoryAccessType::MainNso:
default:
@@ -699,7 +696,7 @@ u64 DmntCheatVm::GetCheatProcessAddress(const CheatProcessMetadata& metadata,
}
}
void DmntCheatVm::ResetState() {
void CheatVirtualMachine::ResetState() {
registers.fill(0);
saved_values.fill(0);
loop_tops.fill(0);
@@ -708,7 +705,7 @@ void DmntCheatVm::ResetState() {
decode_success = true;
}
bool DmntCheatVm::LoadProgram(const std::vector<CheatEntry>& entries) {
bool CheatVirtualMachine::LoadProgram(std::span<const CheatEntry> entries) {
// Reset opcode count.
num_opcodes = 0;
@@ -729,31 +726,31 @@ bool DmntCheatVm::LoadProgram(const std::vector<CheatEntry>& entries) {
return true;
}
void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) {
void CheatVirtualMachine::Execute(const CheatProcessMetadata& metadata) {
CheatVmOpcode cur_opcode{};
// Get Keys down.
u64 kDown = callbacks->HidKeysDown();
u64 kDown = manager.HidKeysDown();
callbacks->CommandLog("Started VM execution.");
callbacks->CommandLog(fmt::format("Main NSO: {:012X}", metadata.main_nso_extents.base));
callbacks->CommandLog(fmt::format("Heap: {:012X}", metadata.main_nso_extents.base));
callbacks->CommandLog(fmt::format("Keys Down: {:08X}", static_cast<u32>(kDown & 0x0FFFFFFF)));
manager.CommandLog("Started VM execution.");
manager.CommandLog(fmt::format("Main NSO: {:012X}", metadata.main_nso_extents.base));
manager.CommandLog(fmt::format("Heap: {:012X}", metadata.main_nso_extents.base));
manager.CommandLog(fmt::format("Keys Down: {:08X}", static_cast<u32>(kDown & 0x0FFFFFFF)));
// Clear VM state.
ResetState();
// Loop until program finishes.
while (DecodeNextOpcode(cur_opcode)) {
callbacks->CommandLog(
manager.CommandLog(
fmt::format("Instruction Ptr: {:04X}", static_cast<u32>(instruction_ptr)));
for (std::size_t i = 0; i < NumRegisters; i++) {
callbacks->CommandLog(fmt::format("Registers[{:02X}]: {:016X}", i, registers[i]));
manager.CommandLog(fmt::format("Registers[{:02X}]: {:016X}", i, registers[i]));
}
for (std::size_t i = 0; i < NumRegisters; i++) {
callbacks->CommandLog(fmt::format("SavedRegs[{:02X}]: {:016X}", i, saved_values[i]));
manager.CommandLog(fmt::format("SavedRegs[{:02X}]: {:016X}", i, saved_values[i]));
}
LogOpcode(cur_opcode);
@@ -773,7 +770,8 @@ void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) {
case 2:
case 4:
case 8:
callbacks->MemoryWriteUnsafe(dst_address, &dst_value, store_static->bit_width);
manager.WriteCheatProcessMemoryUnsafe(dst_address, &dst_value,
store_static->bit_width);
break;
}
} else if (auto begin_cond = std::get_if<BeginConditionalOpcode>(&cur_opcode.opcode)) {
@@ -786,7 +784,8 @@ void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) {
case 2:
case 4:
case 8:
callbacks->MemoryReadUnsafe(src_address, &src_value, begin_cond->bit_width);
manager.ReadCheatProcessMemoryUnsafe(src_address, &src_value,
begin_cond->bit_width);
break;
}
// Check against condition.
@@ -857,8 +856,8 @@ void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) {
case 2:
case 4:
case 8:
callbacks->MemoryReadUnsafe(src_address, &registers[ldr_memory->reg_index],
ldr_memory->bit_width);
manager.ReadCheatProcessMemoryUnsafe(src_address, &registers[ldr_memory->reg_index],
ldr_memory->bit_width);
break;
}
} else if (auto str_static = std::get_if<StoreStaticToAddressOpcode>(&cur_opcode.opcode)) {
@@ -874,7 +873,8 @@ void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) {
case 2:
case 4:
case 8:
callbacks->MemoryWriteUnsafe(dst_address, &dst_value, str_static->bit_width);
manager.WriteCheatProcessMemoryUnsafe(dst_address, &dst_value,
str_static->bit_width);
break;
}
// Increment register if relevant.
@@ -1032,7 +1032,8 @@ void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) {
case 2:
case 4:
case 8:
callbacks->MemoryWriteUnsafe(dst_address, &dst_value, str_register->bit_width);
manager.WriteCheatProcessMemoryUnsafe(dst_address, &dst_value,
str_register->bit_width);
break;
}
@@ -1111,8 +1112,8 @@ void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) {
case 2:
case 4:
case 8:
callbacks->MemoryReadUnsafe(cond_address, &cond_value,
begin_reg_cond->bit_width);
manager.ReadCheatProcessMemoryUnsafe(cond_address, &cond_value,
begin_reg_cond->bit_width);
break;
}
}
@@ -1205,9 +1206,9 @@ void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) {
static_registers[rw_static_reg->static_idx] = registers[rw_static_reg->idx];
}
} else if (std::holds_alternative<PauseProcessOpcode>(cur_opcode.opcode)) {
callbacks->PauseProcess();
manager.PauseCheatProcessUnsafe();
} else if (std::holds_alternative<ResumeProcessOpcode>(cur_opcode.opcode)) {
callbacks->ResumeProcess();
manager.ResumeCheatProcessUnsafe();
} else if (auto debug_log = std::get_if<DebugLogOpcode>(&cur_opcode.opcode)) {
// Read value from memory.
u64 log_value = 0;
@@ -1254,7 +1255,8 @@ void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) {
case 2:
case 4:
case 8:
callbacks->MemoryReadUnsafe(val_address, &log_value, debug_log->bit_width);
manager.ReadCheatProcessMemoryUnsafe(val_address, &log_value,
debug_log->bit_width);
break;
}
}
@@ -1264,5 +1266,4 @@ void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) {
}
}
}
} // namespace Core::Memory
} // namespace Service::DMNT
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
@@ -6,15 +6,14 @@
#pragma once
#include <span>
#include <variant>
#include <vector>
#include <memory>
#include <fmt/printf.h>
#include "common/common_types.h"
#include "core/memory/dmnt_cheat_types.h"
#include "core/hle/service/dmnt/dmnt_types.h"
namespace Core::Memory {
namespace Service::DMNT {
class CheatProcessManager;
enum class CheatVmOpcodeType : u32 {
StoreStatic = 0,
@@ -264,25 +263,8 @@ struct CheatVmOpcode {
opcode{};
};
class DmntCheatVm {
class CheatVirtualMachine {
public:
/// Helper Type for DmntCheatVm <=> yuzu Interface
class Callbacks {
public:
virtual ~Callbacks();
virtual void MemoryReadUnsafe(VAddr address, void* data, u64 size) = 0;
virtual void MemoryWriteUnsafe(VAddr address, const void* data, u64 size) = 0;
virtual u64 HidKeysDown() = 0;
virtual void PauseProcess() = 0;
virtual void ResumeProcess() = 0;
virtual void DebugLog(u8 id, u64 value) = 0;
virtual void CommandLog(std::string_view data) = 0;
};
static constexpr std::size_t MaximumProgramOpcodeCount = 0x400;
static constexpr std::size_t NumRegisters = 0x10;
static constexpr std::size_t NumReadableStaticRegisters = 0x80;
@@ -290,18 +272,43 @@ public:
static constexpr std::size_t NumStaticRegisters =
NumReadableStaticRegisters + NumWritableStaticRegisters;
explicit DmntCheatVm(std::unique_ptr<Callbacks> callbacks_);
~DmntCheatVm();
explicit CheatVirtualMachine(CheatProcessManager& cheat_manager);
~CheatVirtualMachine();
std::size_t GetProgramSize() const {
return this->num_opcodes;
}
bool LoadProgram(const std::vector<CheatEntry>& cheats);
bool LoadProgram(std::span<const CheatEntry> cheats);
void Execute(const CheatProcessMetadata& metadata);
u64 GetStaticRegister(std::size_t register_index) const {
return static_registers[register_index];
}
void SetStaticRegister(std::size_t register_index, u64 value) {
static_registers[register_index] = value;
}
void ResetStaticRegisters() {
static_registers = {};
}
private:
std::unique_ptr<Callbacks> callbacks;
bool DecodeNextOpcode(CheatVmOpcode& out);
void SkipConditionalBlock(bool is_if);
void ResetState();
// For implementing the DebugLog opcode.
void DebugLog(u32 log_id, u64 value) const;
void LogOpcode(const CheatVmOpcode& opcode) const;
static u64 GetVmInt(VmInt value, u32 bit_width);
static u64 GetCheatProcessAddress(const CheatProcessMetadata& metadata,
MemoryAccessType mem_type, u64 rel_address);
CheatProcessManager& manager;
std::size_t num_opcodes = 0;
std::size_t instruction_ptr = 0;
@@ -312,19 +319,5 @@ private:
std::array<u64, NumRegisters> saved_values{};
std::array<u64, NumStaticRegisters> static_registers{};
std::array<std::size_t, NumRegisters> loop_tops{};
bool DecodeNextOpcode(CheatVmOpcode& out);
void SkipConditionalBlock(bool is_if);
void ResetState();
// For implementing the DebugLog opcode.
void DebugLog(u32 log_id, u64 value);
void LogOpcode(const CheatVmOpcode& opcode);
static u64 GetVmInt(VmInt value, u32 bit_width);
static u64 GetCheatProcessAddress(const CheatProcessMetadata& metadata,
MemoryAccessType mem_type, u64 rel_address);
};
}; // namespace Core::Memory
} // namespace Service::DMNT
+26
View File
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "core/core.h"
#include "core/hle/service/dmnt/cheat_interface.h"
#include "core/hle/service/dmnt/cheat_process_manager.h"
#include "core/hle/service/dmnt/cheat_virtual_machine.h"
#include "core/hle/service/dmnt/dmnt.h"
#include "core/hle/service/server_manager.h"
namespace Service::DMNT {
void LoopProcess(Core::System& system) {
auto server_manager = std::make_unique<ServerManager>(system);
auto& cheat_manager = system.GetCheatManager();
auto cheat_vm = std::make_unique<CheatVirtualMachine>(cheat_manager);
cheat_manager.SetVirtualMachine(std::move(cheat_vm));
server_manager->RegisterNamedService("dmnt:cht",
std::make_shared<ICheatInterface>(system, cheat_manager));
ServerManager::RunServer(std::move(server_manager));
}
} // namespace Service::DMNT
+15
View File
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
namespace Core {
class System;
};
namespace Service::DMNT {
void LoopProcess(Core::System& system);
} // namespace Service::DMNT
+26
View File
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "core/hle/result.h"
namespace Service::DMNT {
constexpr Result ResultDebuggingDisabled(ErrorModule::DMNT, 2);
constexpr Result ResultCheatNotAttached(ErrorModule::DMNT, 6500);
constexpr Result ResultCheatNullBuffer(ErrorModule::DMNT, 6501);
constexpr Result ResultCheatInvalidBuffer(ErrorModule::DMNT, 6502);
constexpr Result ResultCheatUnknownId(ErrorModule::DMNT, 6503);
constexpr Result ResultCheatOutOfResource(ErrorModule::DMNT, 6504);
constexpr Result ResultCheatInvalid(ErrorModule::DMNT, 6505);
constexpr Result ResultCheatCannotDisable(ErrorModule::DMNT, 6506);
constexpr Result ResultFrozenAddressInvalidWidth(ErrorModule::DMNT, 6600);
constexpr Result ResultFrozenAddressAlreadyExists(ErrorModule::DMNT, 6601);
constexpr Result ResultFrozenAddressNotFound(ErrorModule::DMNT, 6602);
constexpr Result ResultFrozenAddressOutOfResource(ErrorModule::DMNT, 6603);
constexpr Result ResultVirtualMachineInvalidConditionDepth(ErrorModule::DMNT, 6700);
} // namespace Service::DMNT
+55
View File
@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
namespace Service::DMNT {
struct MemoryRegionExtents {
u64 base{};
u64 size{};
};
static_assert(sizeof(MemoryRegionExtents) == 0x10, "MemoryRegionExtents is an invalid size");
struct CheatProcessMetadata {
u64 process_id{};
u64 program_id{};
MemoryRegionExtents main_nso_extents{};
MemoryRegionExtents heap_extents{};
MemoryRegionExtents alias_extents{};
MemoryRegionExtents aslr_extents{};
std::array<u8, 0x20> main_nso_build_id{};
};
static_assert(sizeof(CheatProcessMetadata) == 0x70, "CheatProcessMetadata is an invalid size");
struct CheatDefinition {
std::array<char, 0x40> readable_name;
u32 num_opcodes;
std::array<u32, 0x100> opcodes;
};
static_assert(sizeof(CheatDefinition) == 0x444, "CheatDefinition is an invalid size");
struct CheatEntry {
bool enabled;
u32 cheat_id;
CheatDefinition definition;
};
static_assert(sizeof(CheatEntry) == 0x44C, "CheatEntry is an invalid size");
static_assert(std::is_trivial_v<CheatEntry>, "CheatEntry type must be trivially copyable.");
struct FrozenAddressValue {
u64 value;
u8 width;
};
static_assert(sizeof(FrozenAddressValue) == 0x10, "FrozenAddressValue is an invalid size");
struct FrozenAddressEntry {
u64 address;
FrozenAddressValue value;
};
static_assert(sizeof(FrozenAddressEntry) == 0x18, "FrozenAddressEntry is an invalid size");
} // namespace Service::DMNT
+19 -15
View File
@@ -1,10 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging.h"
#include "core/arm/debug.h"
#include "core/arm/symbols.h"
#include "core/core.h"
@@ -115,7 +111,10 @@ public:
const VAddr input_ptr{context.AddHeap(in_data.data(), in_data.size())};
const VAddr output_ptr{context.AddHeap(out_data.data(), out_data.size())};
const u64 wrapper_value = context.CallFunction(callbacks.Control, ret_ptr, configuration_ptr, command, input_ptr, in_data.size(), output_ptr, out_data.size());
const u64 wrapper_value{context.CallFunction(callbacks.Control, ret_ptr, configuration_ptr,
command, input_ptr, in_data.size(), output_ptr,
out_data.size())};
*out_return_value = context.GetHeap<s32>(ret_ptr);
context.GetHeap(output_ptr, out_data.data(), out_data.size());
@@ -127,7 +126,9 @@ public:
R_THROW(ResultUnknown);
}
Result LoadPlugin(u64 tmem_size, InCopyHandle<Kernel::KTransferMemory> tmem, InBuffer<BufferAttr_HipcMapAlias> nrr, InBuffer<BufferAttr_HipcMapAlias> nro) {
Result LoadPlugin(u64 tmem_size, InCopyHandle<Kernel::KTransferMemory> tmem,
InBuffer<BufferAttr_HipcMapAlias> nrr,
InBuffer<BufferAttr_HipcMapAlias> nro) {
if (!tmem) {
LOG_ERROR(Service_JIT, "Invalid transfer memory handle!");
R_THROW(ResultUnknown);
@@ -138,10 +139,9 @@ public:
configuration.transfer_memory.size = tmem_size;
// Gather up all the callbacks from the loaded plugin
auto symbols = Core::Symbols::GetSymbols(nro, true);
const auto GetSymbol = [&](const std::string& name) {
return symbols[name].first;
};
auto symbols{Core::Symbols::GetSymbols(nro, true)};
const auto GetSymbol{[&](const std::string& name) { return symbols[name].first; }};
callbacks.rtld_fini = GetSymbol("_fini");
callbacks.rtld_init = GetSymbol("_init");
callbacks.Control = GetSymbol("nnjitpluginControl");
@@ -153,7 +153,8 @@ public:
callbacks.OnPrepared = GetSymbol("nnjitpluginOnPrepared");
callbacks.Keeper = GetSymbol("nnjitpluginKeeper");
if (callbacks.GetVersion == 0 || callbacks.Configure == 0 || callbacks.GenerateCode == 0 || callbacks.OnPrepared == 0 || callbacks.Control == 0) {
if (callbacks.GetVersion == 0 || callbacks.Configure == 0 || callbacks.GenerateCode == 0 ||
callbacks.OnPrepared == 0) {
LOG_ERROR(Service_JIT, "plugin does not implement all necessary functionality");
R_THROW(ResultUnknown);
}
@@ -163,9 +164,12 @@ public:
R_THROW(ResultUnknown);
}
context.MapProcessMemory(configuration.sys_ro_memory.offset, configuration.sys_ro_memory.size);
context.MapProcessMemory(configuration.sys_rx_memory.offset, configuration.sys_rx_memory.size);
context.MapProcessMemory(configuration.transfer_memory.offset, configuration.transfer_memory.size);
context.MapProcessMemory(configuration.sys_ro_memory.offset,
configuration.sys_ro_memory.size);
context.MapProcessMemory(configuration.sys_rx_memory.offset,
configuration.sys_rx_memory.size);
context.MapProcessMemory(configuration.transfer_memory.offset,
configuration.transfer_memory.size);
// Run ELF constructors, if needed
if (callbacks.rtld_init != 0) {
@@ -175,7 +179,7 @@ public:
// Function prototype:
// u64 GetVersion();
const auto version{context.CallFunction(callbacks.GetVersion)};
if (version > 1) {
if (version != 1) {
LOG_ERROR(Service_JIT, "unknown plugin version {}", version);
R_THROW(ResultUnknown);
}
+99 -92
View File
@@ -24,25 +24,15 @@ using namespace Common::ELF;
namespace Service::JIT {
enum HelperFn {
None,
Stop,
Resolve,
Panic,
Memcpy,
Memmove,
Memset,
// sm64
PanicForPlugin,
AbortImpl,
UnexpectedImpl,
Count
};
constexpr std::array<u8, 8> SVC0_ARM64 = {
0x01, 0x00, 0x00, 0xd4, // svc #0
0xc0, 0x03, 0x5f, 0xd6, // ret
};
constexpr std::array HELPER_FUNCTIONS{
"_stop", "_resolve", "_panic", "memcpy", "memmove", "memset",
};
constexpr size_t STACK_ALIGN = 16;
class JITContextImpl;
@@ -52,12 +42,10 @@ using IntervalType = boost::icl::interval_set<VAddr>::interval_type;
class DynarmicCallbacks64 : public Dynarmic::A64::UserCallbacks {
public:
explicit DynarmicCallbacks64(Core::Memory::Memory& memory_, std::vector<u8>& local_memory_, IntervalSet& mapped_ranges_, JITContextImpl& parent_)
: memory{memory_}
, local_memory{local_memory_}
, mapped_ranges{mapped_ranges_}
, parent{parent_}
{}
explicit DynarmicCallbacks64(Core::Memory::Memory& memory_, std::vector<u8>& local_memory_,
IntervalSet& mapped_ranges_, JITContextImpl& parent_)
: memory{memory_}, local_memory{local_memory_},
mapped_ranges{mapped_ranges_}, parent{parent_} {}
std::optional<std::uint32_t> MemoryReadCode(VAddr vaddr) override {
static_assert(Core::Memory::YUZU_PAGESIZE == Dynarmic::CODE_PAGE_SIZE);
@@ -69,7 +57,7 @@ public:
return cached_code_page.inst[(vaddr & Core::Memory::YUZU_PAGEMASK) / sizeof(u32)];
}
void InstructionSynchronizationBarrierRaised() override {
last_code_addr = u64(-1); //reset back, force refetch
last_code_addr = 0; //reset back, force refetch
}
u8 MemoryRead8(u64 vaddr) override {
return ReadMemory<u8>(vaddr);
@@ -87,10 +75,13 @@ public:
return ReadMemory<u128>(vaddr);
}
std::string MemoryReadCString(u64 vaddr) {
std::string result{};
std::string result;
u8 next;
while ((next = MemoryRead8(vaddr++)) != 0)
result += char(next);
while ((next = MemoryRead8(vaddr++)) != 0) {
result += next;
}
return result;
}
@@ -166,26 +157,32 @@ private:
std::vector<u8>& local_memory;
IntervalSet& mapped_ranges;
JITContextImpl& parent;
Dynarmic::CodePage cached_code_page;
u64 last_code_addr = u64(-1);
u64 last_code_addr = 0;
};
class JITContextImpl {
public:
explicit JITContextImpl(Core::Memory::Memory& memory_) : memory{memory_} {
callbacks.emplace(memory, local_memory, mapped_ranges, *this);
user_config.callbacks = std::addressof(callbacks.value());
jit.emplace(user_config);
callbacks =
std::make_unique<DynarmicCallbacks64>(memory, local_memory, mapped_ranges, *this);
user_config.callbacks = callbacks.get();
jit = std::make_unique<Dynarmic::A64::Jit>(user_config);
}
bool LoadNRO(std::span<const u8> data) {
local_memory.clear();
relocbase = local_memory.size();
local_memory.insert(local_memory.end(), data.begin(), data.end());
if (FixupRelocations()) {
InsertHelperFunctions();
InsertStack();
return true;
}
return false;
}
@@ -193,9 +190,11 @@ public:
// The loaded NRO file has ELF relocations that must be processed before it can run.
// Normally this would be processed by RTLD, but in HLE context, we don't have
// the linker available, so we have to do it ourselves.
const VAddr mod_offset{callbacks->MemoryRead32(4)};
if (callbacks->MemoryRead32(mod_offset) != Common::MakeMagic('M', 'O', 'D', '0'))
if (callbacks->MemoryRead32(mod_offset) != Common::MakeMagic('M', 'O', 'D', '0')) {
return false;
}
// For more info about dynamic entries, see the ELF ABI specification:
// https://refspecs.linuxbase.org/elf/gabi4+/ch5.dynamic.html
@@ -206,33 +205,40 @@ public:
while (true) {
const auto dyn{callbacks->ReadMemory<Elf64_Dyn>(dynamic_offset)};
dynamic_offset += sizeof(Elf64_Dyn);
if (!dyn.d_tag) {
break;
} else if (dyn.d_tag == ElfDtRela) {
}
if (dyn.d_tag == ElfDtRela) {
rela_dyn = dyn.d_un.d_ptr;
} else if (dyn.d_tag == ElfDtRelasz) {
}
if (dyn.d_tag == ElfDtRelasz) {
num_rela = dyn.d_un.d_val / sizeof(Elf64_Rela);
} else if (dyn.d_tag == ElfDtRelr) {
}
if (dyn.d_tag == ElfDtRelr) {
relr_dyn = dyn.d_un.d_ptr;
} else if (dyn.d_tag == ElfDtRelrsz) {
}
if (dyn.d_tag == ElfDtRelrsz) {
num_relr = dyn.d_un.d_val / sizeof(Elf64_Relr);
}
}
for (size_t i = 0; i < num_rela; i++) {
const auto rela{callbacks->ReadMemory<Elf64_Rela>(rela_dyn + i * sizeof(Elf64_Rela))};
if (Elf64RelType(rela.r_info) == ElfAArch64Relative) {
const VAddr contents{callbacks->MemoryRead64(rela.r_offset)};
callbacks->MemoryWrite64(rela.r_offset, contents + rela.r_addend);
if (Elf64RelType(rela.r_info) != ElfAArch64Relative) {
continue;
}
const VAddr contents{callbacks->MemoryRead64(rela.r_offset)};
callbacks->MemoryWrite64(rela.r_offset, contents + rela.r_addend);
}
VAddr relr_where = 0;
for (size_t i = 0; i < num_relr; i++) {
const auto relr = callbacks->ReadMemory<Elf64_Relr>(relr_dyn + i * sizeof(Elf64_Relr));
const auto incr = [&](VAddr where) {
const auto relr{callbacks->ReadMemory<Elf64_Relr>(relr_dyn + i * sizeof(Elf64_Relr))};
const auto incr{[&](VAddr where) {
callbacks->MemoryWrite64(where, callbacks->MemoryRead64(where) + relocbase);
};
}};
if ((relr & 1) == 0) {
// where pointer
relr_where = relocbase + relr;
@@ -248,12 +254,13 @@ public:
relr_where += 63 * sizeof(Elf64_Addr);
}
}
return true;
}
void InsertHelperFunctions() {
for (size_t i = 0; i < size_t(HelperFn::Count); ++i) {
helpers[i] = local_memory.size();
for (const auto& name : HELPER_FUNCTIONS) {
helpers[name] = local_memory.size();
local_memory.insert(local_memory.end(), SVC0_ARM64.begin(), SVC0_ARM64.end());
}
}
@@ -261,8 +268,9 @@ public:
void InsertStack() {
// Allocate enough space to avoid any reasonable risk of
// overflowing the stack during plugin execution
const u64 pad_amount = Common::AlignUp(local_memory.size(), STACK_ALIGN) - local_memory.size();
local_memory.insert(local_memory.end(), (4096 * 32) + pad_amount, 0);
const u64 pad_amount{Common::AlignUp(local_memory.size(), STACK_ALIGN) -
local_memory.size()};
local_memory.insert(local_memory.end(), 0x10000 + pad_amount, 0);
top_of_stack = local_memory.size();
heap_pointer = top_of_stack;
}
@@ -289,21 +297,27 @@ public:
//
// For more info, see the AArch64 ABI PCS:
// https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst
for (size_t i = 0; i < 8 && i < argument_stack.size(); i++)
for (size_t i = 0; i < 8 && i < argument_stack.size(); i++) {
jit->SetRegister(i, argument_stack[i]);
}
if (argument_stack.size() > 8) {
const VAddr new_sp = Common::AlignDown(top_of_stack - (argument_stack.size() - 8) * sizeof(u64), STACK_ALIGN);
for (size_t i = 8; i < argument_stack.size(); i++)
const VAddr new_sp = Common::AlignDown(
top_of_stack - (argument_stack.size() - 8) * sizeof(u64), STACK_ALIGN);
for (size_t i = 8; i < argument_stack.size(); i++) {
callbacks->MemoryWrite64(new_sp + (i - 8) * sizeof(u64), argument_stack[i]);
}
jit->SetSP(new_sp);
}
// Reset the call state for the next invocation
argument_stack.clear();
heap_pointer = top_of_stack;
}
u64 CallFunction(VAddr func) {
jit->SetRegister(30, helpers[size_t(HelperFn::Stop)]);
jit->SetRegister(30, helpers["_stop"]);
jit->SetSP(top_of_stack);
SetupArguments();
@@ -312,14 +326,21 @@ public:
return jit->GetRegister(0);
}
VAddr GetHelper(const std::string& name) {
return helpers[name];
}
VAddr AddHeap(const void* data, size_t size) {
// Require all heap data types to have the same alignment as the
// stack pointer, for compatibility
const size_t num_bytes = Common::AlignUp(size, STACK_ALIGN);
const size_t num_bytes{Common::AlignUp(size, STACK_ALIGN)};
// Make additional memory space if required
if (heap_pointer + num_bytes > local_memory.size()) {
local_memory.insert(local_memory.end(), (heap_pointer + num_bytes) - local_memory.size(), 0);
local_memory.insert(local_memory.end(),
(heap_pointer + num_bytes) - local_memory.size(), 0);
}
const VAddr location{heap_pointer};
std::memcpy(local_memory.data() + location, data, size);
heap_pointer += num_bytes;
@@ -330,29 +351,13 @@ public:
std::memcpy(data, local_memory.data() + location, size);
}
VAddr GetHelper(const std::string& name) {
if (name == "_resolve") return helpers[HelperFn::Resolve];
else if (name == "_panic") return helpers[HelperFn::Panic];
else if (name == "_stop") return helpers[HelperFn::Stop];
else if (name == "memset") return helpers[HelperFn::Memset];
else if (name == "memcpy") return helpers[HelperFn::Memcpy];
else if (name == "memmove") return helpers[HelperFn::Memmove];
else if (name == "PanicForPlugin") return helpers[HelperFn::PanicForPlugin];
else if (name == "_ZN2nn4diag6detail9AbortImplEPKcS3_S3_i") return helpers[HelperFn::AbortImpl];
else if (name == "_ZN2nn6detail21UnexpectedDefaultImplEPKcS2_i") return helpers[HelperFn::UnexpectedImpl];
else {
LOG_CRITICAL(Service_JIT, "unresolved {}", name);
return helpers[HelperFn::Panic];
}
}
std::optional<DynarmicCallbacks64> callbacks;
std::optional<Dynarmic::A64::Jit> jit;
std::unique_ptr<DynarmicCallbacks64> callbacks;
std::vector<u8> local_memory;
std::vector<VAddr> argument_stack;
std::vector<u64> argument_stack;
IntervalSet mapped_ranges;
Dynarmic::A64::UserConfig user_config;
std::array<VAddr, size_t(HelperFn::Count)> helpers;
std::unique_ptr<Dynarmic::A64::Jit> jit;
std::map<std::string, VAddr, std::less<>> helpers;
Core::Memory::Memory& memory;
VAddr top_of_stack;
VAddr heap_pointer;
@@ -378,41 +383,44 @@ void DynarmicCallbacks64::CallSVC(u32 swi) {
}
u64 pc{parent.jit->GetPC() - 4};
if (pc == parent.helpers[size_t(HelperFn::Memcpy)] || pc == parent.helpers[size_t(HelperFn::Memmove)]) {
auto& helpers{parent.helpers};
if (pc == helpers["memcpy"] || pc == helpers["memmove"]) {
const VAddr dest{parent.jit->GetRegister(0)};
const VAddr src{parent.jit->GetRegister(1)};
const size_t n{parent.jit->GetRegister(2)};
if (dest < src) {
for (size_t i = 0; i < n; i++)
for (size_t i = 0; i < n; i++) {
MemoryWrite8(dest + i, MemoryRead8(src + i));
}
} else {
for (size_t i = n; i > 0; i--)
for (size_t i = n; i > 0; i--) {
MemoryWrite8(dest + i - 1, MemoryRead8(src + i - 1));
}
}
} else if (pc == parent.helpers[size_t(HelperFn::Memset)]) {
} else if (pc == helpers["memset"]) {
const VAddr dest{parent.jit->GetRegister(0)};
const u64 c{parent.jit->GetRegister(1)};
const size_t n{parent.jit->GetRegister(2)};
for (size_t i = 0; i < n; i++)
MemoryWrite8(dest + i, u8(c));
} else if (pc == parent.helpers[size_t(HelperFn::Resolve)]) {
for (size_t i = 0; i < n; i++) {
MemoryWrite8(dest + i, static_cast<u8>(c));
}
} else if (pc == helpers["_resolve"]) {
// X0 contains a char* for a symbol to resolve
const auto name{MemoryReadCString(parent.jit->GetRegister(0))};
parent.jit->SetRegister(0, u64(parent.GetHelper(name)));
} else if (pc == parent.helpers[size_t(HelperFn::Stop)]) {
const auto helper{helpers[name]};
if (helper != 0) {
parent.jit->SetRegister(0, helper);
} else {
LOG_WARNING(Service_JIT, "plugin requested unknown function {}", name);
parent.jit->SetRegister(0, helpers["_panic"]);
}
} else if (pc == helpers["_stop"]) {
parent.jit->HaltExecution();
} else if (pc == parent.helpers[size_t(HelperFn::Panic)]) {
LOG_CRITICAL(Service_JIT, "plugin panicked!");
parent.jit->HaltExecution();
// SM64
} else if (pc == parent.helpers[size_t(HelperFn::PanicForPlugin)]) {
LOG_CRITICAL(Service_JIT, "plugin panicked!");
parent.jit->HaltExecution();
} else if (pc == parent.helpers[size_t(HelperFn::AbortImpl)]) {
LOG_CRITICAL(Service_JIT, "plugin panicked!");
parent.jit->HaltExecution();
} else if (pc == parent.helpers[size_t(HelperFn::UnexpectedImpl)]) {
} else if (pc == helpers["_panic"]) {
LOG_CRITICAL(Service_JIT, "plugin panicked!");
parent.jit->HaltExecution();
} else {
@@ -422,8 +430,7 @@ void DynarmicCallbacks64::CallSVC(u32 swi) {
}
void DynarmicCallbacks64::ExceptionRaised(u64 pc, Dynarmic::A64::Exception exception) {
auto const inst = MemoryRead32(pc);
LOG_CRITICAL(Service_JIT, "{} PC @ {:08x}, data = {:08x}", exception, pc, inst);
LOG_CRITICAL(Service_JIT, "Illegal operation PC @ {:08x}", pc);
parent.jit->HaltExecution();
}
+3 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
@@ -16,6 +16,7 @@
#include "core/hle/service/btdrv/btdrv.h"
#include "core/hle/service/btm/btm.h"
#include "core/hle/service/caps/caps.h"
#include "core/hle/service/dmnt/dmnt.h"
#include "core/hle/service/erpt/erpt.h"
#include "core/hle/service/es/es.h"
#include "core/hle/service/eupld/eupld.h"
@@ -107,6 +108,7 @@ Services::Services(std::shared_ptr<SM::ServiceManager>& sm, Core::System& system
{"btdrv", &BtDrv::LoopProcess},
{"btm", &BTM::LoopProcess},
{"capsrv", &Capture::LoopProcess},
{"dmnt", &DMNT::LoopProcess},
{"erpt", &ERPT::LoopProcess},
{"es", &ES::LoopProcess},
{"eupld", &EUPLD::LoopProcess},
+42 -7
View File
@@ -16,6 +16,7 @@
#include "common/assert.h"
#include "common/atomic_ops.h"
#include "common/common_types.h"
#include "common/heap_tracker.h"
#include "common/logging.h"
#include "common/page_table.h"
#include "common/scope_exit.h"
@@ -54,24 +55,37 @@ struct Memory::Impl {
} else {
current_page_table->fastmem_arena = nullptr;
}
#ifdef __ANDROID__
heap_tracker.emplace(system.DeviceMemory().buffer);
buffer = std::addressof(*heap_tracker);
#else
buffer = std::addressof(system.DeviceMemory().buffer);
#endif
}
void MapMemoryRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size, Common::PhysicalAddress target, Common::MemoryPermission perms, bool separate_heap) {
void MapMemoryRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size,
Common::PhysicalAddress target, Common::MemoryPermission perms,
bool separate_heap) {
ASSERT_MSG((size & YUZU_PAGEMASK) == 0, "non-page aligned size: {:016X}", size);
ASSERT_MSG((base & YUZU_PAGEMASK) == 0, "non-page aligned base: {:016X}", GetInteger(base));
ASSERT_MSG(target >= DramMemoryMap::Base, "Out of bounds target: {:016X}", GetInteger(target));
MapPages(page_table, base / YUZU_PAGESIZE, size / YUZU_PAGESIZE, target, Common::PageType::Memory);
ASSERT_MSG(target >= DramMemoryMap::Base, "Out of bounds target: {:016X}",
GetInteger(target));
MapPages(page_table, base / YUZU_PAGESIZE, size / YUZU_PAGESIZE, target,
Common::PageType::Memory);
if (current_page_table->fastmem_arena) {
buffer->Map(GetInteger(base), GetInteger(target) - DramMemoryMap::Base, size, perms, separate_heap);
buffer->Map(GetInteger(base), GetInteger(target) - DramMemoryMap::Base, size, perms,
separate_heap);
}
}
void UnmapRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size, bool separate_heap) {
void UnmapRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size,
bool separate_heap) {
ASSERT_MSG((size & YUZU_PAGEMASK) == 0, "non-page aligned size: {:016X}", size);
ASSERT_MSG((base & YUZU_PAGEMASK) == 0, "non-page aligned base: {:016X}", GetInteger(base));
MapPages(page_table, base / YUZU_PAGESIZE, size / YUZU_PAGESIZE, 0, Common::PageType::Unmapped);
MapPages(page_table, base / YUZU_PAGESIZE, size / YUZU_PAGESIZE, 0,
Common::PageType::Unmapped);
if (current_page_table->fastmem_arena) {
buffer->Unmap(GetInteger(base), size, separate_heap);
@@ -843,7 +857,12 @@ struct Memory::Impl {
std::array<Common::ScratchBuffer<u32>, Core::Hardware::NUM_CPU_CORES> scratch_buffers{};
std::span<Core::GPUDirtyMemoryManager> gpu_dirty_managers;
std::mutex sys_core_guard;
#ifdef __ANDROID__
std::optional<Common::HeapTracker> heap_tracker;
Common::HeapTracker* buffer{};
#else
Common::HostMemory* buffer{};
#endif
};
Memory::Memory(Core::System& system_) : system{system_} {
@@ -1036,14 +1055,30 @@ bool Memory::InvalidateNCE(Common::ProcessAddress vaddr, size_t size) {
u8* const ptr = impl->GetPointerImpl(
GetInteger(vaddr),
[&] {
LOG_ERROR(HW_Memory, "Unmapped InvalidateNCE for {} bytes @ {:#x}", size, GetInteger(vaddr));
LOG_ERROR(HW_Memory, "Unmapped InvalidateNCE for {} bytes @ {:#x}", size,
GetInteger(vaddr));
mapped = false;
},
[&] { rasterizer = true; });
if (rasterizer) {
impl->InvalidateGPUMemory(ptr, size);
}
#ifdef __ANDROID__
if (!rasterizer && mapped) {
impl->buffer->DeferredMapSeparateHeap(GetInteger(vaddr));
}
#endif
return mapped && ptr != nullptr;
}
bool Memory::InvalidateSeparateHeap(void* fault_address) {
#ifdef __ANDROID__
return impl->buffer->DeferredMapSeparateHeap(static_cast<u8*>(fault_address));
#else
return false;
#endif
}
} // namespace Core::Memory
+6 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
@@ -490,8 +490,13 @@ public:
* marked as debug or non-debug.
*/
void MarkRegionDebug(Common::ProcessAddress vaddr, u64 size, bool debug);
void SetGPUDirtyManagers(std::span<Core::GPUDirtyMemoryManager> managers);
bool InvalidateNCE(Common::ProcessAddress vaddr, size_t size);
bool InvalidateSeparateHeap(void* fault_address);
private:
Core::System& system;
-288
View File
@@ -1,288 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <locale>
#include "common/hex_util.h"
#include "common/swap.h"
#include "core/arm/debug.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hle/kernel/k_page_table.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/k_process_page_table.h"
#include "core/hle/kernel/svc_types.h"
#include "core/hle/service/hid/hid_server.h"
#include "core/hle/service/sm/sm.h"
#include "core/memory.h"
#include "core/memory/cheat_engine.h"
#include "hid_core/resource_manager.h"
#include "hid_core/resources/npad/npad.h"
namespace Core::Memory {
namespace {
constexpr auto CHEAT_ENGINE_NS = std::chrono::nanoseconds{1000000000 / 12};
std::string_view ExtractName(std::size_t& out_name_size, std::string_view data,
std::size_t start_index, char match) {
auto end_index = start_index;
while (data[end_index] != match) {
++end_index;
if (end_index > data.size()) {
return {};
}
}
out_name_size = end_index - start_index;
// Clamp name if it's too big
if (out_name_size > sizeof(CheatDefinition::readable_name)) {
end_index = start_index + sizeof(CheatDefinition::readable_name);
}
return data.substr(start_index, end_index - start_index);
}
} // Anonymous namespace
StandardVmCallbacks::StandardVmCallbacks(System& system_, const CheatProcessMetadata& metadata_)
: metadata{metadata_}, system{system_} {}
StandardVmCallbacks::~StandardVmCallbacks() = default;
void StandardVmCallbacks::MemoryReadUnsafe(VAddr address, void* data, u64 size) {
// Return zero on invalid address
if (!IsAddressInRange(address) || !system.ApplicationMemory().IsValidVirtualAddress(address)) {
std::memset(data, 0, size);
return;
}
system.ApplicationMemory().ReadBlock(address, data, size);
}
void StandardVmCallbacks::MemoryWriteUnsafe(VAddr address, const void* data, u64 size) {
// Skip invalid memory write address
if (!IsAddressInRange(address) || !system.ApplicationMemory().IsValidVirtualAddress(address)) {
return;
}
if (system.ApplicationMemory().WriteBlock(address, data, size)) {
Core::InvalidateInstructionCacheRange(system.ApplicationProcess(), address, size);
}
}
u64 StandardVmCallbacks::HidKeysDown() {
const auto hid = system.ServiceManager().GetService<Service::HID::IHidServer>("hid");
if (hid == nullptr) {
LOG_WARNING(CheatEngine, "Attempted to read input state, but hid is not initialized!");
return 0;
}
const auto applet_resource = hid->GetResourceManager();
if (applet_resource == nullptr || applet_resource->GetNpad() == nullptr) {
LOG_WARNING(CheatEngine,
"Attempted to read input state, but applet resource is not initialized!");
return 0;
}
const auto press_state = applet_resource->GetNpad()->GetAndResetPressState();
return static_cast<u64>(press_state & HID::NpadButton::All);
}
void StandardVmCallbacks::PauseProcess() {
if (system.ApplicationProcess()->IsSuspended()) {
return;
}
system.ApplicationProcess()->SetActivity(Kernel::Svc::ProcessActivity::Paused);
}
void StandardVmCallbacks::ResumeProcess() {
if (!system.ApplicationProcess()->IsSuspended()) {
return;
}
system.ApplicationProcess()->SetActivity(Kernel::Svc::ProcessActivity::Runnable);
}
void StandardVmCallbacks::DebugLog(u8 id, u64 value) {
LOG_INFO(CheatEngine, "Cheat triggered DebugLog: ID '{:01X}' Value '{:016X}'", id, value);
}
void StandardVmCallbacks::CommandLog(std::string_view data) {
LOG_DEBUG(CheatEngine, "[DmntCheatVm]: {}",
data.back() == '\n' ? data.substr(0, data.size() - 1) : data);
}
bool StandardVmCallbacks::IsAddressInRange(VAddr in) const {
if ((in < metadata.main_nso_extents.base ||
in >= metadata.main_nso_extents.base + metadata.main_nso_extents.size) &&
(in < metadata.heap_extents.base ||
in >= metadata.heap_extents.base + metadata.heap_extents.size) &&
(in < metadata.alias_extents.base ||
in >= metadata.alias_extents.base + metadata.alias_extents.size) &&
(in < metadata.aslr_extents.base ||
in >= metadata.aslr_extents.base + metadata.aslr_extents.size)) {
LOG_DEBUG(CheatEngine,
"Cheat attempting to access memory at invalid address={:016X}, if this "
"persists, "
"the cheat may be incorrect. However, this may be normal early in execution if "
"the game has not properly set up yet.",
in);
return false; ///< Invalid addresses will hard crash
}
return true;
}
CheatParser::~CheatParser() = default;
TextCheatParser::~TextCheatParser() = default;
std::vector<CheatEntry> TextCheatParser::Parse(std::string_view data) const {
std::vector<CheatEntry> out(1);
std::optional<u64> current_entry;
for (std::size_t i = 0; i < data.size(); ++i) {
if (::isspace(data[i])) {
continue;
}
if (data[i] == '{') {
current_entry = 0;
if (out[*current_entry].definition.num_opcodes > 0) {
return {};
}
std::size_t name_size{};
const auto name = ExtractName(name_size, data, i + 1, '}');
if (name.empty()) {
return {};
}
std::memcpy(out[*current_entry].definition.readable_name.data(), name.data(),
std::min<std::size_t>(out[*current_entry].definition.readable_name.size(),
name.size()));
out[*current_entry]
.definition.readable_name[out[*current_entry].definition.readable_name.size() - 1] =
'\0';
i += name_size + 1;
} else if (data[i] == '[') {
current_entry = out.size();
out.emplace_back();
std::size_t name_size{};
const auto name = ExtractName(name_size, data, i + 1, ']');
if (name.empty()) {
return {};
}
std::memcpy(out[*current_entry].definition.readable_name.data(), name.data(),
std::min<std::size_t>(out[*current_entry].definition.readable_name.size(),
name.size()));
out[*current_entry]
.definition.readable_name[out[*current_entry].definition.readable_name.size() - 1] =
'\0';
i += name_size + 1;
} else if (::isxdigit(data[i])) {
if (!current_entry || out[*current_entry].definition.num_opcodes >=
out[*current_entry].definition.opcodes.size()) {
return {};
}
const auto hex = std::string(data.substr(i, 8));
if (!std::all_of(hex.begin(), hex.end(), ::isxdigit)) {
return {};
}
const auto value = static_cast<u32>(std::strtoul(hex.c_str(), nullptr, 0x10));
out[*current_entry].definition.opcodes[out[*current_entry].definition.num_opcodes++] =
value;
i += 8;
} else {
return {};
}
}
out[0].enabled = out[0].definition.num_opcodes > 0;
out[0].cheat_id = 0;
for (u32 i = 1; i < out.size(); ++i) {
out[i].enabled = out[i].definition.num_opcodes > 0;
out[i].cheat_id = i;
}
return out;
}
CheatEngine::CheatEngine(System& system_, std::vector<CheatEntry> cheats_,
const std::array<u8, 0x20>& build_id_)
: vm{std::make_unique<StandardVmCallbacks>(system_, metadata)},
cheats(std::move(cheats_)), core_timing{system_.CoreTiming()}, system{system_} {
metadata.main_nso_build_id = build_id_;
}
CheatEngine::~CheatEngine() {
if (event)
core_timing.UnscheduleEvent(event);
else
LOG_ERROR(CheatEngine, "~CheatEngine before event was registered");
}
void CheatEngine::Initialize() {
event = Core::Timing::CreateEvent(
"CheatEngine::FrameCallback::" + Common::HexToString(metadata.main_nso_build_id),
[this](s64 time, std::chrono::nanoseconds ns_late) -> std::optional<std::chrono::nanoseconds> {
FrameCallback(ns_late);
return std::nullopt;
});
core_timing.ScheduleLoopingEvent(CHEAT_ENGINE_NS, CHEAT_ENGINE_NS, event);
metadata.process_id = system.ApplicationProcess()->GetProcessId();
metadata.title_id = system.GetApplicationProcessProgramID();
const auto& page_table = system.ApplicationProcess()->GetPageTable();
metadata.heap_extents = {
.base = GetInteger(page_table.GetHeapRegionStart()),
.size = page_table.GetHeapRegionSize(),
};
metadata.aslr_extents = {
.base = GetInteger(page_table.GetAliasCodeRegionStart()),
.size = page_table.GetAliasCodeRegionSize(),
};
metadata.alias_extents = {
.base = GetInteger(page_table.GetAliasRegionStart()),
.size = page_table.GetAliasRegionSize(),
};
is_pending_reload.exchange(true);
}
void CheatEngine::SetMainMemoryParameters(VAddr main_region_begin, u64 main_region_size) {
metadata.main_nso_extents = {
.base = main_region_begin,
.size = main_region_size,
};
}
void CheatEngine::Reload(std::vector<CheatEntry> reload_cheats) {
cheats = std::move(reload_cheats);
is_pending_reload.exchange(true);
}
void CheatEngine::FrameCallback(std::chrono::nanoseconds ns_late) {
if (is_pending_reload.exchange(false)) {
vm.LoadProgram(cheats);
}
if (vm.GetProgramSize() == 0) {
return;
}
vm.Execute(metadata);
}
} // namespace Core::Memory
-88
View File
@@ -1,88 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <chrono>
#include <memory>
#include <vector>
#include "common/common_types.h"
#include "core/memory/dmnt_cheat_types.h"
#include "core/memory/dmnt_cheat_vm.h"
namespace Core {
class System;
}
namespace Core::Timing {
class CoreTiming;
struct EventType;
} // namespace Core::Timing
namespace Core::Memory {
class StandardVmCallbacks : public DmntCheatVm::Callbacks {
public:
StandardVmCallbacks(System& system_, const CheatProcessMetadata& metadata_);
~StandardVmCallbacks() override;
void MemoryReadUnsafe(VAddr address, void* data, u64 size) override;
void MemoryWriteUnsafe(VAddr address, const void* data, u64 size) override;
u64 HidKeysDown() override;
void PauseProcess() override;
void ResumeProcess() override;
void DebugLog(u8 id, u64 value) override;
void CommandLog(std::string_view data) override;
private:
bool IsAddressInRange(VAddr address) const;
const CheatProcessMetadata& metadata;
Core::System& system;
};
// Intermediary class that parses a text file or other disk format for storing cheats into a
// CheatList object, that can be used for execution.
class CheatParser {
public:
virtual ~CheatParser();
[[nodiscard]] virtual std::vector<CheatEntry> Parse(std::string_view data) const = 0;
};
// CheatParser implementation that parses text files
class TextCheatParser final : public CheatParser {
public:
~TextCheatParser() override;
[[nodiscard]] std::vector<CheatEntry> Parse(std::string_view data) const override;
};
// Class that encapsulates a CheatList and manages its interaction with memory and CoreTiming
class CheatEngine final {
public:
CheatEngine(System& system_, std::vector<CheatEntry> cheats_,
const std::array<u8, 0x20>& build_id_);
~CheatEngine();
void Initialize();
void SetMainMemoryParameters(VAddr main_region_begin, u64 main_region_size);
void Reload(std::vector<CheatEntry> reload_cheats);
private:
void FrameCallback(std::chrono::nanoseconds ns_late);
DmntCheatVm vm;
CheatProcessMetadata metadata;
std::vector<CheatEntry> cheats;
std::atomic_bool is_pending_reload{false};
std::shared_ptr<Core::Timing::EventType> event;
Core::Timing::CoreTiming& core_timing;
Core::System& system;
};
} // namespace Core::Memory
-37
View File
@@ -1,37 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
namespace Core::Memory {
struct MemoryRegionExtents {
u64 base{};
u64 size{};
};
struct CheatProcessMetadata {
u64 process_id{};
u64 title_id{};
MemoryRegionExtents main_nso_extents{};
MemoryRegionExtents heap_extents{};
MemoryRegionExtents alias_extents{};
MemoryRegionExtents aslr_extents{};
std::array<u8, 0x20> main_nso_build_id{};
};
struct CheatDefinition {
std::array<char, 0x40> readable_name{};
u32 num_opcodes{};
std::array<u32, 0x100> opcodes{};
};
struct CheatEntry {
bool enabled{};
u32 cheat_id{};
CheatDefinition definition{};
};
} // namespace Core::Memory
@@ -82,6 +82,8 @@ private:
std::thread thread;
mach_port_t server_port;
void MessagePump();
};
MachHandler::MachHandler() {
@@ -95,30 +97,7 @@ MachHandler::MachHandler() {
KCHECK(mach_port_request_notification(mach_task_self(), server_port, MACH_NOTIFY_PORT_DESTROYED, 0, server_port, MACH_MSG_TYPE_MAKE_SEND_ONCE, &prev));
#undef KCHECK
thread = std::thread([this] {
mach_msg_return_t mr;
MachMessage request;
MachMessage reply;
while (true) {
mr = mach_msg(&request.head, MACH_RCV_MSG | MACH_RCV_LARGE, 0, sizeof(request), server_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (mr != MACH_MSG_SUCCESS) {
fmt::print(stderr, "dynarmic: macOS MachHandler: Failed to receive mach message. error: {:#08x} ({})\n", mr, mach_error_string(mr));
return;
}
if (!mach_exc_server(&request.head, &reply.head)) {
fmt::print(stderr, "dynarmic: macOS MachHandler: Unexpected mach message\n");
return;
}
mr = mach_msg(&reply.head, MACH_SEND_MSG, reply.head.msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (mr != MACH_MSG_SUCCESS) {
fmt::print(stderr, "dynarmic: macOS MachHandler: Failed to send mach message. error: {:#08x} ({})\n", mr, mach_error_string(mr));
return;
}
}
});
thread = std::thread(&MachHandler::MessagePump, this);
thread.detach();
}
@@ -126,6 +105,31 @@ MachHandler::~MachHandler() {
mach_port_deallocate(mach_task_self(), server_port);
}
void MachHandler::MessagePump() {
mach_msg_return_t mr;
MachMessage request;
MachMessage reply;
while (true) {
mr = mach_msg(&request.head, MACH_RCV_MSG | MACH_RCV_LARGE, 0, sizeof(request), server_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (mr != MACH_MSG_SUCCESS) {
fmt::print(stderr, "dynarmic: macOS MachHandler: Failed to receive mach message. error: {:#08x} ({})\n", mr, mach_error_string(mr));
return;
}
if (!mach_exc_server(&request.head, &reply.head)) {
fmt::print(stderr, "dynarmic: macOS MachHandler: Unexpected mach message\n");
return;
}
mr = mach_msg(&reply.head, MACH_SEND_MSG, reply.head.msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (mr != MACH_MSG_SUCCESS) {
fmt::print(stderr, "dynarmic: macOS MachHandler: Failed to send mach message. error: {:#08x} ({})\n", mr, mach_error_string(mr));
return;
}
}
}
#if defined(ARCHITECTURE_x86_64)
kern_return_t MachHandler::HandleRequest(x86_thread_state64_t* ts) {
std::lock_guard<std::mutex> guard(code_block_infos_mutex);
@@ -47,18 +47,22 @@ class SigHandler {
});
}
static void SigAction(int sig, siginfo_t* info, void* raw_context);
std::vector<u8> signal_stack_memory;
bool supports_fast_mem = true;
void* signal_stack_memory = nullptr;
ankerl::unordered_dense::map<u64, CodeBlockInfo> code_block_infos;
std::shared_mutex code_block_infos_mutex;
struct sigaction old_sa_segv;
struct sigaction old_sa_bus;
bool supports_fast_mem = true;
std::size_t signal_stack_size;
public:
SigHandler() noexcept {
signal_stack_memory.resize(std::max<std::size_t>(SIGSTKSZ, 2 * 1024 * 1024), 0);
signal_stack_size = std::max<size_t>(SIGSTKSZ, 2 * 1024 * 1024);
signal_stack_memory = mmap(nullptr, signal_stack_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
stack_t signal_stack{};
signal_stack.ss_sp = signal_stack_memory.data();
signal_stack.ss_size = signal_stack_memory.size();
signal_stack.ss_sp = signal_stack_memory;
signal_stack.ss_size = signal_stack_size;
signal_stack.ss_flags = 0;
if (sigaltstack(&signal_stack, nullptr) != 0) {
fmt::print(stderr, "dynarmic: POSIX SigHandler: init failure at sigaltstack\n");
@@ -85,6 +89,10 @@ public:
#endif
}
~SigHandler() noexcept {
munmap(signal_stack_memory, signal_stack_size);
}
void AddCodeBlock(u64 offset, CodeBlockInfo cbi) noexcept {
std::unique_lock guard(code_block_infos_mutex);
code_block_infos.insert_or_assign(offset, cbi);
+3 -3
View File
@@ -46,10 +46,10 @@ const std::string GetDataDirString(DataDir dir, const std::string &user_id)
u64 ClearDir(DataDir dir, const std::string &user_id)
{
fs::path data_dir = GetDataDir(dir, user_id);
std::error_code ec;
u64 result = fs::remove_all(data_dir, ec);
u64 result = fs::remove_all(data_dir);
// mkpath at the end just so it actually exists
fs::create_directories(data_dir, ec);
fs::create_directories(data_dir);
return result;
}
+6 -4
View File
@@ -181,13 +181,15 @@ void InstallFirmware(const QString& location, bool recursive) {
QString UnzipFirmwareToTmp(const QString& location) {
namespace fs = std::filesystem;
fs::path tmp{fs::temp_directory_path() / "eden" / "firmware"};
std::error_code ec;
fs::remove_all(tmp, ec);
if (!fs::create_directories(tmp, ec)) {
fs::path tmp{fs::temp_directory_path()};
if (!fs::create_directories(tmp / "eden" / "firmware")) {
return QString();
}
tmp /= "eden";
tmp /= "firmware";
QString qCacheDir = QString::fromStdString(tmp.string());
QFile zip(location);
+5 -6
View File
@@ -83,9 +83,8 @@ QStringList GetModFolders(const QString& root, const QString& fallbackName) {
// now make a temp directory...
const auto mod_dir = fs::temp_directory_path() / "eden" / "mod" / name.toStdString();
const auto tmp = mod_dir / to_make;
std::error_code ec;
fs::remove_all(mod_dir, ec);
if (!fs::create_directories(tmp, ec)) {
fs::remove_all(mod_dir);
if (!fs::create_directories(tmp)) {
LOG_ERROR(Frontend, "Failed to create temporary directory {}", tmp.string());
return {};
}
@@ -117,9 +116,9 @@ QStringList GetModFolders(const QString& root, const QString& fallbackName) {
const QString ExtractMod(const QString& path) {
namespace fs = std::filesystem;
fs::path tmp{fs::temp_directory_path() / "eden" / "unzip_mod"};
std::error_code ec;
fs::remove_all(tmp, ec);
if (!fs::create_directories(tmp, ec)) {
fs::remove_all(tmp);
if (!fs::create_directories(tmp)) {
QtCommon::Frontend::Critical(tr("Mod Extract Failed"),
tr("Failed to create temporary directory %1")
.arg(QString::fromStdString(tmp.string())));
@@ -54,7 +54,7 @@ using VideoCommon::LoadPipelines;
using VideoCommon::SerializePipeline;
using Context = ShaderContext::Context;
constexpr u32 CACHE_VERSION = 15;
constexpr u32 CACHE_VERSION = 14;
template <typename Container>
auto MakeSpan(Container& container) {
@@ -45,10 +45,6 @@
#include "video_core/vulkan_common/vulkan_wrapper.h"
#include "video_core/gpu_logging/gpu_logging.h"
#ifdef ANDROID
#include "../../android/app/src/main/jni/android_settings.h"
#endif
namespace Vulkan {
namespace {
@@ -62,7 +58,7 @@ using VideoCommon::FileEnvironment;
using VideoCommon::GenericEnvironment;
using VideoCommon::GraphicsEnvironment;
constexpr u32 CACHE_VERSION = 17;
constexpr u32 CACHE_VERSION = 16;
constexpr std::array<char, 8> VULKAN_CACHE_MAGIC_NUMBER{'y', 'u', 'z', 'u', 'v', 'k', 'c', 'h'};
template <typename Container>
@@ -329,13 +325,13 @@ size_t GetTotalPipelineWorkers() {
const size_t max_core_threads =
std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL) - 1ULL;
#ifdef ANDROID
const int configured = AndroidSettings::values.pipeline_worker_count.GetValue();
const int clamped = std::clamp(configured, 4, 8);
const size_t desired = static_cast<size_t>(clamped);
if (desired == 0) {
// Leave at least one core free on Android. Previously we reserved two, but
// shipping builds benefit from one extra compilation worker.
constexpr size_t free_cores = 1ULL;
if (max_core_threads <= free_cores) {
return 1ULL;
}
return std::min(max_core_threads, desired);
return max_core_threads - free_cores;
#else
return max_core_threads;
#endif
@@ -430,12 +426,13 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
driver_id == VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA,
.has_broken_spirv_clamp = driver_id == VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS,
.has_broken_spirv_position_input = driver_id == false,
.has_broken_spirv_position_input = driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY,
.has_broken_unsigned_image_offsets = false,
.has_broken_signed_operations = false,
.has_broken_fp16_float_controls = driver_id == VK_DRIVER_ID_NVIDIA_PROPRIETARY,
.ignore_nan_fp_comparisons = false,
.has_broken_spirv_subgroup_mask_vector_extract_dynamic = false,
.has_broken_spirv_subgroup_mask_vector_extract_dynamic =
driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY,
.has_broken_robust =
device.IsNvidia() && device.GetNvidiaArch() <= NvidiaArchitecture::Arch_Pascal,
.min_ssbo_alignment = device.GetStorageBufferAlignment(),
@@ -5,6 +5,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <functional>
#include <map>
#include <memory>
#include <utility>
@@ -106,18 +108,39 @@ void ConfigurePerGameAddons::OnItemChanged(QStandardItem* item) {
void ConfigurePerGameAddons::ApplyConfiguration() {
std::vector<std::string> disabled_addons;
for (const auto& item : list_items) {
const auto disabled = item.front()->checkState() == Qt::Unchecked;
if (disabled) {
QVariant userData = item.front()->data(Qt::UserRole);
// Helper function to recursively collect disabled items
std::function<void(QStandardItem*)> collect_disabled = [&](QStandardItem* item) {
if (item == nullptr) {
return;
}
// Check if this item is disabled
if (item->isCheckable() && item->checkState() == Qt::Unchecked) {
QVariant userData = item->data(Qt::UserRole);
if (userData.isValid() && userData.canConvert<quint32>() &&
item.front()->text() == QStringLiteral("Update")) {
quint32 numeric_version = userData.toUInt();
item->text() == QStringLiteral("Update")) {
const quint32 numeric_version = userData.toUInt();
disabled_addons.push_back(fmt::format("Update@{}", numeric_version));
} else {
disabled_addons.push_back(item.front()->text().toStdString());
// Use the stored key from UserRole, falling back to text
const auto key = userData.toString();
if (!key.isEmpty()) {
disabled_addons.push_back(key.toStdString());
} else {
disabled_addons.push_back(item->text().toStdString());
}
}
}
// Process children (for cheats under mods)
for (int row = 0; row < item->rowCount(); ++row) {
collect_disabled(item->child(row, 0));
}
};
// Process all root items
for (int row = 0; row < item_model->rowCount(); ++row) {
collect_disabled(item_model->item(row, 0));
}
auto current = Settings::values.disabled_addons[title_id];
@@ -314,19 +337,40 @@ void ConfigurePerGameAddons::LoadConfiguration() {
bool has_enabled_update = false;
// Map to store parent items for mods (for adding cheat children)
std::map<std::string, QStandardItem*> mod_items;
for (const auto& patch : patches) {
const auto name = QString::fromStdString(patch.name);
// For cheats, we need to use the full key (parent::name) for storage
std::string storage_key;
if (patch.type == FileSys::PatchType::Cheat && !patch.parent_name.empty()) {
storage_key = patch.parent_name + "::" + patch.name;
} else {
storage_key = patch.name;
}
auto* const first_item = new QStandardItem;
first_item->setText(name);
first_item->setCheckable(true);
const bool is_external_update = patch.type == FileSys::PatchType::Update &&
patch.source == FileSys::PatchSource::External &&
patch.numeric_version != 0;
const bool is_mod = patch.type == FileSys::PatchType::Mod;
const bool is_incompatible_cheat =
patch.type == FileSys::PatchType::Cheat &&
patch.cheat_compat != FileSys::CheatCompatibility::Compatible;
if (is_incompatible_cheat) {
first_item->setCheckable(false);
first_item->setEnabled(false);
} else {
first_item->setCheckable(true);
first_item->setData(QString::fromStdString(storage_key), Qt::UserRole);
}
if (is_external_update) {
first_item->setData(static_cast<quint32>(patch.numeric_version), NUMERIC_VERSION);
} else if (is_mod) {
@@ -334,16 +378,18 @@ void ConfigurePerGameAddons::LoadConfiguration() {
}
bool patch_disabled = false;
if (is_external_update) {
std::string disabled_key = fmt::format("Update@{}", patch.numeric_version);
patch_disabled =
std::find(disabled.begin(), disabled.end(), disabled_key) != disabled.end();
} else {
patch_disabled =
std::find(disabled.begin(), disabled.end(), name.toStdString()) != disabled.end();
if (!is_incompatible_cheat) {
if (is_external_update) {
std::string disabled_key = fmt::format("Update@{}", patch.numeric_version);
patch_disabled =
std::find(disabled.begin(), disabled.end(), disabled_key) != disabled.end();
} else {
patch_disabled =
std::find(disabled.begin(), disabled.end(), storage_key) != disabled.end();
}
}
bool should_enable = !patch_disabled;
bool should_enable = !patch_disabled && !is_incompatible_cheat;
if (patch.type == FileSys::PatchType::Update) {
if (should_enable) {
@@ -356,12 +402,37 @@ void ConfigurePerGameAddons::LoadConfiguration() {
update_items.push_back(first_item);
}
first_item->setCheckState(should_enable ? Qt::Checked : Qt::Unchecked);
if (!is_incompatible_cheat) {
first_item->setCheckState(should_enable ? Qt::Checked : Qt::Unchecked);
}
list_items.push_back(QList<QStandardItem*>{
first_item, new QStandardItem{QString::fromStdString(patch.version)}});
item_model->appendRow(list_items.back());
auto* const version_item = new QStandardItem{QString::fromStdString(patch.version)};
if (is_incompatible_cheat) {
version_item->setEnabled(false);
}
if (patch.type == FileSys::PatchType::Cheat && !patch.parent_name.empty()) {
// This is a cheat - add as child of its parent mod
auto parent_it = mod_items.find(patch.parent_name);
if (parent_it != mod_items.end()) {
parent_it->second->appendRow(QList<QStandardItem*>{first_item, version_item});
} else {
// Parent not found (shouldn't happen), add to root
list_items.push_back(QList<QStandardItem*>{first_item, version_item});
item_model->appendRow(list_items.back());
}
} else {
// This is a top-level item (Update, Mod, DLC)
list_items.push_back(QList<QStandardItem*>{first_item, version_item});
item_model->appendRow(list_items.back());
// Store mod items for later cheat attachment
if (patch.type == FileSys::PatchType::Mod) {
mod_items[patch.name] = first_item;
}
}
}
tree_view->expandAll();
tree_view->resizeColumnToContents(1);
}