1
0

38 Commits

Author SHA1 Message Date
a9fab50ada feat: support view model closing view window 2026-04-05 18:07:15 +08:00
a55a8c7456 feat: write shit 2026-04-02 20:35:30 +08:00
08734c6ef7 feat: introduce batchly visit in tas sequence 2026-04-01 13:33:30 +08:00
5f10338d33 test: finish tas op test 2026-04-01 13:16:05 +08:00
c74e22bff0 test: add test for tas oper and fix uniform fps op issue 2026-03-31 16:52:22 +08:00
b8184c6ab4 test: add test for tas oper and fix issue 2026-03-31 14:26:17 +08:00
d0174bbf86 fix: fix build issue and tas seq refactor issue 2026-03-30 10:58:26 +08:00
530dc2a76e feat: add before and after support for tas operation 2026-03-30 10:40:50 +08:00
eeb6f1802c feat: update tas oper oocupation calc 2026-03-29 10:44:04 +08:00
d8f8536b8b feat: fix nullable warning 2026-03-29 10:39:59 +08:00
b73a035311 feat: finish tas operation 2026-03-29 10:33:13 +08:00
97458d893e feat: write shit operator 2026-03-29 10:06:10 +08:00
43c24c63c7 feat: write some tas oper 2026-03-29 09:21:21 +08:00
7f4d511715 feat: update some interface 2026-03-27 16:11:11 +08:00
ba53ad1da4 refactor: refactor project 2026-03-25 10:33:05 +08:00
6c07355601 refactor: refactor editor 2026-03-11 13:56:49 +08:00
802c258a99 fix: fix shit 2026-01-20 09:24:03 +08:00
8f1b7cc196 fix: fix what a shit 2026-01-19 22:43:58 +08:00
b16c7508f0 doc: update doc style for sonnet 2026-01-19 19:21:08 +08:00
590645b13c feat: add pyi generation for pyi in sonnet 2026-01-19 16:14:52 +08:00
84897a409b feat: add fps module in sonnet 2026-01-19 11:14:13 +08:00
5323617ca6 chore: change project layout 2026-01-19 10:44:10 +08:00
0e9837a75a doc: add docstring for sonnet 2026-01-19 10:27:53 +08:00
87e6c63aae feat: add build notes and fix build issue for sonnet 2026-01-19 10:14:53 +08:00
85fc2ad3ce feat: remove fps support from rust to python 2026-01-19 09:34:16 +08:00
06482e2218 feat: finish sonnet expose interface 2026-01-18 22:50:47 +08:00
c42305c8d2 feat: finish sonnet tas file rw functions 2026-01-18 20:39:47 +08:00
49940b43d5 write some sonnet code 2026-01-18 14:56:37 +08:00
941e59e471 chore: adjust directory layout for new added planned peoject 2026-01-18 11:07:52 +08:00
ab5a68bed7 feat: commit content which I don't know when I create them 2026-01-18 10:43:30 +08:00
4aaf64eae5 feat: add tas operation interface 2025-11-19 13:19:07 +08:00
eb40906975 fix: update some words and styles 2025-11-18 22:34:01 +08:00
ee6a565ce0 feat: add clear keys icon 2025-11-18 22:05:05 +08:00
334accd070 feat: use new IEnumeratable type 2025-11-18 21:56:50 +08:00
9d41119710 feat: add icon for editor layout 2025-11-18 21:12:33 +08:00
86ea296a1b refactor: migrate project to .net core 8 to resolve the issue that we can not use CommunityToolkit.Mvvm 2025-11-18 20:53:42 +08:00
a1b1fcbf7b feat: got stuck with bug of Community.Mvvm generating codes 2025-11-18 14:50:17 +08:00
02118f4c0a feat: add all essential dialogs 2025-11-17 13:22:00 +08:00
244 changed files with 7221 additions and 2050 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View 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

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View 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

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
<Solution>
<Project Path="BallanceTasEditor/BallanceTasEditor.csproj" />
<Project Path="BallanceTasEditorTests/BallanceTasEditorTests.csproj" />
</Solution>

View File

@@ -1,14 +1,14 @@
<Application x:Class="BallanceTasEditor.App"
<Application x:Class="BallanceTasEditor.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BallanceTasEditor"
StartupUri="Views/MainWindow.xaml">
StartupUri="Frontend/Views/MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Styles/AccessoryIconControl.xaml"/>
<ResourceDictionary Source="/Styles/NoteBanner.xaml"/>
<ResourceDictionary Source="/Styles/GenericButton.xaml"/>
<ResourceDictionary Source="/Frontend/Styles/AccessoryIconControl.xaml"/>
<ResourceDictionary Source="/Frontend/Styles/NoteBanner.xaml"/>
<ResourceDictionary Source="/Frontend/Styles/GenericButton.xaml"/>
<ResourceDictionary>
</ResourceDictionary>

View File

@@ -1,9 +1,5 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace BallanceTasEditor {

View 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)
)]

View 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; }
}
}

View File

@@ -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;
}
}
}

View 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();
}
}
}

View 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;
}
}
}

View 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;
}
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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为当前CursorIndex--为对应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();
}
}
}

View 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;
}
}
}
}

View 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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Some files were not shown because too many files have changed in this diff Show More