Compare commits
38 Commits
9d51866443
...
ng
| Author | SHA1 | Date | |
|---|---|---|---|
| a9fab50ada | |||
| a55a8c7456 | |||
| 08734c6ef7 | |||
| 5f10338d33 | |||
| c74e22bff0 | |||
| b8184c6ab4 | |||
| d0174bbf86 | |||
| 530dc2a76e | |||
| eeb6f1802c | |||
| d8f8536b8b | |||
| b73a035311 | |||
| 97458d893e | |||
| 43c24c63c7 | |||
| 7f4d511715 | |||
| ba53ad1da4 | |||
| 6c07355601 | |||
| 802c258a99 | |||
| 8f1b7cc196 | |||
| b16c7508f0 | |||
| 590645b13c | |||
| 84897a409b | |||
| 5323617ca6 | |||
| 0e9837a75a | |||
| 87e6c63aae | |||
| 85fc2ad3ce | |||
| 06482e2218 | |||
| c42305c8d2 | |||
| 49940b43d5 | |||
| 941e59e471 | |||
| ab5a68bed7 | |||
| 4aaf64eae5 | |||
| eb40906975 | |||
| ee6a565ce0 | |||
| 334accd070 | |||
| 9d41119710 | |||
| 86ea296a1b | |||
| a1b1fcbf7b | |||
| 02118f4c0a |
@@ -1,57 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
sodipodi:docname="HorizontalLayout.svg"
|
|
||||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="32.916667"
|
|
||||||
inkscape:cx="12"
|
|
||||||
inkscape:cy="12"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1017"
|
|
||||||
inkscape:window-x="-8"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg1" />
|
|
||||||
<path
|
|
||||||
d="M 20,2 H 4 C 2.9,2 2,2.9 2,4 v 16 c 0,1.1 0.9,2 2,2 h 16 c 1.1,0 2,-0.9 2,-2 V 4 C 22,2.9 21.1,2 20,2 M 6.5,20 H 4 V 4 H 6.5 V 20 M 11,20 H 8.5 V 4 H 11 v 16 m 4.5,0 H 13 V 4 h 2.5 V 20 M 20,20 H 17.5 V 4 H 20 Z"
|
|
||||||
id="path1"
|
|
||||||
style="display:none" />
|
|
||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.000911392"
|
|
||||||
d="M 17.513924,12 V 4.0101266 h 1.23038 1.23038 V 12 19.989873 h -1.23038 -1.23038 z"
|
|
||||||
id="path2" />
|
|
||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.000911392"
|
|
||||||
d="M 13.017722,12 V 4.0101266 h 1.230379 1.23038 V 12 19.989873 h -1.23038 -1.230379 z"
|
|
||||||
id="path3" />
|
|
||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.000911392"
|
|
||||||
d="M 8.521519,12 V 4.0101266 H 9.7518987 10.982278 V 12 19.989873 H 9.7518987 8.521519 Z"
|
|
||||||
id="path4" />
|
|
||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.000911392"
|
|
||||||
d="M 4.0253165,12 V 4.0101266 H 5.2556962 6.4860759 V 12 19.989873 H 5.2556962 4.0253165 Z"
|
|
||||||
id="path5" />
|
|
||||||
<path
|
|
||||||
style="fill:#607d8b;fill-opacity:1;stroke-width:0.000911392"
|
|
||||||
d="M 3.8126582,21.975791 C 3.409833,21.91582 3.0889628,21.785022 2.7913767,21.559483 2.4877736,21.329384 2.2588732,21.010805 2.1154671,20.618767 L 2.035443,20.4 2.0275241,12.098344 C 2.0187718,2.9230804 2.0049609,3.5827436 2.2148669,3.1501174 2.4741771,2.6156669 2.9780349,2.2110802 3.5696203,2.0622805 3.8184349,1.9996968 20.18008,1.99945 20.43038,2.0620261 c 0.694973,0.1737467 1.236637,0.6755781 1.463303,1.3556954 l 0.07087,0.2126582 V 12 20.36962 l -0.07087,0.212658 c -0.220781,0.662458 -0.70682,1.12592 -1.412833,1.347206 -0.142686,0.04472 -0.896441,0.04953 -8.374521,0.05338 -4.5197467,0.0023 -8.2518986,-8.54e-4 -8.2936708,-0.0071 z M 6.5164557,12 V 3.9797468 H 5.2556962 3.9949367 V 12 20.020253 h 1.2607595 1.2607595 z m 4.4962023,0 V 3.9797468 H 9.7518987 8.4911392 V 12 20.020253 h 1.2607595 1.2607593 z m 4.496203,0 V 3.9797468 h -1.26076 -1.260759 V 12 20.020253 h 1.260759 1.26076 z m 4.496202,0 V 3.9797468 h -1.260759 -1.26076 V 12 20.020253 h 1.26076 1.260759 z"
|
|
||||||
id="path6" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,57 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
sodipodi:docname="VerticalLayout.svg"
|
|
||||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="32.916667"
|
|
||||||
inkscape:cx="12"
|
|
||||||
inkscape:cy="12"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1017"
|
|
||||||
inkscape:window-x="-8"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg1" />
|
|
||||||
<path
|
|
||||||
d="M 22,20 V 4 C 22,2.9 21.1,2 20,2 H 4 C 2.9,2 2,2.9 2,4 v 16 c 0,1.1 0.9,2 2,2 h 16 c 1.1,0 2,-0.9 2,-2 M 4,6.5 V 4 H 20 V 6.5 H 4 M 4,11 V 8.5 H 20 V 11 H 4 m 0,4.5 V 13 h 16 v 2.5 H 4 M 4,20 V 17.5 H 20 V 20 Z"
|
|
||||||
id="path1"
|
|
||||||
style="display:none" />
|
|
||||||
<path
|
|
||||||
style="fill:#607d8b;fill-opacity:1;stroke-width:0.000911392"
|
|
||||||
d="M 3.8126582,21.975791 C 3.409833,21.91582 3.0889628,21.785022 2.7913767,21.559483 2.4877736,21.329384 2.2588732,21.010805 2.1154671,20.618767 L 2.035443,20.4 2.0275241,12.098344 C 2.0187718,2.9230804 2.0049609,3.5827436 2.2148669,3.1501174 2.4741771,2.6156669 2.9780349,2.2110802 3.5696203,2.0622805 3.8184349,1.9996968 20.18008,1.99945 20.43038,2.0620261 c 0.694973,0.1737467 1.236637,0.6755781 1.463303,1.3556954 l 0.07087,0.2126582 V 12 20.36962 l -0.07087,0.212658 c -0.220781,0.662458 -0.70682,1.12592 -1.412833,1.347206 -0.142686,0.04472 -0.896441,0.04953 -8.374521,0.05338 -4.5197467,0.0023 -8.2518986,-8.54e-4 -8.2936708,-0.0071 z M 20.005063,18.759494 v -1.26076 H 12 3.9949367 v 1.26076 1.260759 H 12 20.005063 Z m 0,-4.511393 V 12.972152 H 12 3.9949367 v 1.275949 1.27595 H 12 20.005063 Z m 0,-4.4962023 V 8.4759494 H 12 3.9949367 V 9.7518987 11.027848 H 12 20.005063 Z m 0,-4.5113924 V 3.9797468 H 12 3.9949367 V 5.2405063 6.5012658 H 12 20.005063 Z"
|
|
||||||
id="path2" />
|
|
||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.000911392"
|
|
||||||
d="m 4.0253165,18.759494 v -1.23038 H 12 19.974684 v 1.23038 1.230379 H 12 4.0253165 Z"
|
|
||||||
id="path3" />
|
|
||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.000911392"
|
|
||||||
d="M 4.0253165,14.248101 V 13.002532 H 12 19.974684 v 1.245569 1.24557 H 12 4.0253165 Z"
|
|
||||||
id="path4" />
|
|
||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.000911392"
|
|
||||||
d="M 4.0253165,9.7518987 V 8.5063291 H 12 19.974684 V 9.7518987 10.997468 H 12 4.0253165 Z"
|
|
||||||
id="path5" />
|
|
||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.000911392"
|
|
||||||
d="M 4.0253165,5.2405063 V 4.0101266 H 12 19.974684 V 5.2405063 6.4708861 H 12 4.0253165 Z"
|
|
||||||
id="path6" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,31 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.14.36414.22
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BallanceTasEditor", "BallanceTasEditor\BallanceTasEditor.csproj", "{DD898514-03ED-4257-AFD1-290EEDF68113}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BallanceTasEditorTests", "BallanceTasEditorTests\BallanceTasEditorTests.csproj", "{D2E825CE-691B-48D7-8D87-D2CED1B25FF9}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{DD898514-03ED-4257-AFD1-290EEDF68113}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{DD898514-03ED-4257-AFD1-290EEDF68113}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{DD898514-03ED-4257-AFD1-290EEDF68113}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{DD898514-03ED-4257-AFD1-290EEDF68113}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{D2E825CE-691B-48D7-8D87-D2CED1B25FF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{D2E825CE-691B-48D7-8D87-D2CED1B25FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{D2E825CE-691B-48D7-8D87-D2CED1B25FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{D2E825CE-691B-48D7-8D87-D2CED1B25FF9}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
SolutionGuid = {5A4468A2-79ED-47F3-80FE-299A89DE9D0E}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
44
BallanceTasEditor/Assets/AppIcons/ClearKeys.svg
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="ClearKeys.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="32.916667"
|
||||||
|
inkscape:cx="12"
|
||||||
|
inkscape:cy="12"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1017"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
d="m 19.36,2.72 1.42,1.42 -5.72,5.71 c 1.07,1.54 1.22,3.39 0.32,4.59 L 9.06,8.12 c 1.2,-0.9 3.05,-0.75 4.59,0.32 L 19.36,2.72 M 5.93,17.57 C 3.92,15.56 2.69,13.16 2.35,10.92 l 4.88,-2.09 7.44,7.44 -2.09,4.88 C 10.34,20.81 7.94,19.58 5.93,17.57 Z"
|
||||||
|
id="path1"
|
||||||
|
style="display:none" />
|
||||||
|
<path
|
||||||
|
style="fill:#607d8b;stroke-width:0.000911392;fill-opacity:1"
|
||||||
|
d="M 12.136709,21.054947 C 9.849802,20.570498 7.6401138,19.296543 5.8025316,17.403096 4.4028758,15.960889 3.3992023,14.37058 2.7993906,12.644655 2.6463627,12.204326 2.4273042,11.35402 2.3985171,11.088608 L 2.3803943,10.921519 4.7251585,9.9189873 C 6.0147788,9.3675949 7.1043011,8.9044989 7.1463187,8.8898851 c 0.071606,-0.024904 0.3085786,0.2056059 3.7789633,3.6759029 2.036411,2.03636 3.702566,3.714727 3.702566,3.729703 0,0.01498 -0.461067,1.105251 -1.024592,2.422831 l -1.024593,2.395602 -0.09186,-0.0021 c -0.05053,-0.0012 -0.208066,-0.02675 -0.350091,-0.05684 z"
|
||||||
|
id="path2" />
|
||||||
|
<path
|
||||||
|
style="fill:#607d8b;fill-opacity:1;stroke-width:0.000911392"
|
||||||
|
d="M 12.228787,11.256636 9.0890771,8.1168152 9.262956,8.0055861 c 0.537045,-0.3435433 1.29721,-0.5112188 2.003468,-0.4419202 0.416509,0.040868 0.869029,0.1466631 1.241447,0.2902384 0.298885,0.1152267 0.773018,0.3576119 0.98551,0.5038104 0.07023,0.04832 0.138874,0.087855 0.152539,0.087855 0.01366,0 1.306513,-1.281613 2.872996,-2.8480292 l 2.84815,-2.8480293 0.691078,0.6910787 0.691079,0.6910786 -2.855424,2.8555452 C 15.12356,9.75757 15.039674,9.8448024 15.081906,9.911253 c 0.598284,0.941377 0.857172,1.729415 0.860857,2.620393 0.002,0.475353 -0.04848,0.782692 -0.194207,1.183063 -0.08105,0.222676 -0.299494,0.631956 -0.355907,0.666821 -0.01328,0.0082 -1.437021,-1.397993 -3.163862,-3.124894 z"
|
||||||
|
id="path3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
48
BallanceTasEditor/Assets/AppIcons/EditorLayout.svg
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="EditorLayout.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="32.916667"
|
||||||
|
inkscape:cx="12"
|
||||||
|
inkscape:cy="12"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1017"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
d="m 17,16.88 c 0.56,0 1,0.44 1,1 0,0.56 -0.44,1 -1,1 -0.56,0 -1,-0.45 -1,-1 0,-0.55 0.44,-1 1,-1 m 0,-3 c 2.73,0 5.06,1.66 6,4 -0.94,2.34 -3.27,4 -6,4 -2.73,0 -5.06,-1.66 -6,-4 0.94,-2.34 3.27,-4 6,-4 m 0,1.5 c -1.38,0 -2.5,1.12 -2.5,2.5 0,1.38 1.12,2.5 2.5,2.5 1.38,0 2.5,-1.12 2.5,-2.5 0,-1.38 -1.12,-2.5 -2.5,-2.5 M 18,3 H 4 C 2.9,3 2,3.9 2,5 v 12 c 0,1.1 0.9,2 2,2 H 9.42 C 9.26,18.68 9.12,18.34 9,18 9.12,17.66 9.26,17.32 9.42,17 H 4 v -4 h 6 v 2.97 c 0.55,-0.86 1.23,-1.6 2,-2.21 V 13 h 1.15 c 1.16,-0.64 2.47,-1 3.85,-1 1.06,0 2.07,0.21 3,0.59 V 5 C 20,3.9 19.1,3 18,3 m -8,8 H 4 V 7 h 6 v 4 m 8,0 H 12 V 7 h 6 z"
|
||||||
|
id="path1"
|
||||||
|
style="display:none" />
|
||||||
|
<path
|
||||||
|
style="fill:#607d8b;stroke-width:0.000911392"
|
||||||
|
d="M 3.6628462,18.954277 C 2.9389876,18.812286 2.3759743,18.324642 2.1168369,17.615228 l -0.081394,-0.222823 -0.00795,-6.31273 C 2.0187833,4.1608898 2.0079771,4.5989137 2.1975777,4.1854184 2.4627257,3.6071637 3.0327014,3.1622473 3.6490655,3.0524039 4.0078744,2.98846 18.006733,2.9865703 18.3391,3.050421 c 0.772499,0.1484042 1.363905,0.6920224 1.57275,1.4456672 0.06203,0.2238257 0.06283,0.2770438 0.06283,4.1387442 0,2.1516016 -0.01025,3.9119406 -0.02278,3.9118636 -0.01253,-7.7e-5 -0.130705,-0.04212 -0.262607,-0.09342 -0.298338,-0.116046 -1.048666,-0.308327 -1.491824,-0.3823 -0.478751,-0.07991 -1.886507,-0.08033 -2.36962,-7.01e-4 -0.888588,0.146464 -1.949529,0.494282 -2.526929,0.828427 -0.122446,0.07086 -0.147925,0.07345 -0.721519,0.07345 h -0.59459 v 0.389632 0.389632 l -0.205063,0.164798 c -0.480657,0.386278 -1.187873,1.161369 -1.598081,1.751459 l -0.15635,0.22491 -0.0079,-1.460216 -0.0079,-1.460215 H 7.00224 3.9949367 v 2.020253 2.020253 h 2.6886076 c 1.4807347,0 2.6886076,0.01174 2.6886076,0.02613 0,0.01437 -0.052575,0.140828 -0.1168326,0.281013 -0.064258,0.140185 -0.1534676,0.351781 -0.1982437,0.470213 l -0.081411,0.215332 0.134109,0.333693 c 0.07376,0.183531 0.1629697,0.390265 0.1982436,0.459407 0.035274,0.06914 0.064135,0.13993 0.064135,0.157305 0,0.03934 -5.5084997,0.03791 -5.7093057,-0.0015 z M 10.010127,9.0075949 V 6.9873418 H 7.0025316 3.9949367 v 2.0202531 2.0202531 h 3.0075949 3.0075954 z m 7.989873,0 V 6.9873418 H 14.992405 11.98481 V 9.0075949 11.027848 H 14.992405 18 Z"
|
||||||
|
id="path2" />
|
||||||
|
<path
|
||||||
|
style="fill:#2196f3;stroke-width:0.000911392;fill-opacity:1"
|
||||||
|
d="m 16.426066,21.8414 c -1.029487,-0.09183 -2.15905,-0.487628 -3.013408,-1.055912 -0.958091,-0.637283 -1.676676,-1.447469 -2.190361,-2.46958 -0.23281,-0.463235 -0.231597,-0.392375 -0.01432,-0.836503 0.780517,-1.595445 2.19126,-2.786582 3.914876,-3.305459 1.824557,-0.549264 3.823645,-0.266322 5.413859,0.766254 1.009506,0.655504 1.846896,1.618122 2.329721,2.67812 l 0.123713,0.2716 -0.186695,0.378922 c -0.715355,1.451907 -1.925808,2.556638 -3.464893,3.162265 -0.353851,0.13924 -0.974949,0.300502 -1.400537,0.363635 -0.41,0.06082 -1.110727,0.08244 -1.511952,0.04666 z m 1.285326,-1.547018 c 0.362606,-0.115278 0.723028,-0.320767 0.974453,-0.55557 0.376823,-0.351912 0.585002,-0.684396 0.730974,-1.167446 0.07096,-0.234832 0.08086,-0.319606 0.08086,-0.692885 0,-0.373279 -0.0099,-0.458053 -0.08086,-0.692885 -0.213191,-0.705491 -0.660219,-1.237582 -1.315696,-1.566056 -0.446454,-0.223727 -1.086544,-0.305521 -1.591507,-0.20337 -1.296023,0.262177 -2.165591,1.462516 -2.003867,2.766108 0.133179,1.073496 0.897442,1.909623 1.975264,2.161001 0.268477,0.06262 0.966925,0.03486 1.230379,-0.0489 z"
|
||||||
|
id="path3" />
|
||||||
|
<path
|
||||||
|
style="fill:#2196f3;fill-opacity:1;stroke-width:0.000911392"
|
||||||
|
d="m 16.70278,18.811229 c -0.316978,-0.113491 -0.534483,-0.329272 -0.63793,-0.632872 -0.06423,-0.188514 -0.04057,-0.52591 0.0503,-0.717166 0.224074,-0.471628 0.817052,-0.692295 1.278645,-0.475827 0.222808,0.104487 0.362138,0.230984 0.471128,0.427734 0.08235,0.148668 0.08951,0.185873 0.08951,0.465383 0,0.280009 -0.0071,0.316545 -0.09017,0.466597 -0.228247,0.412084 -0.739454,0.617254 -1.161479,0.466151 z"
|
||||||
|
id="path4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 100 KiB |
@@ -1,215 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
|
||||||
<PropertyGroup>
|
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
|
||||||
<ProjectGuid>{DD898514-03ED-4257-AFD1-290EEDF68113}</ProjectGuid>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<RootNamespace>BallanceTasEditor</RootNamespace>
|
|
||||||
<AssemblyName>BallanceTasEditor</AssemblyName>
|
|
||||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
|
||||||
<FileAlignment>512</FileAlignment>
|
|
||||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
|
||||||
<Deterministic>true</Deterministic>
|
|
||||||
<NuGetPackageImportStamp>
|
|
||||||
</NuGetPackageImportStamp>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
|
||||||
<DebugType>full</DebugType>
|
|
||||||
<Optimize>false</Optimize>
|
|
||||||
<OutputPath>bin\Debug\</OutputPath>
|
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
|
||||||
<DebugType>pdbonly</DebugType>
|
|
||||||
<Optimize>true</Optimize>
|
|
||||||
<OutputPath>bin\Release\</OutputPath>
|
|
||||||
<DefineConstants>TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<ApplicationIcon>App.ico</ApplicationIcon>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="System" />
|
|
||||||
<Reference Include="System.ComponentModel.DataAnnotations" />
|
|
||||||
<Reference Include="System.Data" />
|
|
||||||
<Reference Include="System.Numerics" />
|
|
||||||
<Reference Include="System.Xml" />
|
|
||||||
<Reference Include="Microsoft.CSharp" />
|
|
||||||
<Reference Include="System.Core" />
|
|
||||||
<Reference Include="System.Xml.Linq" />
|
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
|
||||||
<Reference Include="System.Net.Http" />
|
|
||||||
<Reference Include="System.Xaml">
|
|
||||||
<RequiredTargetFramework>4.0</RequiredTargetFramework>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="WindowsBase" />
|
|
||||||
<Reference Include="PresentationCore" />
|
|
||||||
<Reference Include="PresentationFramework" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ApplicationDefinition Include="App.xaml">
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</ApplicationDefinition>
|
|
||||||
<Compile Include="Styles\AccessoryIcon.cs" />
|
|
||||||
<Compile Include="Views\AboutDialog.xaml.cs">
|
|
||||||
<DependentUpon>AboutDialog.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Views\GotoDialog.xaml.cs">
|
|
||||||
<DependentUpon>GotoDialog.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Views\NewFileDialog.xaml.cs">
|
|
||||||
<DependentUpon>NewFileDialog.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Views\PreferenceDialog.xaml.cs">
|
|
||||||
<DependentUpon>PreferenceDialog.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Views\SetupCountAndFpsDialog.xaml.cs">
|
|
||||||
<DependentUpon>SetupCountAndFpsDialog.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Page Include="Styles\AccessoryIconControl.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
<Page Include="Styles\GenericButton.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
<Page Include="Styles\NoteBanner.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
<Page Include="Views\AboutDialog.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
<Page Include="Views\GotoDialog.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
<Page Include="Views\MainWindow.xaml">
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</Page>
|
|
||||||
<Compile Include="App.xaml.cs">
|
|
||||||
<DependentUpon>App.xaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Settings.cs" />
|
|
||||||
<Compile Include="Utils\FpsConverter.cs" />
|
|
||||||
<Compile Include="Utils\TasFrame.cs" />
|
|
||||||
<Compile Include="Utils\TasStorage.cs" />
|
|
||||||
<Compile Include="Views\MainWindow.xaml.cs">
|
|
||||||
<DependentUpon>MainWindow.xaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Page Include="Views\NewFileDialog.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
<Page Include="Views\PreferenceDialog.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
<Page Include="Views\SetupCountAndFpsDialog.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="Properties\AssemblyInfo.cs">
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Properties\Resources.Designer.cs">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
<DependentUpon>Resources.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Properties\Settings.Designer.cs">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Settings.settings</DependentUpon>
|
|
||||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
|
||||||
</Compile>
|
|
||||||
<EmbeddedResource Include="Properties\Resources.resx">
|
|
||||||
<Generator>ResXFileCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
|
||||||
</EmbeddedResource>
|
|
||||||
<None Include="Properties\Settings.settings">
|
|
||||||
<Generator>SettingsSingleFileGenerator</Generator>
|
|
||||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="App.config" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Models\" />
|
|
||||||
<Folder Include="ViewModels\" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Resource Include="Assets\App.ico" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Resource Include="App.ico" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Resource Include="Assets\About.ico" />
|
|
||||||
<Resource Include="Assets\AddFrame.ico" />
|
|
||||||
<Resource Include="Assets\Cancel.ico" />
|
|
||||||
<Resource Include="Assets\CloseFile.ico" />
|
|
||||||
<Resource Include="Assets\CopyFrame.ico" />
|
|
||||||
<Resource Include="Assets\CutFrame.ico" />
|
|
||||||
<Resource Include="Assets\DeleteFrame.ico" />
|
|
||||||
<Resource Include="Assets\DrawMode.ico" />
|
|
||||||
<Resource Include="Assets\EntrySpan.ico" />
|
|
||||||
<Resource Include="Assets\Exit.ico" />
|
|
||||||
<Resource Include="Assets\FillMode.ico" />
|
|
||||||
<Resource Include="Assets\FlipCell.ico" />
|
|
||||||
<Resource Include="Assets\Fps.ico" />
|
|
||||||
<Resource Include="Assets\HorizontalLayout.ico" />
|
|
||||||
<Resource Include="Assets\NewFile.ico" />
|
|
||||||
<Resource Include="Assets\Ok.ico" />
|
|
||||||
<Resource Include="Assets\OpenFile.ico" />
|
|
||||||
<Resource Include="Assets\Goto.ico" />
|
|
||||||
<Resource Include="Assets\NextItem.ico" />
|
|
||||||
<Resource Include="Assets\NextPage.ico" />
|
|
||||||
<Resource Include="Assets\PreviousItem.ico" />
|
|
||||||
<Resource Include="Assets\PreviousPage.ico" />
|
|
||||||
<Resource Include="Assets\PasteFrame.ico" />
|
|
||||||
<Resource Include="Assets\Preference.ico" />
|
|
||||||
<Resource Include="Assets\Redo.ico" />
|
|
||||||
<Resource Include="Assets\ReportBug.ico" />
|
|
||||||
<Resource Include="Assets\SaveFile.ico" />
|
|
||||||
<Resource Include="Assets\SaveFileAs.ico" />
|
|
||||||
<Resource Include="Assets\SaveFileThenRunGame.ico" />
|
|
||||||
<Resource Include="Assets\SelectMode.ico" />
|
|
||||||
<Resource Include="Assets\SetCell.ico" />
|
|
||||||
<Resource Include="Assets\SetFps.ico" />
|
|
||||||
<Resource Include="Assets\Undo.ico" />
|
|
||||||
<Resource Include="Assets\UnsetCell.ico" />
|
|
||||||
<Resource Include="Assets\VerticalLayout.ico" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Resource Include="Assets\Count.ico" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm">
|
|
||||||
<Version>8.2.1</Version>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="DotNetZip">
|
|
||||||
<Version>1.9.1.8</Version>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
|
||||||
</Project>
|
|
||||||
4
BallanceTasEditor/BallanceTasEditor.slnx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="BallanceTasEditor/BallanceTasEditor.csproj" />
|
||||||
|
<Project Path="BallanceTasEditorTests/BallanceTasEditorTests.csproj" />
|
||||||
|
</Solution>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<Application x:Class="BallanceTasEditor.App"
|
<Application x:Class="BallanceTasEditor.App"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:BallanceTasEditor"
|
xmlns:local="clr-namespace:BallanceTasEditor"
|
||||||
StartupUri="Views/MainWindow.xaml">
|
StartupUri="Frontend/Views/MainWindow.xaml">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ResourceDictionary Source="/Styles/AccessoryIconControl.xaml"/>
|
<ResourceDictionary Source="/Frontend/Styles/AccessoryIconControl.xaml"/>
|
||||||
<ResourceDictionary Source="/Styles/NoteBanner.xaml"/>
|
<ResourceDictionary Source="/Frontend/Styles/NoteBanner.xaml"/>
|
||||||
<ResourceDictionary Source="/Styles/GenericButton.xaml"/>
|
<ResourceDictionary Source="/Frontend/Styles/GenericButton.xaml"/>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
using System.Configuration;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace BallanceTasEditor {
|
namespace BallanceTasEditor {
|
||||||
10
BallanceTasEditor/BallanceTasEditor/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
[assembly: ThemeInfo(
|
||||||
|
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// or application resource dictionaries)
|
||||||
|
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// app, or any theme specific resource dictionaries)
|
||||||
|
)]
|
||||||
20
BallanceTasEditor/BallanceTasEditor/Backend/AppSettings.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BallanceTasEditor.Backend {
|
||||||
|
|
||||||
|
public enum EditorLayoutKind {
|
||||||
|
Horizontal, Vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AppSettings {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public EditorLayoutKind EditorLayout { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BallanceTasEditor.Backend {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一种提前给定元素个数的的IEnumerable。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
public interface IExactSizeEnumerable<out T> : IEnumerable<T> {
|
||||||
|
/// <summary>
|
||||||
|
/// 该迭代器会返回的元素的个数。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 如果迭代器返回的元素个数与该方法给定的个数不同,
|
||||||
|
/// 则是未定义行为。
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>迭代器会返回的元素的准确个数。大于等于0。</returns>
|
||||||
|
public int GetCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将普通IEnumerable转变为IExactSizeEnumerable的适配器。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
public sealed class ExactSizeEnumerableAdapter<T> : IExactSizeEnumerable<T> {
|
||||||
|
/// <summary>
|
||||||
|
/// 以迭代器和指定长度构建适配器。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 如果迭代器返回的元素个数与该方法给定的个数不同,
|
||||||
|
/// 则是未定义行为。
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="enumerable">一个迭代器,其最多只能迭代给定次数。</param>
|
||||||
|
/// <param name="count">迭代器会迭代的次数。</param>
|
||||||
|
public ExactSizeEnumerableAdapter(IEnumerable<T> enumerable, int count) {
|
||||||
|
m_Inner = enumerable;
|
||||||
|
m_Count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IEnumerable<T> m_Inner;
|
||||||
|
private readonly int m_Count;
|
||||||
|
|
||||||
|
public IEnumerator<T> GetEnumerator() {
|
||||||
|
return m_Inner.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() {
|
||||||
|
return m_Inner.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetCount() {
|
||||||
|
return m_Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
190
BallanceTasEditor/BallanceTasEditor/Backend/FileWatcher.cs
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BallanceTasEditor.Backend {
|
||||||
|
public class FileWatcher : IDisposable {
|
||||||
|
/// <summary>
|
||||||
|
/// Create a file watcher.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This new created file watcher is not watching specified file
|
||||||
|
/// unless you explicitly call <see cref="Start"/> method.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="filepath">The path to watching file.</param>
|
||||||
|
public FileWatcher(string filepath) {
|
||||||
|
m_FilePath = filepath;
|
||||||
|
m_IsWatching = false;
|
||||||
|
m_EventMutex = new Mutex();
|
||||||
|
m_IsEventProcessing = false;
|
||||||
|
|
||||||
|
// Get directory and file info
|
||||||
|
string directory = Path.GetDirectoryName(filepath) ?? throw new ArgumentException("Invalid file path", nameof(filepath));
|
||||||
|
string filename = Path.GetFileName(filepath);
|
||||||
|
|
||||||
|
// Create FileSystemWatcher
|
||||||
|
m_FileSystemWatcher = new FileSystemWatcher(directory, filename) {
|
||||||
|
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.FileName,
|
||||||
|
EnableRaisingEvents = false
|
||||||
|
};
|
||||||
|
m_FileSystemWatcher.Changed += OnFileSystemChanged;
|
||||||
|
m_FileSystemWatcher.Deleted += OnFileSystemDeleted;
|
||||||
|
m_FileSystemWatcher.Renamed += OnFileSystemRenamed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The path to watching file.
|
||||||
|
/// </summary>
|
||||||
|
private string m_FilePath;
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the file watcher is watching file.
|
||||||
|
/// </summary>
|
||||||
|
private bool m_IsWatching;
|
||||||
|
/// <summary>
|
||||||
|
/// The FileSystemWatcher instance.
|
||||||
|
/// </summary>
|
||||||
|
private readonly FileSystemWatcher m_FileSystemWatcher;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flag to indicate if disposed.
|
||||||
|
/// </summary>
|
||||||
|
private bool m_Disposed = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start watching file.
|
||||||
|
/// </summary>
|
||||||
|
public void Start() {
|
||||||
|
if (m_Disposed) throw new ObjectDisposedException(nameof(FileWatcher));
|
||||||
|
|
||||||
|
if (m_IsWatching) {
|
||||||
|
throw new InvalidOperationException("File watcher is already watching file.");
|
||||||
|
} else {
|
||||||
|
m_FileSystemWatcher.EnableRaisingEvents = true;
|
||||||
|
m_IsWatching = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop watching file.
|
||||||
|
/// </summary>
|
||||||
|
public void Stop() {
|
||||||
|
if (m_Disposed) throw new ObjectDisposedException(nameof(FileWatcher));
|
||||||
|
|
||||||
|
if (m_IsWatching) {
|
||||||
|
m_FileSystemWatcher.EnableRaisingEvents = false;
|
||||||
|
m_IsWatching = false;
|
||||||
|
} else {
|
||||||
|
throw new InvalidOperationException("File watcher is not watching file.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose the file watcher and release resources.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose() {
|
||||||
|
if (!m_Disposed) {
|
||||||
|
// Stop watching.
|
||||||
|
if (m_IsWatching) {
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
// Dispose members
|
||||||
|
m_FileSystemWatcher.Dispose();
|
||||||
|
m_EventMutex.Dispose();
|
||||||
|
m_Disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The event handler when file is modified.
|
||||||
|
/// </summary>
|
||||||
|
public delegate void FileModifiedHandler();
|
||||||
|
/// <summary>
|
||||||
|
/// The event handler when file is deleted.
|
||||||
|
/// </summary>
|
||||||
|
public delegate void FileDeletedHandler();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The event when file is modified.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Before user process this event completely,
|
||||||
|
/// there is no any other event will be triggered.
|
||||||
|
/// </remarks>
|
||||||
|
public event FileModifiedHandler? FileModified;
|
||||||
|
/// <summary>
|
||||||
|
/// The event when file is deleted.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Before user process this event completely,
|
||||||
|
/// there is no any other event will be triggered.
|
||||||
|
/// </remarks>
|
||||||
|
public event FileDeletedHandler? FileDeleted;
|
||||||
|
|
||||||
|
private Mutex m_EventMutex;
|
||||||
|
private bool m_IsEventProcessing;
|
||||||
|
|
||||||
|
private void OnFileModified() {
|
||||||
|
if (FileModified is not null) {
|
||||||
|
lock (m_EventMutex) {
|
||||||
|
if (m_IsEventProcessing) return;
|
||||||
|
else m_IsEventProcessing = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
FileModified.Invoke();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
lock (m_EventMutex) {
|
||||||
|
m_IsEventProcessing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFileDeleted() {
|
||||||
|
if (FileDeleted is not null) {
|
||||||
|
lock (m_EventMutex) {
|
||||||
|
if (m_IsEventProcessing) return;
|
||||||
|
else m_IsEventProcessing = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
FileDeleted.Invoke();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
lock (m_EventMutex) {
|
||||||
|
m_IsEventProcessing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for FileSystemWatcher Changed event.
|
||||||
|
/// </summary>
|
||||||
|
private void OnFileSystemChanged(object sender, FileSystemEventArgs e) {
|
||||||
|
// Filter out our own change notifications to avoid infinite loops
|
||||||
|
if (e.ChangeType == WatcherChangeTypes.Changed) {
|
||||||
|
OnFileModified();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for FileSystemWatcher Deleted event.
|
||||||
|
/// </summary>
|
||||||
|
private void OnFileSystemDeleted(object sender, FileSystemEventArgs e) {
|
||||||
|
OnFileDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for FileSystemWatcher Renamed event.
|
||||||
|
/// </summary>
|
||||||
|
private void OnFileSystemRenamed(object sender, RenamedEventArgs e) {
|
||||||
|
// Treat rename as a delete since the original file is gone
|
||||||
|
OnFileDeleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
BallanceTasEditor/BallanceTasEditor/Backend/FpsConverter.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BallanceTasEditor.Backend {
|
||||||
|
/// <summary>
|
||||||
|
/// FPS converter
|
||||||
|
/// </summary>
|
||||||
|
public static class FpsConverter {
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the FPS is valid
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fps">FPS in integer</param>
|
||||||
|
/// <returns>Is valid</returns>
|
||||||
|
public static bool IsValidFps(uint fps) {
|
||||||
|
return fps > 0;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the FPS is valid
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fps">FPS in float point</param>
|
||||||
|
/// <returns>Is valid</returns>
|
||||||
|
public static bool IsValidFps(float fps) {
|
||||||
|
return fps > 0;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the delta time is valid
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delta">Delta time in float point</param>
|
||||||
|
/// <returns>Is valid</returns>
|
||||||
|
public static bool IsValidDelta(float delta) {
|
||||||
|
return delta > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert float point delta time to float point FPS
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delta">Delta time in float point</param>
|
||||||
|
/// <returns>FPS in float point</returns>
|
||||||
|
public static float ToFps(float delta) {
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(delta, nameof(delta));
|
||||||
|
return 1f / delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert float point delta time to integer FPS
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delta">Delta time in float point</param>
|
||||||
|
/// <returns>FPS in round integer</returns>
|
||||||
|
public static uint ToRoundFps(float delta) {
|
||||||
|
return Convert.ToUInt32(ToFps(delta));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert integer FPS to float point delta time
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fps">FPS in integer</param>
|
||||||
|
/// <returns>Delta time in float point</returns>
|
||||||
|
public static float ToDelta(uint fps) {
|
||||||
|
return ToDelta((float)fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert float point FPS to float point delta time
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fps">FPS in float point</param>
|
||||||
|
/// <returns>Delta time in float point</returns>
|
||||||
|
public static float ToDelta(float fps) {
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(fps, nameof(fps));
|
||||||
|
return 1f / fps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
BallanceTasEditor/BallanceTasEditor/Backend/TasClipboard.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace BallanceTasEditor.Backend {
|
||||||
|
public class TasClipboard {
|
||||||
|
// Reference: https://stackoverflow.com/questions/22272822/copy-binary-data-to-clipboard
|
||||||
|
|
||||||
|
private static readonly string CLIPBOARD_DATA_FORMAT = "BallanceTasEditor.TasFrames";
|
||||||
|
|
||||||
|
public static void SetClipboard(IExactSizeEnumerable<TasFrame> frames) {
|
||||||
|
DataObject data = new DataObject();
|
||||||
|
var rawFrames = frames.Select((f) => f.ToRaw()).ToArray();
|
||||||
|
data.SetData(CLIPBOARD_DATA_FORMAT, rawFrames, false);
|
||||||
|
Clipboard.SetDataObject(data, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RawTasFrame[]? GetClipboardObject() {
|
||||||
|
DataObject? retrievedData = Clipboard.GetDataObject() as DataObject;
|
||||||
|
if (retrievedData is null) return null;
|
||||||
|
if (!retrievedData.GetDataPresent(CLIPBOARD_DATA_FORMAT)) return null;
|
||||||
|
|
||||||
|
RawTasFrame[]? rawFrames = retrievedData.GetData(CLIPBOARD_DATA_FORMAT) as RawTasFrame[];
|
||||||
|
if (rawFrames is null) return null;
|
||||||
|
|
||||||
|
return rawFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasClipboard() {
|
||||||
|
return GetClipboardObject() is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IExactSizeEnumerable<TasFrame>? GetClipboard() {
|
||||||
|
var rawFrames = GetClipboardObject();
|
||||||
|
if (rawFrames is null) return null;
|
||||||
|
|
||||||
|
return new EnumerableArray(rawFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class EnumerableArray : IExactSizeEnumerable<TasFrame> {
|
||||||
|
public EnumerableArray(RawTasFrame[] rawFrames) {
|
||||||
|
m_RawFrames = rawFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RawTasFrame[] m_RawFrames;
|
||||||
|
|
||||||
|
public IEnumerator<TasFrame> GetEnumerator() {
|
||||||
|
return m_RawFrames.Select((f) => TasFrame.FromRaw(f)).GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() {
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetCount() {
|
||||||
|
return m_RawFrames.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
297
BallanceTasEditor/BallanceTasEditor/Backend/TasFrame.cs
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BallanceTasEditor.Backend {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原始的TAS帧结构,与二进制结构保持一致。
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
[Serializable]
|
||||||
|
public struct RawTasFrame {
|
||||||
|
/// <summary>
|
||||||
|
/// 该帧的持续时间(以秒为单位)。
|
||||||
|
/// </summary>
|
||||||
|
public float TimeDelta;
|
||||||
|
/// <summary>
|
||||||
|
/// 该帧的按键组合。
|
||||||
|
/// </summary>
|
||||||
|
public uint KeyFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述TAS文件中的可能的按键。
|
||||||
|
/// </summary>
|
||||||
|
public struct TasKey : IEquatable<TasKey> {
|
||||||
|
private TasKey(int bitPos) {
|
||||||
|
m_BitPos = bitPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int m_BitPos;
|
||||||
|
|
||||||
|
public static readonly TasKey KEY_UP = new TasKey(0);
|
||||||
|
public static readonly TasKey KEY_DOWN = new TasKey(1);
|
||||||
|
public static readonly TasKey KEY_LEFT = new TasKey(2);
|
||||||
|
public static readonly TasKey KEY_RIGHT = new TasKey(3);
|
||||||
|
public static readonly TasKey KEY_SHIFT = new TasKey(4);
|
||||||
|
public static readonly TasKey KEY_SPACE = new TasKey(5);
|
||||||
|
public static readonly TasKey KEY_Q = new TasKey(6);
|
||||||
|
public static readonly TasKey KEY_ESC = new TasKey(7);
|
||||||
|
public static readonly TasKey KEY_ENTER = new TasKey(8);
|
||||||
|
|
||||||
|
public const int MIN_KEY_INDEX = 0;
|
||||||
|
public const int MAX_KEY_INDEX = 8;
|
||||||
|
|
||||||
|
public static bool IsValidIndex(int index) {
|
||||||
|
return index >= MIN_KEY_INDEX && index <= MAX_KEY_INDEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TasKey FromIndex(int index) {
|
||||||
|
if (index < MIN_KEY_INDEX || index > MAX_KEY_INDEX) {
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
} else {
|
||||||
|
return new TasKey(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ToIndex() {
|
||||||
|
return m_BitPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint ToBitMaskKey() {
|
||||||
|
return 1u << m_BitPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(TasKey other) {
|
||||||
|
return m_BitPos == other.m_BitPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) {
|
||||||
|
if (obj is TasKey other) {
|
||||||
|
return Equals(other);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return m_BitPos.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator ==(TasKey left, TasKey right) {
|
||||||
|
return left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator !=(TasKey left, TasKey right) {
|
||||||
|
return !left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return m_BitPos switch {
|
||||||
|
0 => "KeyUp",
|
||||||
|
1 => "KeyDown",
|
||||||
|
2 => "KeyLeft",
|
||||||
|
3 => "KeyRight",
|
||||||
|
4 => "KeyShift",
|
||||||
|
5 => "KeySpace",
|
||||||
|
6 => "KeyQ",
|
||||||
|
7 => "KeyEsc",
|
||||||
|
8 => "KeyEnter",
|
||||||
|
_ => $"KeyUnknown<Pos={m_BitPos}>"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述TAS文件中一帧的结构。
|
||||||
|
/// </summary>
|
||||||
|
public class TasFrame : IEquatable<TasFrame> {
|
||||||
|
private TasFrame(float timeDelta, uint keyFlags) {
|
||||||
|
m_TimeDelta = timeDelta;
|
||||||
|
m_KeyFlags = keyFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 以指定的FPS,无任何按键初始化当前帧。
|
||||||
|
/// </summary>
|
||||||
|
public static TasFrame FromFps(uint fps = 60) {
|
||||||
|
return new TasFrame(FpsConverter.ToDelta(fps), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从原始TAS数据初始化。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="raw">要用来初始化的原始数据。</param>
|
||||||
|
public static TasFrame FromRaw(RawTasFrame raw) {
|
||||||
|
return new TasFrame(raw.TimeDelta, raw.KeyFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将原始TAS数据覆写到自身
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="raw">要写入的原始TAS数据</param>
|
||||||
|
public void FromRawImplace(RawTasFrame raw) {
|
||||||
|
m_TimeDelta = raw.TimeDelta;
|
||||||
|
m_KeyFlags = raw.KeyFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为原始TAS数据。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>转换后的原始TAS数据。</returns>
|
||||||
|
public RawTasFrame ToRaw() {
|
||||||
|
return new RawTasFrame() { TimeDelta = m_TimeDelta, KeyFlags = m_KeyFlags };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原位转换为原始TAS数据。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="raw">以引用传递的原始TAS数据。</param>
|
||||||
|
public void ToRawImplace(ref RawTasFrame raw) {
|
||||||
|
raw.TimeDelta = m_TimeDelta;
|
||||||
|
raw.KeyFlags = m_KeyFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回自身的克隆(深拷贝)。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>自身的克隆。</returns>
|
||||||
|
public TasFrame Clone() {
|
||||||
|
return new TasFrame(m_TimeDelta, m_KeyFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 该帧的持续时间(以秒为单位)。
|
||||||
|
/// </summary>
|
||||||
|
private float m_TimeDelta;
|
||||||
|
/// <summary>
|
||||||
|
/// 该帧的按键组合。
|
||||||
|
/// </summary>
|
||||||
|
private uint m_KeyFlags;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取帧时间Delta。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>获取到的帧时间Delta。</returns>
|
||||||
|
public float GetTimeDelta() {
|
||||||
|
return m_TimeDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置帧时间Delta。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delta">要设置的帧时间Delta。</param>
|
||||||
|
public void SetTimeDelta(float delta) {
|
||||||
|
m_TimeDelta = delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断按键是否被按下。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">要检查的按键。</param>
|
||||||
|
/// <returns>true表示被按下,否则为false。</returns>
|
||||||
|
public bool IsKeyPressed(TasKey key) {
|
||||||
|
return (m_KeyFlags & key.ToBitMaskKey()) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置按键状态。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">要设置的按键。</param>
|
||||||
|
/// <param name="pressed">true表示设置为按下,否则为松开。</param>
|
||||||
|
public void SetKeyPressed(TasKey key, bool pressed = true) {
|
||||||
|
if (pressed) m_KeyFlags |= key.ToBitMaskKey();
|
||||||
|
else m_KeyFlags &= ~key.ToBitMaskKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 反转按键状态。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">要反转的按键。</param>
|
||||||
|
public void FlipKeyPressed(TasKey key) {
|
||||||
|
m_KeyFlags ^= key.ToBitMaskKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置Up键的按下状态。
|
||||||
|
/// </summary>
|
||||||
|
public bool KeyUpPressed { get { return IsKeyPressed(TasKey.KEY_UP); } set { SetKeyPressed(TasKey.KEY_UP, value); } }
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置Down键的按下状态。
|
||||||
|
/// </summary>
|
||||||
|
public bool KeyDownPressed { get { return IsKeyPressed(TasKey.KEY_DOWN); } set { SetKeyPressed(TasKey.KEY_DOWN, value); } }
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置Left键的按下状态。
|
||||||
|
/// </summary>
|
||||||
|
public bool KeyLeftPressed { get { return IsKeyPressed(TasKey.KEY_LEFT); } set { SetKeyPressed(TasKey.KEY_LEFT, value); } }
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置Right键的按下状态。
|
||||||
|
/// </summary>
|
||||||
|
public bool KeyRightPressed { get { return IsKeyPressed(TasKey.KEY_RIGHT); } set { SetKeyPressed(TasKey.KEY_RIGHT, value); } }
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置Shift键的按下状态。
|
||||||
|
/// </summary>
|
||||||
|
public bool KeyShiftPressed { get { return IsKeyPressed(TasKey.KEY_SHIFT); } set { SetKeyPressed(TasKey.KEY_SHIFT, value); } }
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置Space键的按下状态。
|
||||||
|
/// </summary>
|
||||||
|
public bool KeySpacePressed { get { return IsKeyPressed(TasKey.KEY_SPACE); } set { SetKeyPressed(TasKey.KEY_SPACE, value); } }
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置Q键的按下状态。
|
||||||
|
/// </summary>
|
||||||
|
public bool KeyQPressed { get { return IsKeyPressed(TasKey.KEY_Q); } set { SetKeyPressed(TasKey.KEY_Q, value); } }
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置Esc键的按下状态。
|
||||||
|
/// </summary>
|
||||||
|
public bool KeyEscPressed { get { return IsKeyPressed(TasKey.KEY_ESC); } set { SetKeyPressed(TasKey.KEY_ESC, value); } }
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置回车键的按下状态。
|
||||||
|
/// </summary>
|
||||||
|
public bool KeyEnterPressed { get { return IsKeyPressed(TasKey.KEY_ENTER); } set { SetKeyPressed(TasKey.KEY_ENTER, value); } }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清除所有按键,将所有按键设置为不按下。
|
||||||
|
/// </summary>
|
||||||
|
public void ClearKeyPressed() {
|
||||||
|
m_KeyFlags = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指示当前对象是否等于另一个 TasFrame 对象。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="other">要比较的 TasFrame 对象。</param>
|
||||||
|
/// <returns>如果两个对象相等则为 true,否则为 false。</returns>
|
||||||
|
public bool Equals(TasFrame? other) {
|
||||||
|
return other is not null &&
|
||||||
|
m_TimeDelta == other.m_TimeDelta &&
|
||||||
|
m_KeyFlags == other.m_KeyFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指示当前对象是否等于另一个对象。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">要比较的对象。</param>
|
||||||
|
/// <returns>如果两个对象相等则为 true,否则为 false。</returns>
|
||||||
|
public override bool Equals(object? obj) {
|
||||||
|
if (obj is TasFrame other) {
|
||||||
|
return Equals(other);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回此实例的哈希代码。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>32 位有符号整数哈希代码。</returns>
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return HashCode.Combine(m_TimeDelta, m_KeyFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
532
BallanceTasEditor/BallanceTasEditor/Backend/TasOperation.cs
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BallanceTasEditor.Backend {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TAS操作接口。所有TAS操作均需要支持此接口。
|
||||||
|
/// </summary>
|
||||||
|
public interface ITasOperation {
|
||||||
|
/// <summary>
|
||||||
|
/// 执行对应的TAS操作。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seq">所要操作的TAS存储容器。</param>
|
||||||
|
void Execute(ITasSequence seq);
|
||||||
|
/// <summary>
|
||||||
|
/// 检查该操作是否已经被执行过。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 所有Tas操作类创建后只能执行一次或者不执行,
|
||||||
|
/// 因此有此函数用于获取是否已经执行过。
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>如果已经执行过,返回true,否则返回false。</returns>
|
||||||
|
bool IsExecuted();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可撤销的TAS操作接口,所有可撤销的TAS操作均需支持此接口。
|
||||||
|
/// </summary>
|
||||||
|
public interface ITasRevocableOperation : ITasOperation {
|
||||||
|
/// <summary>
|
||||||
|
/// 撤销对应TAS操作。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seq">所要撤销操作的TAS存储容器。</param>
|
||||||
|
void Revoke(ITasSequence seq);
|
||||||
|
/// <summary>
|
||||||
|
/// 返回该TAS操作占用的内存大小。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 可撤销的TAS操作会在内存中存储一定数据,用于撤销对应操作。
|
||||||
|
/// 该函数返回的占用用于衡量该操作的开销。
|
||||||
|
/// 我们应当基于大小,而非写死的个数决定撤销栈中的最大操作次数,
|
||||||
|
/// 例如对于小型操作我们可以存储100个,对于大型操作则只能存储5个等。
|
||||||
|
/// 用于解决编辑者目前认为撤销栈大小不足的情况。
|
||||||
|
/// <para/>
|
||||||
|
/// 该函数返回的大小可以不是特别精确,但要准确反映空间复杂度。
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>占用的内存大小(以byte为单位)。</returns>
|
||||||
|
int Occupation();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class OperationUtils {
|
||||||
|
internal const int SIZEOF_DELTA_TIME = sizeof(float);
|
||||||
|
internal const int SIZEOF_KEYS = sizeof(uint);
|
||||||
|
internal const int SIZEOF_FRAME = SIZEOF_DELTA_TIME + SIZEOF_KEYS;
|
||||||
|
|
||||||
|
internal static readonly InvalidOperationException ExecutionEnvironment = new InvalidOperationException("Can not execute one TAS operation multiple times.");
|
||||||
|
internal static readonly InvalidOperationException RevokeEnvironment = new InvalidOperationException("Can not revoke an not executed TAS operation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CellKeysOperationKind {
|
||||||
|
Set, Unset, Flip
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CellKeysOperation : ITasRevocableOperation {
|
||||||
|
public static CellKeysOperation FromSingleCell(CellKeysOperationKind kind, int index, TasKey key) {
|
||||||
|
return new CellKeysOperation(kind, index, index, key, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CellKeysOperation FromCellRange(CellKeysOperationKind kind, int startIndex, int endIndex, TasKey startKey, TasKey endKey) {
|
||||||
|
return new CellKeysOperation(kind, startIndex, endIndex, startKey, endKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CellKeysOperation(CellKeysOperationKind kind, int startIndex, int endIndex, TasKey startKey, TasKey endKey) {
|
||||||
|
// Check arguments.
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(startKey.ToIndex(), endKey.ToIndex());
|
||||||
|
// Setup members.
|
||||||
|
m_Kind = kind;
|
||||||
|
m_StartIndex = startIndex;
|
||||||
|
m_EndIndex = endIndex;
|
||||||
|
m_StartKey = startKey;
|
||||||
|
m_EndKey = endKey;
|
||||||
|
m_FramesBackup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CellKeysOperationKind m_Kind;
|
||||||
|
private int m_StartIndex, m_EndIndex;
|
||||||
|
private TasKey m_StartKey, m_EndKey;
|
||||||
|
private RawTasFrame[]? m_FramesBackup;
|
||||||
|
|
||||||
|
[MemberNotNullWhen(true, nameof(m_FramesBackup))]
|
||||||
|
public bool IsExecuted() {
|
||||||
|
return m_FramesBackup is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(ITasSequence seq) {
|
||||||
|
if (IsExecuted()) {
|
||||||
|
throw OperationUtils.ExecutionEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check index range.
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_EndIndex, seq.GetCount());
|
||||||
|
ArgumentOutOfRangeException.ThrowIfLessThan(m_StartIndex, 0);
|
||||||
|
|
||||||
|
// Do backup and set values at the same time
|
||||||
|
int backupIndex = 0;
|
||||||
|
var backups = new RawTasFrame[m_EndIndex - m_StartIndex + 1];
|
||||||
|
// Pre-build key list for fast fetching.
|
||||||
|
var keys = Enumerable.Range(m_StartKey.ToIndex(), m_EndKey.ToIndex() - m_StartKey.ToIndex() + 1).Select((i) => TasKey.FromIndex(i)).ToArray();
|
||||||
|
foreach (var frame in seq.BatchlyVisit(m_StartIndex, m_EndIndex)) {
|
||||||
|
// Do backup
|
||||||
|
frame.ToRawImplace(ref backups[backupIndex++]);
|
||||||
|
// Modify keys
|
||||||
|
foreach (var key in keys) {
|
||||||
|
switch (m_Kind) {
|
||||||
|
case CellKeysOperationKind.Set:
|
||||||
|
frame.SetKeyPressed(key, true);
|
||||||
|
break;
|
||||||
|
case CellKeysOperationKind.Unset:
|
||||||
|
frame.SetKeyPressed(key, false);
|
||||||
|
break;
|
||||||
|
case CellKeysOperationKind.Flip:
|
||||||
|
frame.FlipKeyPressed(key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign backups
|
||||||
|
m_FramesBackup = backups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Revoke(ITasSequence seq) {
|
||||||
|
if (!IsExecuted()) {
|
||||||
|
throw OperationUtils.RevokeEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index range is checked,
|
||||||
|
// so we directly restore backup.
|
||||||
|
int backupIndex = 0;
|
||||||
|
foreach (var frame in seq.BatchlyVisit(m_StartIndex, m_EndIndex)) {
|
||||||
|
frame.FromRawImplace(m_FramesBackup[backupIndex++]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear backups
|
||||||
|
m_FramesBackup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Occupation() {
|
||||||
|
return (m_EndIndex - m_StartIndex) * OperationUtils.SIZEOF_FRAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FrameFpsOperation : ITasRevocableOperation {
|
||||||
|
public static FrameFpsOperation FromSingleFrame(int index, uint fps) {
|
||||||
|
return new FrameFpsOperation(index, index, fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FrameFpsOperation FromFrameRange(int startIndex, int endIndex, uint fps) {
|
||||||
|
return new FrameFpsOperation(startIndex, endIndex, fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FrameFpsOperation(int startIndex, int endIndex, uint fps) {
|
||||||
|
// Check arguments
|
||||||
|
if (!FpsConverter.IsValidFps(fps)) {
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(fps));
|
||||||
|
}
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
|
||||||
|
// Assign arguments
|
||||||
|
m_StartIndex = startIndex;
|
||||||
|
m_EndIndex = endIndex;
|
||||||
|
m_DeltaTime = FpsConverter.ToDelta(fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int m_StartIndex, m_EndIndex;
|
||||||
|
private float m_DeltaTime;
|
||||||
|
private float[]? m_DeltaTimesBackup;
|
||||||
|
|
||||||
|
[MemberNotNullWhen(true, nameof(m_DeltaTimesBackup))]
|
||||||
|
public bool IsExecuted() {
|
||||||
|
return m_DeltaTimesBackup is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(ITasSequence seq) {
|
||||||
|
if (IsExecuted()) {
|
||||||
|
throw OperationUtils.ExecutionEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check index range
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_EndIndex, seq.GetCount());
|
||||||
|
ArgumentOutOfRangeException.ThrowIfLessThan(m_StartIndex, 0);
|
||||||
|
|
||||||
|
// Do backup and set values at the same time
|
||||||
|
int backupIndex = 0;
|
||||||
|
var backups = new float[m_EndIndex - m_StartIndex + 1];
|
||||||
|
foreach (var frame in seq.BatchlyVisit(m_StartIndex, m_EndIndex)) {
|
||||||
|
// Do backup
|
||||||
|
backups[backupIndex++] = frame.GetTimeDelta();
|
||||||
|
// Modify delta time
|
||||||
|
frame.SetTimeDelta(m_DeltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign backups
|
||||||
|
m_DeltaTimesBackup = backups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Revoke(ITasSequence seq) {
|
||||||
|
if (!IsExecuted()) {
|
||||||
|
throw OperationUtils.RevokeEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index range is checked,
|
||||||
|
// so we directly restore backup.
|
||||||
|
int backupIndex = 0;
|
||||||
|
foreach (var frame in seq.BatchlyVisit(m_StartIndex, m_EndIndex)) {
|
||||||
|
frame.SetTimeDelta(m_DeltaTimesBackup[backupIndex++]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear backups
|
||||||
|
m_DeltaTimesBackup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Occupation() {
|
||||||
|
return (m_EndIndex - m_StartIndex) * OperationUtils.SIZEOF_DELTA_TIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RemoveFrameOperation : ITasRevocableOperation {
|
||||||
|
public RemoveFrameOperation(int startIndex, int endIndex) {
|
||||||
|
// Check arguments
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
|
||||||
|
// Assign arguments
|
||||||
|
m_StartIndex = startIndex;
|
||||||
|
m_EndIndex = endIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int m_StartIndex, m_EndIndex;
|
||||||
|
private RawTasFrame[]? m_FramesBackup;
|
||||||
|
|
||||||
|
[MemberNotNullWhen(true, nameof(m_FramesBackup))]
|
||||||
|
public bool IsExecuted() {
|
||||||
|
return m_FramesBackup is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(ITasSequence seq) {
|
||||||
|
if (IsExecuted()) {
|
||||||
|
throw OperationUtils.ExecutionEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check index range
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_EndIndex, seq.GetCount());
|
||||||
|
ArgumentOutOfRangeException.ThrowIfLessThan(m_StartIndex, 0);
|
||||||
|
|
||||||
|
// Do backups
|
||||||
|
int backupIndex = 0;
|
||||||
|
var backups = new RawTasFrame[m_EndIndex - m_StartIndex + 1];
|
||||||
|
foreach (var frame in seq.BatchlyVisit(m_StartIndex, m_EndIndex)) {
|
||||||
|
frame.ToRawImplace(ref backups[backupIndex++]);
|
||||||
|
}
|
||||||
|
// Do remove
|
||||||
|
seq.Remove(m_StartIndex, m_EndIndex);
|
||||||
|
|
||||||
|
// Assign backups
|
||||||
|
m_FramesBackup = backups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Revoke(ITasSequence seq) {
|
||||||
|
if (!IsExecuted()) {
|
||||||
|
throw OperationUtils.RevokeEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index range is checked,
|
||||||
|
// so we directly restore backup.
|
||||||
|
// Build iterator first
|
||||||
|
var iter = m_FramesBackup.Select((frame) => TasFrame.FromRaw(frame));
|
||||||
|
var exactSizedIter = new ExactSizeEnumerableAdapter<TasFrame>(iter, m_EndIndex - m_StartIndex + 1);
|
||||||
|
// Insert at start index
|
||||||
|
seq.Insert(m_StartIndex, exactSizedIter);
|
||||||
|
|
||||||
|
// Clear backups
|
||||||
|
m_FramesBackup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Occupation() {
|
||||||
|
return (m_EndIndex - m_StartIndex) * OperationUtils.SIZEOF_FRAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AddFrameOperationKind {
|
||||||
|
Before, After
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AddFrameOperation : ITasRevocableOperation {
|
||||||
|
public AddFrameOperation(AddFrameOperationKind kind, int index, uint fps, int count) {
|
||||||
|
// Check argument
|
||||||
|
if (!FpsConverter.IsValidFps(fps)) {
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(fps));
|
||||||
|
}
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(count);
|
||||||
|
// Assign argument
|
||||||
|
m_Kind = kind;
|
||||||
|
m_Index = index;
|
||||||
|
m_Fps = fps;
|
||||||
|
m_Count = count;
|
||||||
|
m_IsExecuted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AddFrameOperationKind m_Kind;
|
||||||
|
private int m_Index;
|
||||||
|
private uint m_Fps;
|
||||||
|
private int m_Count;
|
||||||
|
private bool m_IsExecuted;
|
||||||
|
|
||||||
|
public bool IsExecuted() {
|
||||||
|
return m_IsExecuted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(ITasSequence seq) {
|
||||||
|
if (IsExecuted()) {
|
||||||
|
throw OperationUtils.ExecutionEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check arguments
|
||||||
|
// If we add before some frame, the valid index can be [0, count],
|
||||||
|
// however, if we add after some frame, the valid index is [0, count),
|
||||||
|
switch (m_Kind) {
|
||||||
|
case AddFrameOperationKind.Before:
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(m_Index, seq.GetCount());
|
||||||
|
break;
|
||||||
|
case AddFrameOperationKind.After:
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_Index, seq.GetCount());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnreachableException("Unknown AddFrameOperationKind");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if count is zero.
|
||||||
|
if (m_Count != 0) {
|
||||||
|
// Prepare data builder.
|
||||||
|
var iter = Enumerable.Range(0, m_Count).Select((_) => TasFrame.FromFps(m_Fps));
|
||||||
|
var exactSizedIter = new ExactSizeEnumerableAdapter<TasFrame>(iter, m_Count);
|
||||||
|
// Compute the insert index
|
||||||
|
var index = m_Kind switch {
|
||||||
|
AddFrameOperationKind.Before => m_Index,
|
||||||
|
AddFrameOperationKind.After => m_Index + 1,
|
||||||
|
_ => throw new UnreachableException("Unknown AddFrameOperationKind"),
|
||||||
|
};
|
||||||
|
seq.Insert(index, exactSizedIter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set status
|
||||||
|
m_IsExecuted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Revoke(ITasSequence seq) {
|
||||||
|
if (!IsExecuted()) {
|
||||||
|
throw OperationUtils.RevokeEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arguments were checked so we directly resotre them.
|
||||||
|
// If we inserted count is not zero, remove inserted frames, otherwise do nothing.
|
||||||
|
if (m_Count != 0) {
|
||||||
|
// Compute the index for removing
|
||||||
|
var index = m_Kind switch {
|
||||||
|
AddFrameOperationKind.Before => m_Index,
|
||||||
|
AddFrameOperationKind.After => m_Index + 1,
|
||||||
|
_ => throw new UnreachableException("Unknown AddFrameOperationKind"),
|
||||||
|
};
|
||||||
|
// Execute removing.
|
||||||
|
seq.Remove(index, index + m_Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify execution status
|
||||||
|
m_IsExecuted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Occupation() {
|
||||||
|
return OperationUtils.SIZEOF_FRAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum InsertFrameOperationKind {
|
||||||
|
Before, After
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InsertFrameOperation : ITasRevocableOperation {
|
||||||
|
public InsertFrameOperation(InsertFrameOperationKind kind, int index, IExactSizeEnumerable<TasFrame> frames) {
|
||||||
|
m_Kind = kind;
|
||||||
|
m_Index = index;
|
||||||
|
m_InsertedFrames = frames.Select((frame) => frame.ToRaw()).ToArray();
|
||||||
|
m_IsExecuted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private InsertFrameOperationKind m_Kind;
|
||||||
|
private int m_Index;
|
||||||
|
private RawTasFrame[] m_InsertedFrames;
|
||||||
|
private bool m_IsExecuted;
|
||||||
|
|
||||||
|
public bool IsExecuted() {
|
||||||
|
return m_IsExecuted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(ITasSequence seq) {
|
||||||
|
if (IsExecuted()) {
|
||||||
|
throw OperationUtils.ExecutionEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check arguments
|
||||||
|
// If we insert before some frame, the valid index can be [0, count],
|
||||||
|
// however, if we insert after some frame, the valid index is [0, count),
|
||||||
|
switch (m_Kind) {
|
||||||
|
case InsertFrameOperationKind.Before:
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(m_Index, seq.GetCount());
|
||||||
|
break;
|
||||||
|
case InsertFrameOperationKind.After:
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_Index, seq.GetCount());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnreachableException("Unknown InsertFrameOperationKind");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if count is zero
|
||||||
|
var count = m_InsertedFrames.Length;
|
||||||
|
if (count != 0) {
|
||||||
|
// Prepare iterator
|
||||||
|
var iter = m_InsertedFrames.Select((frame) => TasFrame.FromRaw(frame));
|
||||||
|
var exactSizedIter = new ExactSizeEnumerableAdapter<TasFrame>(iter, count);
|
||||||
|
// Compute the insert index
|
||||||
|
var index = m_Kind switch {
|
||||||
|
InsertFrameOperationKind.Before => m_Index,
|
||||||
|
InsertFrameOperationKind.After => m_Index + 1,
|
||||||
|
_ => throw new UnreachableException("Unknown InsertFrameOperationKind"),
|
||||||
|
};
|
||||||
|
// Execute inserting.
|
||||||
|
seq.Insert(index, exactSizedIter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set execution status
|
||||||
|
m_IsExecuted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Revoke(ITasSequence seq) {
|
||||||
|
if (!IsExecuted()) {
|
||||||
|
throw OperationUtils.RevokeEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arguments were checked so we directly restore them.
|
||||||
|
var count = m_InsertedFrames.Length;
|
||||||
|
if (count != 0) {
|
||||||
|
// Compute the index for removing
|
||||||
|
var index = m_Kind switch {
|
||||||
|
InsertFrameOperationKind.Before => m_Index,
|
||||||
|
InsertFrameOperationKind.After => m_Index + 1,
|
||||||
|
_ => throw new UnreachableException("Unknown InsertFrameOperationKind"),
|
||||||
|
};
|
||||||
|
// Execute removing.
|
||||||
|
seq.Remove(index, index + count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify execution status
|
||||||
|
m_IsExecuted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Occupation() {
|
||||||
|
return m_InsertedFrames.Length * OperationUtils.SIZEOF_FRAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClearKeysOperation : ITasOperation {
|
||||||
|
public ClearKeysOperation() {
|
||||||
|
m_IsExecuted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool m_IsExecuted;
|
||||||
|
|
||||||
|
public void Execute(ITasSequence seq) {
|
||||||
|
// Check execution status first.
|
||||||
|
if (IsExecuted()) {
|
||||||
|
throw OperationUtils.ExecutionEnvironment;
|
||||||
|
}
|
||||||
|
// Execute operation
|
||||||
|
foreach (var frame in seq) {
|
||||||
|
frame.ClearKeyPressed();
|
||||||
|
}
|
||||||
|
m_IsExecuted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsExecuted() {
|
||||||
|
return m_IsExecuted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UniformFpsOperation : ITasOperation {
|
||||||
|
public UniformFpsOperation(uint fps) {
|
||||||
|
// Check arguments
|
||||||
|
if (!FpsConverter.IsValidFps(fps)) {
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(fps));
|
||||||
|
}
|
||||||
|
// Assign arguments
|
||||||
|
m_DeltaTime = FpsConverter.ToDelta(fps);
|
||||||
|
m_IsExecuted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float m_DeltaTime;
|
||||||
|
private bool m_IsExecuted;
|
||||||
|
|
||||||
|
public void Execute(ITasSequence seq) {
|
||||||
|
// Check execution status first.
|
||||||
|
if (IsExecuted()) {
|
||||||
|
throw OperationUtils.ExecutionEnvironment;
|
||||||
|
}
|
||||||
|
// Execute operation
|
||||||
|
foreach (var frame in seq) {
|
||||||
|
frame.SetTimeDelta(m_DeltaTime);
|
||||||
|
}
|
||||||
|
m_IsExecuted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsExecuted() {
|
||||||
|
return m_IsExecuted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
425
BallanceTasEditor/BallanceTasEditor/Backend/TasSequence.cs
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BallanceTasEditor.Backend {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所有用于在内存中存储TAS帧的结构都必须实现此interface。
|
||||||
|
/// </summary>
|
||||||
|
public interface ITasSequence : IEnumerable<TasFrame> {
|
||||||
|
/// <summary>
|
||||||
|
/// 访问给定索引的帧的值。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 实现此函数时需要格外注意以下事项:
|
||||||
|
/// <para/>
|
||||||
|
/// 该函数应当保证在访问临近项时有较高的效率。
|
||||||
|
/// <para/>
|
||||||
|
/// 该函数理论上的复杂度应为O(1)。
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="index">要访问的单元的索引。</param>
|
||||||
|
/// <returns>被访问的单元。</returns>
|
||||||
|
/// <exception cref="IndexOutOfRangeException">给定的索引无效。</exception>
|
||||||
|
TasFrame Visit(int index);
|
||||||
|
/// <summary>
|
||||||
|
/// 按顺序访问给定索引区间内的帧的值。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 实现此函数时需要格外注意以下事项:
|
||||||
|
/// <para/>
|
||||||
|
/// 该函数如果可以进行顺序访问优化,则应当优化。
|
||||||
|
/// 即使用此函数可以获得等于或大于单独一次使用<see cref="Visit(int)"/>函数。
|
||||||
|
/// <para/>
|
||||||
|
/// 该函数理论上的复杂度应为O(1)。
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="startIndex">要访问的帧区间的起始索引(包含)。</param>
|
||||||
|
/// <param name="endIndex">要访问的帧区间的终止索引(包含)</param>
|
||||||
|
/// <exception cref="IndexOutOfRangeException">给定的索引无效。</exception>
|
||||||
|
IExactSizeEnumerable<TasFrame> BatchlyVisit(int startIndex, int endIndex);
|
||||||
|
/// <summary>
|
||||||
|
/// 在给定的帧索引<b>之前</b>插入给定的项目。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 实现此函数时需要格外注意以下事项:
|
||||||
|
/// <para/>
|
||||||
|
/// 按照函数约定,如果要在头部插入数据,则可以通过指定0来实现。
|
||||||
|
/// 然而对于在尾部插入数据,或在空的存储中插入数据,可以指定存储结构的长度来实现。
|
||||||
|
/// 即指定<c>(最大Index + 1)</c>作为参数来实现。
|
||||||
|
/// <para/>
|
||||||
|
/// 该函数理论上的复杂度应为O(1)。
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="index">要在前方插入数据的元素的索引。</param>
|
||||||
|
/// <param name="items">要插入的元素的迭代器。</param>
|
||||||
|
/// <exception cref="IndexOutOfRangeException">给定的索引无效。</exception>
|
||||||
|
void Insert(int index, IExactSizeEnumerable<TasFrame> items);
|
||||||
|
/// <summary>
|
||||||
|
/// 从序列中移出给定帧区间的元素。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 实现此函数时需要格外注意以下事项:
|
||||||
|
/// <para/>
|
||||||
|
/// 该函数理论上的复杂度应为O(1)。
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="startIndex">要移除的帧区间的起始索引(包含)。</param>
|
||||||
|
/// <param name="endIndex">要移除的帧区间的终止索引(包含)</param>
|
||||||
|
/// <exception cref="IndexOutOfRangeException">给定的索引无效。</exception>
|
||||||
|
void Remove(int startIndex, int endIndex);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空存储结构。
|
||||||
|
/// </summary>
|
||||||
|
void Clear();
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前存储的TAS帧的个数。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>存储的TAS帧的个数。</returns>
|
||||||
|
int GetCount();
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前存储结构是不是空的。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>如果是空的就返回true,否则返回false。</returns>
|
||||||
|
bool IsEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// We may introduce ITasSequenceSlice to have iterator on a specific range.
|
||||||
|
// We also need introduce a new function in ITasSequence to fetch this instance.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基于Gap Buffer思想的TAS存储器。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 其实就是把List的InsertRange的复杂度从O(n*m)修正为O(n)。
|
||||||
|
/// </remarks>
|
||||||
|
public class GapBufferSequence : ITasSequence, IEnumerable<TasFrame> {
|
||||||
|
public GapBufferSequence() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
public TasFrame Visit(int index) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IExactSizeEnumerable<TasFrame> BatchlyVisit(int startIndex, int endIndex) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Insert(int index, IExactSizeEnumerable<TasFrame> items) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(int startIndex, int endIndex) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetCount() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEmpty() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<TasFrame> GetEnumerator() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() {
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基于简单的List的TAS存储器。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 由于List的InsertRange的复杂度是O(n*m),可能不符合要求。
|
||||||
|
/// </remarks>
|
||||||
|
public class ListTasSequence : ITasSequence, IEnumerable<TasFrame> {
|
||||||
|
public ListTasSequence() {
|
||||||
|
m_Container = new List<TasFrame>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TasFrame> m_Container;
|
||||||
|
|
||||||
|
public TasFrame Visit(int index) {
|
||||||
|
if (index >= m_Container.Count || index < 0) {
|
||||||
|
throw new IndexOutOfRangeException("Invalid index for frame.");
|
||||||
|
} else {
|
||||||
|
return m_Container[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<TasFrame> BatchlyVisitEx(int startIndex, int endIndex) {
|
||||||
|
if (endIndex < startIndex || startIndex < 0 || endIndex >= m_Container.Count) {
|
||||||
|
throw new IndexOutOfRangeException("Invalid index for frame.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate items one by one.
|
||||||
|
for (int i = startIndex; i <= endIndex; ++i) {
|
||||||
|
yield return m_Container[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IExactSizeEnumerable<TasFrame> BatchlyVisit(int startIndex, int endIndex) {
|
||||||
|
return new ExactSizeEnumerableAdapter<TasFrame>(BatchlyVisitEx(startIndex, endIndex), endIndex - startIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Insert(int index, IExactSizeEnumerable<TasFrame> items) {
|
||||||
|
if (index == m_Container.Count) {
|
||||||
|
m_Container.AddRange(items);
|
||||||
|
} else {
|
||||||
|
if (index > m_Container.Count || index < 0) {
|
||||||
|
throw new IndexOutOfRangeException("Invalid index for frame.");
|
||||||
|
} else {
|
||||||
|
m_Container.InsertRange(index, items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(int startIndex, int endIndex) {
|
||||||
|
if (endIndex < startIndex || startIndex < 0 || endIndex >= m_Container.Count) {
|
||||||
|
throw new IndexOutOfRangeException("Invalid index for frame.");
|
||||||
|
} else {
|
||||||
|
m_Container.RemoveRange(startIndex, endIndex - startIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() {
|
||||||
|
m_Container.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetCount() {
|
||||||
|
return m_Container.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEmpty() {
|
||||||
|
return GetCount() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<TasFrame> GetEnumerator() {
|
||||||
|
return m_Container.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() {
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 传统的基于LinkedList的TAS存储器。
|
||||||
|
/// </summary>
|
||||||
|
public class LegacyTasSequence : ITasSequence, IEnumerable<TasFrame> {
|
||||||
|
public LegacyTasSequence() {
|
||||||
|
m_Container = new LinkedList<TasFrame>();
|
||||||
|
m_Cursor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LinkedListCursor<T> {
|
||||||
|
public LinkedListCursor(LinkedListNode<T> node, int index) {
|
||||||
|
this.Node = node;
|
||||||
|
this.Index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinkedListNode<T> Node;
|
||||||
|
public int Index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinkedList<TasFrame> m_Container;
|
||||||
|
private LinkedListCursor<TasFrame>? m_Cursor;
|
||||||
|
|
||||||
|
private enum NodeSeekOrigin {
|
||||||
|
Head,
|
||||||
|
Cursor,
|
||||||
|
Tail,
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct NodeSeekInfo : IComparable<NodeSeekInfo> {
|
||||||
|
public required NodeSeekOrigin Origin;
|
||||||
|
public required int Offset;
|
||||||
|
|
||||||
|
public int CompareTo(NodeSeekInfo other) {
|
||||||
|
return Math.Abs(this.Offset).CompareTo(Math.Abs(other.Offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快速将内部游标移动到指定Index,并更新与之匹配的Index。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="desiredIndex"></param>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
[MemberNotNull(nameof(m_Cursor))]
|
||||||
|
private void MoveToIndex(int desiredIndex) {
|
||||||
|
// 检查基本环境
|
||||||
|
if (desiredIndex < 0 || desiredIndex >= GetCount())
|
||||||
|
throw new IndexOutOfRangeException("Invalid index for frame.");
|
||||||
|
if (m_Cursor is null || IsEmpty())
|
||||||
|
throw new InvalidOperationException("Can not move cursor when container is empty.");
|
||||||
|
|
||||||
|
// 创建三个候选方案。
|
||||||
|
var candidates = new NodeSeekInfo[3] {
|
||||||
|
new NodeSeekInfo() { Origin = NodeSeekOrigin.Head, Offset = desiredIndex },
|
||||||
|
new NodeSeekInfo() { Origin = NodeSeekOrigin.Tail, Offset = desiredIndex - (GetCount() - 1) },
|
||||||
|
new NodeSeekInfo() { Origin = NodeSeekOrigin.Cursor, Offset = desiredIndex - m_Cursor.Index },
|
||||||
|
};
|
||||||
|
// 确定哪个候选方案最短。
|
||||||
|
var bestCandidate = candidates.Min();
|
||||||
|
// 用最短候选方案移动。
|
||||||
|
int pickedOffset = bestCandidate.Offset;
|
||||||
|
LinkedListNode<TasFrame> pickedNode = bestCandidate.Origin switch {
|
||||||
|
NodeSeekOrigin.Head => m_Container.First.Unwrap(),
|
||||||
|
NodeSeekOrigin.Cursor => m_Cursor.Node,
|
||||||
|
NodeSeekOrigin.Tail => m_Container.Last.Unwrap(),
|
||||||
|
_ => throw new UnreachableException("Unknown NodeSeekOrigin"),
|
||||||
|
};
|
||||||
|
int alreadyMoved = 0;
|
||||||
|
if (pickedOffset < 0) {
|
||||||
|
while (alreadyMoved != pickedOffset) {
|
||||||
|
pickedNode = pickedNode.Previous.Unwrap();
|
||||||
|
alreadyMoved--;
|
||||||
|
}
|
||||||
|
} else if (pickedOffset > 0) {
|
||||||
|
while (alreadyMoved != pickedOffset) {
|
||||||
|
pickedNode = pickedNode.Next.Unwrap();
|
||||||
|
alreadyMoved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置Cursor
|
||||||
|
m_Cursor = new LinkedListCursor<TasFrame>(pickedNode, desiredIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TasFrame Visit(int index) {
|
||||||
|
if (index >= m_Container.Count || index < 0) {
|
||||||
|
throw new IndexOutOfRangeException("Invalid index for frame.");
|
||||||
|
} else {
|
||||||
|
MoveToIndex(index);
|
||||||
|
return m_Cursor.Node.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<TasFrame> BatchlyVisitEx(int startIndex, int endIndex) {
|
||||||
|
if (endIndex < startIndex || startIndex < 0 || endIndex >= m_Container.Count) {
|
||||||
|
throw new IndexOutOfRangeException("Invalid index for frame.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// We move to start index first.
|
||||||
|
MoveToIndex(startIndex);
|
||||||
|
// Then we copy its reference
|
||||||
|
LinkedListNode<TasFrame>? node = m_Cursor.Node;
|
||||||
|
// Then compute count
|
||||||
|
var count = endIndex - startIndex + 1;
|
||||||
|
// Now we can iterate items one by one.
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
node = node.Unwrap();
|
||||||
|
yield return node.Unwrap().Value;
|
||||||
|
node = node.Next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IExactSizeEnumerable<TasFrame> BatchlyVisit(int startIndex, int endIndex) {
|
||||||
|
return new ExactSizeEnumerableAdapter<TasFrame>(BatchlyVisitEx(startIndex, endIndex), endIndex - startIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Insert(int index, IExactSizeEnumerable<TasFrame> items) {
|
||||||
|
// YYC MARK:
|
||||||
|
// We must test the equal first, to handle back appending properly.
|
||||||
|
if (index == m_Container.Count) {
|
||||||
|
foreach (TasFrame item in items) {
|
||||||
|
m_Container.AddLast(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pendingCursor = m_Container.First;
|
||||||
|
if (pendingCursor is null) {
|
||||||
|
m_Cursor = null;
|
||||||
|
} else {
|
||||||
|
m_Cursor = new LinkedListCursor<TasFrame>(pendingCursor, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index >= m_Container.Count || index < 0) {
|
||||||
|
throw new IndexOutOfRangeException("Invalid index for frame.");
|
||||||
|
} else {
|
||||||
|
MoveToIndex(index);
|
||||||
|
|
||||||
|
foreach (TasFrame item in items) {
|
||||||
|
m_Container.AddBefore(m_Cursor.Node, item);
|
||||||
|
}
|
||||||
|
m_Cursor.Index += items.GetCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(int startIndex, int endIndex) {
|
||||||
|
if (endIndex < startIndex || startIndex < 0 || endIndex >= m_Container.Count) {
|
||||||
|
throw new IndexOutOfRangeException("Invalid index for frame.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute count and move to index.
|
||||||
|
var count = endIndex - startIndex + 1;
|
||||||
|
MoveToIndex(startIndex);
|
||||||
|
|
||||||
|
// 我们总是获取要删除的项目的前一项来作为参照。
|
||||||
|
// 如果获取到的是null,则说明是正在删第一项,从m_Container里获取First来删除就行,
|
||||||
|
// 否则就继续用这个Node的Next来删除。
|
||||||
|
var prevNode = m_Cursor.Node.Previous;
|
||||||
|
if (prevNode is null) {
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
m_Container.RemoveFirst();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
m_Container.Remove(prevNode.Next.Unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后设置Cursor和Index
|
||||||
|
if (IsEmpty()) {
|
||||||
|
// 如果全部删完了,就清除这两个的设置。
|
||||||
|
m_Cursor = null;
|
||||||
|
} else {
|
||||||
|
if (prevNode is null) {
|
||||||
|
// 如果是按头部删除的,则直接获取头部及其Index。
|
||||||
|
m_Cursor = new LinkedListCursor<TasFrame>(m_Container.First.Unwrap(), 0);
|
||||||
|
} else {
|
||||||
|
// 否则就以prevNode为当前Cursor,Index--为对应Index。
|
||||||
|
m_Cursor.Node = prevNode;
|
||||||
|
--m_Cursor.Index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() {
|
||||||
|
m_Container.Clear();
|
||||||
|
m_Cursor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetCount() {
|
||||||
|
return m_Container.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEmpty() {
|
||||||
|
return GetCount() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<TasFrame> GetEnumerator() {
|
||||||
|
return m_Container.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() {
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
177
BallanceTasEditor/BallanceTasEditor/Backend/TasStorage.cs
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
using CommunityToolkit.HighPerformance;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BallanceTasEditor.Backend {
|
||||||
|
public static class TasStorage {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize given TAS sequence with given count frame which has given FPS.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seq">The TAS sequence to initialize.</param>
|
||||||
|
/// <param name="count">The count of frame.</param>
|
||||||
|
/// <param name="fps">The FPS of frame.</param>
|
||||||
|
public static void Init(ITasSequence seq, int count, uint fps) {
|
||||||
|
var frame = TasFrame.FromFps(fps);
|
||||||
|
var iter = Enumerable.Range(0, count).Select((_) => frame.Clone());
|
||||||
|
var exactSizeIter = new ExactSizeEnumerableAdapter<TasFrame>(iter, count);
|
||||||
|
seq.Insert(seq.GetCount(), exactSizeIter);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal const int SIZEOF_I32 = sizeof(int);
|
||||||
|
internal const int SIZEOF_F32 = sizeof(float);
|
||||||
|
internal const int SIZEOF_U32 = sizeof(uint);
|
||||||
|
internal const int SIZEOF_RAW_TAS_FRAME = SIZEOF_F32 + SIZEOF_U32;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save given TAS sequence into given file path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filepath">The path to file for saving.</param>
|
||||||
|
/// <param name="seq">The TAS sequence to save.</param>
|
||||||
|
/// <exception cref="Exception">Any exception occurs when saving.</exception>
|
||||||
|
public static void Save(string filepath, ITasSequence seq) {
|
||||||
|
using (var fs = new FileStream(filepath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None)) {
|
||||||
|
Save(fs, seq);
|
||||||
|
fs.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save given TAS sequence into given file stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fs">The file stream for saving.</param>
|
||||||
|
/// <param name="seq">The TAS sequence to save.</param>
|
||||||
|
/// <exception cref="Exception">Any exception occurs when saving.</exception>
|
||||||
|
public static void Save(Stream fs, ITasSequence seq) {
|
||||||
|
var totalByte = seq.GetCount() * SIZEOF_RAW_TAS_FRAME;
|
||||||
|
fs.Write(BitConverter.GetBytes(totalByte), 0, SIZEOF_I32);
|
||||||
|
|
||||||
|
using (var zo = new Ionic.Zlib.ZlibStream(fs, Ionic.Zlib.CompressionMode.Compress, Ionic.Zlib.CompressionLevel.Level9, true)) {
|
||||||
|
foreach (var item in seq) {
|
||||||
|
var rawItem = item.ToRaw();
|
||||||
|
zo.Write(BitConverter.GetBytes(rawItem.TimeDelta), 0, SIZEOF_F32);
|
||||||
|
zo.Write(BitConverter.GetBytes(rawItem.KeyFlags), 0, SIZEOF_U32);
|
||||||
|
}
|
||||||
|
zo.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
//var zo = new zlib.ZOutputStream(file, 9);
|
||||||
|
//var node = mem.First;
|
||||||
|
//while (node != null) {
|
||||||
|
// zo.Write(BitConverter.GetBytes(node.Value.deltaTime), 0, 4);
|
||||||
|
// zo.Write(BitConverter.GetBytes(node.Value.keystates), 0, 4);
|
||||||
|
// node = node.Next;
|
||||||
|
//}
|
||||||
|
//zo.finish();
|
||||||
|
//zo.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load TAS sequence from given file path into given sequence.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filepath">The path to file for loading.</param>
|
||||||
|
/// <param name="seq">The TAS sequence to load.</param>
|
||||||
|
/// <exception cref="Exception">Any exception occurs when loading.</exception>
|
||||||
|
public static void Load(string filepath, ITasSequence seq) {
|
||||||
|
using (var fs = new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.Read)) {
|
||||||
|
Load(fs, seq);
|
||||||
|
fs.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load TAS sequence from given file stream into given sequence.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fs">The file stream for loading.</param>
|
||||||
|
/// <param name="seq">The TAS sequence to load.</param>
|
||||||
|
/// <exception cref="Exception">Any exception occurs when loading.</exception>
|
||||||
|
public static void Load(Stream fs, ITasSequence seq) {
|
||||||
|
// Read total bytes
|
||||||
|
var lenCache = new byte[SIZEOF_I32];
|
||||||
|
fs.Read(lenCache, 0, 4);
|
||||||
|
int expectedLength = BitConverter.ToInt32(lenCache, 0);
|
||||||
|
// Check length and compute count
|
||||||
|
int expectedCount = Math.DivRem(expectedLength, SIZEOF_RAW_TAS_FRAME, out var remainder);
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNotEqual(remainder, 0);
|
||||||
|
|
||||||
|
using (var mem = new MemoryStream()) {
|
||||||
|
using (var zo = new Ionic.Zlib.ZlibStream(mem, Ionic.Zlib.CompressionMode.Decompress, true)) {
|
||||||
|
CopyStream(fs, zo);
|
||||||
|
zo.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
var memWrapper = new EnumerableMemoryStream(mem, expectedCount);
|
||||||
|
seq.Clear();
|
||||||
|
seq.Insert(0, memWrapper);
|
||||||
|
|
||||||
|
mem.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
//mem.Seek(0, SeekOrigin.Begin);
|
||||||
|
//for (long i = 0; i < expectedCount; i++) {
|
||||||
|
// ls.AddLast(new FrameData(mem));
|
||||||
|
//}
|
||||||
|
//mem.Close();
|
||||||
|
//zo.Close();
|
||||||
|
|
||||||
|
//var zo = new zlib.ZOutputStream(mem);
|
||||||
|
//CopyStream(file, zo);
|
||||||
|
//zo.finish();
|
||||||
|
|
||||||
|
//mem.Seek(0, SeekOrigin.Begin);
|
||||||
|
//for (long i = 0; i < expectedCount; i++) {
|
||||||
|
// ls.AddLast(new FrameData(mem));
|
||||||
|
//}
|
||||||
|
//mem.Close();
|
||||||
|
//zo.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int STREAM_COPY_CHUNK_SIZE = 10240;
|
||||||
|
|
||||||
|
private static void CopyStream(Stream origin, Stream target) {
|
||||||
|
var buffer = new byte[STREAM_COPY_CHUNK_SIZE];
|
||||||
|
int len;
|
||||||
|
while ((len = origin.Read(buffer, 0, STREAM_COPY_CHUNK_SIZE)) > 0) {
|
||||||
|
target.Write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
//target.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class EnumerableMemoryStream : IExactSizeEnumerable<TasFrame> {
|
||||||
|
public EnumerableMemoryStream(MemoryStream mem, int frameCnt) {
|
||||||
|
m_MemoryStream = mem;
|
||||||
|
m_FrameCount = frameCnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MemoryStream m_MemoryStream;
|
||||||
|
private int m_FrameCount;
|
||||||
|
|
||||||
|
public IEnumerator<TasFrame> GetEnumerator() {
|
||||||
|
// Get the view of underlying array
|
||||||
|
var memory = m_MemoryStream.GetBuffer().AsMemory();
|
||||||
|
// Get the span which actually storing the data,
|
||||||
|
// because the length of buffer is equal or longer than the length of all stored data.
|
||||||
|
var exactMemory = memory.Slice(0, m_FrameCount * SIZEOF_RAW_TAS_FRAME);
|
||||||
|
// Convert to raw frame type.
|
||||||
|
var frameMemory = exactMemory.Cast<byte, RawTasFrame>();
|
||||||
|
// Map it and return.
|
||||||
|
return MemoryMarshal.ToEnumerable<RawTasFrame>(frameMemory).Select((rawFrame) => TasFrame.FromRaw(rawFrame)).GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() {
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetCount() {
|
||||||
|
return m_FrameCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
27
BallanceTasEditor/BallanceTasEditor/BallanceTasEditor.csproj
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<ApplicationIcon>Frontend\Assets\App.ico</ApplicationIcon>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Resource Include="Frontend\Assets\*.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.0" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
||||||
|
<PackageReference Include="DotNetZip" Version="1.9.1.8" />
|
||||||
|
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Frontend\Models\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
BIN
BallanceTasEditor/BallanceTasEditor/Frontend/Assets/Cancel.ico
Normal file
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 102 KiB |
BIN
BallanceTasEditor/BallanceTasEditor/Frontend/Assets/Count.ico
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
BallanceTasEditor/BallanceTasEditor/Frontend/Assets/CutFrame.ico
Normal file
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 109 KiB |
BIN
BallanceTasEditor/BallanceTasEditor/Frontend/Assets/FlipCell.ico
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
BallanceTasEditor/BallanceTasEditor/Frontend/Assets/Fps.ico
Normal file
|
After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 105 KiB |
BIN
BallanceTasEditor/BallanceTasEditor/Frontend/Assets/NewFile.ico
Normal file
|
After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 105 KiB |
BIN
BallanceTasEditor/BallanceTasEditor/Frontend/Assets/OpenFile.ico
Normal file
|
After Width: | Height: | Size: 107 KiB |