Compare commits

...

210 Commits

Author SHA1 Message Date
Valentin
d132bc46e2
Merge pull request #510 from 4sval/dev
Dev
2024-10-23 09:14:58 +02:00
Asval
c6c8cf201e FModel v4.4.4
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-10-20 21:11:17 +02:00
GMatrixGames
f266cd4c34
Bump for build
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-10-14 18:55:07 -04:00
Marlon
18ed85b17b
netease & gameforpeace fixes 2024-10-14 21:55:42 +02:00
LongerWarrior
d1ca498643 CommitControl update
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-10-14 19:05:23 +03:00
Marlon
07d26f7235
nuget updates
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-10-12 10:07:01 +02:00
Asval
0f459624c0 bump
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-10-12 01:15:55 +02:00
Asval
45996b730e my dumbass
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-10-11 18:31:05 +02:00
Asval
905ab7cf98 actually removed update modes + uemodel texture export 2024-10-11 18:20:35 +02:00
LongerWarrior
3c614775c9 bump
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-10-11 00:10:23 +03:00
LongerWarrior
65118dbc72 bump
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-10-07 23:16:05 +03:00
Valentin
7fda6bc3b8
Merge pull request #503 from 4sval/no-update-modes
Some checks failed
FModel QA Builder / build (push) Has been cancelled
No update modes
2024-10-06 16:58:36 +02:00
Asval
774525ed50 actually implementing dotnet features 2024-10-06 15:31:04 +02:00
Asval
784704b645 add past 20 commits releases just in case 2024-10-06 03:13:03 +02:00
Asval
0060635e66 done? 2024-10-06 02:37:04 +02:00
LongerWarrior
e668c53947 bump
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-10-04 01:32:57 +03:00
Asval
2d001ff21f more of the logic 2024-10-02 21:08:41 +02:00
Marlon
cd3a41d18c
make use of RandomAccess archives
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-09-30 21:16:44 +02:00
LongerWarrior
bde2ce54ed GlobalShaderCache and TonyHawkProSkater support
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-09-30 15:43:56 +03:00
Marlon
d5e7aba05c
Merge pull request #502 from Krowe-moh/patch-2
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-09-29 23:03:13 +02:00
Krowe Moh
54ced30f51
support for more types 2024-09-30 06:54:41 +10:00
Asval
1edfab594d bump
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-09-29 10:16:15 +02:00
Marlon
0df8501650
cue4p sync
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-09-29 02:43:06 +02:00
Marlon
09b694c33c
nuget updates 2024-09-29 02:42:28 +02:00
Asval
ddee7c1479 current/latest badges 2024-09-29 01:52:41 +02:00
Asval
fdf3deed76 poc 2024-09-28 20:19:15 +02:00
Asval
7e35306b01 history ui done 2024-09-28 20:10:08 +02:00
Asval
51575d1025 Merge remote-tracking branch 'origin/no-update-modes' into no-update-modes 2024-09-25 18:42:56 +02:00
Asval
6a527c3a2f fix 2024-09-25 18:42:09 +02:00
Asval
6d4c76f2a4 ctrl s 2024-09-25 18:38:01 +02:00
Asval
bbb495b485 versioned backups
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-09-25 18:32:23 +02:00
Asval
a54e0056c2 ctrl s 2024-09-22 20:23:59 +02:00
LongerWarrior
4b82eb37b0 MonsterJam+Rennsport+FunkoFusion support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-09-16 20:43:16 +03:00
Valentin
b536f10b14
Merge pull request #497 from GhostScissors/TGA
Some checks failed
FModel QA Builder / build (push) Has been cancelled
Texture export format based on user settings
2024-09-09 12:09:21 +02:00
GhostScissors
581db18c7b Update CUE4Parse and remove .png 2024-09-09 15:37:54 +05:30
GhostScissors
9c3eeaf9cc Remove GetBitmap since it is not being used anywhere 2024-09-09 15:28:00 +05:30
GhostScissors
4bce892c79 Texture export based on texture export settings
Asval is goofy
2024-09-09 15:24:01 +05:30
GhostScissors
3ebc245cf3 Revert 2024-09-09 13:33:34 +05:30
GhostScissors
1a62062289 Comment this out 2024-09-09 13:15:05 +05:30
GhostScissors
da62476639 Export textures as TGA 2024-09-09 13:13:30 +05:30
LongerWarrior
b6d67741e7 bump and improving MorphTarget data generation for Snooper
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-09-08 21:10:18 +03:00
Asval
aa41224d57 slipping this in
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-08-30 17:51:38 +02:00
LongerWarrior
0ae8386316 Updating MorphTarget 2024-08-30 18:48:01 +03:00
Asval
3df3e802d4 filterable ue combobox
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-08-28 17:31:02 +02:00
Marlon
fbbf3c9c57
load preview from datalist first
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-08-19 19:51:39 +02:00
Asval
693484f8e8 bump
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-08-18 12:51:12 +02:00
Asval
3af527770b dotless i?
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-08-15 21:56:09 +02:00
LongerWarrior
bf97c0002f Delta Force Hawk Ops support
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-08-15 01:08:13 +03:00
Asval
1983e6f8bd bump 2024-08-14 19:11:47 +02:00
GMatrixGames
148c2c7db9
Bump for MR 2
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-08-07 18:03:45 -04:00
GMatrixGames
f78903f1d2
Bump for MR 2024-08-07 16:19:53 -04:00
LongerWarrior
8d646b077b bump for VisionsofMana
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-08-04 00:49:43 +03:00
LongerWarrior
a2f2117602 bump for WildAssault
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-08-03 01:27:45 +03:00
Asval
fabc064a46 update ui
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-07-29 12:48:35 +02:00
Marlon
19c2eb9e78
sync cue4p
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-07-23 10:54:04 +02:00
GMatrixGames
5f9cda1016
Merge branch 'dev' of https://github.com/4sval/FModel into dev
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2024-07-22 11:03:51 -04:00
GMatrixGames
7a06ac967e
parse 2024-07-22 11:03:46 -04:00
Asval
5494c6b259 bump 2024-07-17 22:19:04 +02:00
Minshu
ac835497da little better game name detection ig 2024-07-09 00:07:26 +05:30
Asval
c6383a1d22 close #479 2024-07-02 20:08:58 +02:00
GMatrixGames
9c50e2b7d7
Up parse 2024-06-30 14:42:09 -04:00
Marlon
6132f120c2
sync cue4p 2024-06-14 01:43:04 +02:00
Asval
2cf21c69ba fixed series border color 2024-06-13 22:33:56 +02:00
GMatrixGames
3e200bd5d0
Fix some issues 2024-06-13 13:57:48 -04:00
GMatrixGames
3aa1d60901
Merge branch 'dev' of https://github.com/4sval/FModel into dev 2024-06-13 13:35:34 -04:00
GMatrixGames
9774737e67
Fix icons on //Fortnite/Release-30.10 2024-06-13 13:35:22 -04:00
Asval
bd378594b1 fixed #476 2024-06-13 15:18:50 +02:00
Marlon
86ee2b98f3
updated EpicManifestParser 2024-06-11 02:11:51 +02:00
Marlon
1ccb2aa83b
nuget updates 2024-06-11 02:11:39 +02:00
Marlon
3fa30b5979
fix 2024-06-11 02:10:18 +02:00
Marlon
5d991e27b1
fix 2024-06-11 02:09:53 +02:00
GMatrixGames
631530dbf7
Outline Shader Drifting 2024-05-29 14:39:59 -04:00
Valentin
bcbda59523
Merge pull request #472 from 4sval/dev
Dev
2024-05-26 19:26:31 +02:00
Valentin
eb3f302552
Merge branch 'master' into dev 2024-05-26 19:25:23 +02:00
Asval
244462cc77 FModel v4.4.3.6 2024-05-26 19:18:42 +02:00
Asval
11bad8687e event triggered game directory 2024-05-25 19:51:43 +02:00
GMatrixGames
ed575429b1
Epic decided this was a good idea 2024-05-25 00:38:21 -04:00
Asval
e87e75acd4 removed atom model 2024-05-25 02:42:54 +02:00
Asval
9781446aef juno building preview 2024-05-25 00:19:47 +02:00
Asval
eb49b3c853 HighlightedCheckBox 2024-05-24 16:42:15 +02:00
GMatrixGames
ba31c63073
Fix exception when deleting saved games. 2024-05-21 16:34:33 -04:00
Asval
608e6fd42d forgot this 2024-05-19 21:17:41 +02:00
Asval
e94f558d6f split EGames 2024-05-19 20:31:36 +02:00
Asval
d7dac033cb bump 2024-05-18 21:09:11 +02:00
Asval
0bef5967c4 improved shader 2024-05-18 05:48:57 +02:00
LongerWarrior
c1c8adf122 3on3FreestyleRebound support 2024-05-13 22:01:12 +03:00
Asval
b2e7de5a47 time multiplier 2024-05-13 17:03:01 +02:00
GMatrixGames
ad665a83b9
Bump for Farlight 84 2024-05-12 15:48:41 -04:00
GMatrixGames
8867f4bf0c
Merge branch 'dev' of https://github.com/4sval/FModel into dev 2024-05-12 14:23:26 -04:00
GMatrixGames
b9a9a1e60e
Bump for QA 2024-05-12 14:23:08 -04:00
Asval
a8a278c989 more collisions 2024-05-12 07:25:23 +02:00
GMatrixGames
ade8da87a9
Marvel Rivals 2024-05-10 20:15:24 -04:00
LongerWarrior
330ab0a5b2 bump for PaxDei support 2024-05-09 19:22:10 +03:00
Asval
0b1c4bd4c9 bump 2024-05-08 13:56:02 +02:00
Asval
967ffefbe0 point clouds for now + reduced opacity 2024-05-06 23:22:59 +02:00
Asval
7e69adf978 crash fix 2024-05-04 23:48:32 +02:00
Asval
ca95df2dd4 convex collision preview 2024-05-04 23:25:01 +02:00
GMatrixGames
d3ce4bedd7
Cherry pick 0b11888ed2 2024-05-04 00:02:14 -04:00
Garrett Koleda
0b11888ed2
Change default mapping settings for Fortnite and Fortnite [LIVE]
Modifies the RegEx to display the first value in the array rather than only Oodle. Having this restriction prevents FModel from automatically loading mappings of other compression types. FModel and CUE4Parse both natively support all upstream compression types of the Usmap format, so it's unnecessary.
2024-05-03 23:56:48 -04:00
halfuwu
66ed2ce62a remove model/anim extension strings 2024-05-03 22:36:40 -04:00
LongerWarrior
2a733b9d9a compression settings 2024-05-03 22:36:40 -04:00
LongerWarrior
9a9eb02f05 DbD custom encryption handling 2024-05-03 23:40:00 +03:00
GMatrixGames
2df24242ac
Undawn custom encryption handling 2024-05-02 14:30:54 -04:00
Marlon
c3e4e7d404
obsolete fix? 2024-04-30 10:46:33 +02:00
Marlon
3714f1e125
cue4p sync 2024-04-30 10:46:23 +02:00
Asval
071d32b2da 8 bone influence 2024-04-30 02:54:40 +02:00
Asval
21cfa0781f bye bye 2024-04-27 12:35:13 +02:00
Marlon
0f20b543ca
use zip 2024-04-27 12:33:58 +02:00
Marlon
547dadbfc2
nuget updates 2024-04-27 12:33:47 +02:00
Marlon
81be2dacec
7z 2024-04-27 12:14:28 +02:00
Marlon
2d87dcf83c
grr 2024-04-27 12:10:24 +02:00
Marlon
5965cb7e7f
fix 2024-04-27 12:09:30 +02:00
Marlon
049a4434c9
updated qa qorkflow 2024-04-27 12:07:00 +02:00
Marlon
c52df06e7a
sync cue4parse 2024-04-27 11:22:59 +02:00
Marlon
82ce5e74aa
Merge pull request #463 from 4sval/manifestparser-rework 2024-04-27 11:19:21 +02:00
Marlon
a6eb13c0ab
updated faulty epicmanifestparser 2024-04-10 10:27:50 +02:00
Asval
eb9983bb18 improvements
- custom imgui settings directory
- viewport icons (not finished)
- can read ue version project name, number of archives, number of aes from logs
- bug fixes
2024-04-06 20:38:45 +02:00
Marlon
1852eb1b7d
removed submodule 2024-04-05 06:39:34 +02:00
Marlon
bc15757db0
nuget updates 2024-04-04 14:36:24 +02:00
Marlon
158bf9d6c7
minor changes 2024-04-04 14:36:12 +02:00
Marlon
4be7d4c3fb
small rework
removed DotNetZip
make use of Oodle.NET & Zlib-ng.NET
updated EpicManifestParser
minor optimizations
2024-04-04 06:18:49 +02:00
Asval
eabe4690aa bump 2024-03-08 16:57:08 +01:00
Lyndsey Winter
f64151b0ee
add support for ComponentContainer (#454) 2024-02-22 16:18:01 -05:00
FireMonkey
53bb6e1f90
LEGO Fortnite JIDO Emote Support (#455)
* LEGO Fortnite JIDO Emote Support

* LEGO Fortnite JIDO Emote Support

Might be a better way of doing this but if it fits it sits.
2024-02-22 16:06:30 -05:00
GMatrixGames
5e6765f4b0
version bump so you dont have to pull 2024-02-22 05:09:25 -05:00
GMatrixGames
8cbb9503fd
hm 2024-02-15 19:38:44 -05:00
GMatrixGames
3e8b155d4a
parse 2024-02-10 15:50:44 -05:00
Marlon
e22703c272
Merge pull request #450 from GhostScissors/dev 2024-02-08 14:56:47 +01:00
GhostScissors
033009ef82
Update BaseJuno.cs
Hmm not doing this anymore
2024-02-08 19:23:37 +05:30
GhostScissors
44f704258c
Update BaseJuno.cs
Some JIDO use "LowDetailsAssembledMeshSchema" proeprty instead of "AssembledMeshSchema"
2024-02-08 16:29:08 +05:30
Marlon
cd138b4037
Merge pull request #449 from djlorenzouasset/dev 2024-02-07 20:03:20 +01:00
ᴅᴊʟᴏʀ3xᴢᴏ
bb0741af38 QuestsBundle property change (DisplayName->ItemName) 2024-02-07 16:14:42 +01:00
GMatrixGames
ea279b6785
28.20 property name change 2024-02-07 09:50:22 -05:00
Asval
eb546602ea quest fix + juno icons 2024-01-28 15:53:32 +01:00
GMatrixGames
b2f6223958
parse update 2024-01-25 18:24:29 -05:00
Asval
69b18039e6 bump 2024-01-09 23:47:07 +01:00
Asval
fa11884936 bump acl 2024-01-09 17:26:27 +01:00
Asval
0160abf630 FModel v4.4.3.5 2024-01-07 13:18:23 +01:00
Asval
3721d7cd17 fixes 2023-12-07 18:05:47 +01:00
GMatrixGames
1461a6e525
Force QA build 2023-12-04 15:56:33 -05:00
Asval
a80379c3b9 bind pose stripped anim export + cubemap to panorama
Co-authored-by: Zain / Kaiser M <106357974+KaiserM21@users.noreply.github.com>
2023-11-25 05:36:08 +01:00
Asval
0551bc3731 .NET 8 2023-11-17 23:08:34 +01:00
Asval
8a790321dd if you have, you have 2023-11-16 22:50:41 +01:00
Asval
d6af826097 instanced SMs in umap 2023-11-16 20:39:01 +01:00
Asval
b525796f79 acl track skipping workaround 2023-11-09 20:34:58 +01:00
Asval
88adcd03be load ondemand archives 2023-11-08 22:15:04 +01:00
Valentin
5ef205c142
Update qa.yml 2023-11-04 20:23:37 +01:00
Valentin
b5a3b1c655
Update qa.yml 2023-11-04 20:18:11 +01:00
Valentin
e81259b298
Update qa.yml 2023-11-04 20:14:18 +01:00
Valentin
f510ca0a0c
Update qa.yml 2023-11-04 20:10:15 +01:00
Valentin
26a0dfcde8
Update qa.yml 2023-11-04 20:08:53 +01:00
Valentin
1e9f7e7355
Update qa.yml 2023-11-04 20:03:55 +01:00
Valentin
0f7bd1833a
Update qa.yml 2023-11-04 19:59:12 +01:00
Valentin
88e3ab1a71
Update qa.yml 2023-11-04 19:51:21 +01:00
Valentin
3b2a0fb044
Update qa.yml 2023-11-04 19:42:41 +01:00
Valentin
33e311c457
Update qa.yml 2023-11-04 19:05:58 +01:00
Valentin
3afb34018d
Update qa.yml 2023-11-04 19:04:10 +01:00
Valentin
5f2fea6828
Update qa.yml 2023-11-04 18:54:02 +01:00
Valentin
3cab5e6a27
Create qa.yml 2023-11-04 18:47:02 +01:00
Asval
af10998e31 hello 2023-11-04 18:40:26 +01:00
Asval
ef94d35ca2 qa testing test 2023-11-04 18:09:07 +01:00
GMatrixGames
5e5051628d
CUE4Parse-up 2023-11-02 20:52:27 -04:00
Valentin
9031dd40a3
Merge pull request #430 from KaiserM21/dev
Fix Overwrite Material Index
2023-10-30 20:51:13 +01:00
Zain / Kaiser M
422f892af8 Fix Overwrite Material Index 2023-10-30 18:52:40 +01:00
Asval
b478d43987 yes 2023-10-28 19:52:07 +02:00
Valentin
a83de461d4
Merge pull request #429 from 4sval/dev
Dev
2023-10-28 19:43:28 +02:00
Asval
db07574a61 FModel v.4.4.3.4 2023-10-28 19:40:36 +02:00
Asval
73e027c867 preview skeletons + save mesh materials switch 2023-10-28 18:17:16 +02:00
Marlon
4c5f5a213b
cue4p update 2023-10-20 06:11:34 +02:00
Asval
1f8f080c20 fixes 2023-09-18 18:10:55 +02:00
Asval
cf512a3d3d default checker texture instead of red + same vertex color texture 2023-08-30 18:47:18 +02:00
Asval
6b6403e9c1 better 2023-08-29 00:33:10 +02:00
Asval
6796577b9d whoops 2023-08-28 23:57:56 +02:00
Asval
b1be4da9b5 better skeleton tree 2023-08-28 23:50:09 +02:00
Asval
3c2795cded draw skeleton POC 2023-08-27 23:59:29 +02:00
Asval
a1494bf2e1 FModel v4.4.3.3 2023-08-27 19:03:46 +02:00
Asval
53bfbedefa fixes 2023-08-26 21:48:18 +02:00
Asval
ee9f3feb09 fixes 2023-08-25 22:30:11 +02:00
Asval
841f40e32b skeleton tree + fixed skeleton bones incorrect relation 2023-08-20 03:54:39 +02:00
Asval
1aa45b1b88 mock-up bone hierarchy 2023-08-17 01:06:36 +02:00
Asval
7d698ea8dd trying to merge 2 skeletons, doesn't fix anything 2023-08-13 08:10:29 +02:00
Asval
51516cd7d7 GetBufferSubData doesn't like being called too much, too bad 2023-08-12 19:09:48 +02:00
Asval
d97f570328 GetBoneTransform at runtime but it's flickering now, for later me 2023-08-12 06:34:12 +02:00
Asval
250b199b2a settings layout 2023-08-10 00:28:33 +02:00
Asval
223dd8fc3d multiple anims on same model 2023-08-09 23:56:20 +02:00
Asval
946d38c87b it is about time the update thread gets fixed 2023-08-06 20:02:21 +02:00
Asval
5dc90e6ec8 virtual textures preview 2023-08-03 04:39:15 +02:00
Asval
6ed335d483 POC frame interpolation 2023-08-01 23:06:16 +02:00
Asval
993726c681 ouin ouin ouin cue4parse errors 2023-07-30 13:06:50 +02:00
GMatrixGames
13c74f1506
AT9, forgot about this when recommiting 2023-07-11 11:00:44 -04:00
GMatrixGames
e92a66ac63
Recommit "AT9 Support" 2023-07-09 16:46:53 -04:00
GMatrixGames
a7fd4674b1
Revert "AT9 Support"
This reverts commit 8a72f29489.
2023-07-09 16:45:22 -04:00
GMatrixGames
8a72f29489
AT9 Support 2023-07-09 16:44:53 -04:00
Asval
487e40a64e #403 done 2023-07-09 04:03:13 +02:00
Asval
cb347adb52 ouin ouin ouin there's two 5.3 2023-06-24 00:00:57 +02:00
Valentin
b337ab7abd
Merge pull request #401 from 4sval/feature/settingsv2
feature/settingsv2
2023-06-23 00:15:21 +02:00
Asval
1829660256 valorant dirs 2023-06-23 00:08:27 +02:00
4sval
8fa027737b c'est ciao 2023-06-18 22:21:29 +02:00
4sval
d891c3e8da migrate main key + fix custom dirs dup + fix manual games add by ref 2023-06-18 04:51:36 +02:00
4sval
2a265eae60 usable 2023-06-18 03:26:50 +02:00
4sval
25c10e8638 that's the way 2023-06-18 02:34:54 +02:00
4sval
011046aa22 and this is where problems begin 2023-06-14 21:39:27 +02:00
4sval
a184a258ab explicit ue version 2023-06-13 23:54:09 +02:00
GMatrixGames
3435897e99
Handle KeepMobileMinLODSettingOnDesktop 2023-06-11 08:51:24 -04:00
Valentin
3f9b1197d9
Merge pull request #391 from 4sval/dev
Dev
2023-06-04 18:12:16 +02:00
Valentin
6cc0cbbf72
Delete autoclose.yml 2023-05-21 22:30:13 +02:00
Valentin
36123c4cb2
Merge pull request #389 from 4sval/dev
Dev
2023-05-21 22:28:12 +02:00
Valentin
8e2363d114
Merge pull request #349 from 4sval/dev 2022-12-29 10:51:06 +01:00
Valentin
918339c56b
sigh 2022-07-25 22:15:29 +02:00
Valentin
c014478abc
Merge pull request #308 from 4sval/dev
Dev
2022-07-24 20:50:20 +02:00
141 changed files with 7079 additions and 5572 deletions

View File

@ -21,16 +21,16 @@ jobs:
- name: Fetch Submodules Recursively
run: git submodule update --init --recursive
- name: .NET 6 Setup
- name: .NET 8 Setup
uses: actions/setup-dotnet@v2
with:
dotnet-version: '6.0.x'
dotnet-version: '8.0.x'
- name: .NET Restore
run: dotnet restore FModel
- name: .NET Publish
run: dotnet publish FModel -c Release --no-self-contained -r win-x64 -f net6.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }}
run: dotnet publish FModel -c Release --no-self-contained -r win-x64 -f net8.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }}
- name: ZIP File
uses: papeloto/action-zip@v1

65
.github/workflows/qa.yml vendored Normal file
View File

@ -0,0 +1,65 @@
name: FModel QA Builder
on:
push:
branches: [ dev ]
jobs:
build:
runs-on: windows-latest
steps:
- name: GIT Checkout
uses: actions/checkout@v4
with:
submodules: 'recursive'
- name: .NET 8 Setup
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: .NET Restore
run: dotnet restore FModel
- name: .NET Publish
run: dotnet publish "./FModel/FModel.csproj" -c Release --no-restore --no-self-contained -r win-x64 -f net8.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false
- name: ZIP File
uses: thedoctor0/zip-release@0.7.6
with:
type: zip
filename: ${{ github.sha }}.zip # will end up in working directory not the Publish folder
path: ./FModel/bin/Publish/FModel.exe
- name: Edit QA Artifact
uses: ncipollo/release-action@v1.14.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: 'FModel QA Testing'
body: 'Dev builds'
tag: 'qa'
artifacts: ${{ github.sha }}.zip
prerelease: true
allowUpdates: true
- name: Get Version
id: package_version
uses: kzrnm/get-net-sdk-project-versions-action@v2
with:
proj-path: ./FModel/FModel.csproj
- name: FModel Auth
id: fmodel_auth
uses: fjogeleit/http-request-action@v1.15.5
with:
url: "https://api.fmodel.app/v1/oauth/token"
data: '{"username": "${{ secrets.API_USERNAME }}", "password": "${{ secrets.API_PASSWORD }}"}'
- name: FModel Deploy Build
uses: fjogeleit/http-request-action@v1.15.5
with:
url: "https://api.fmodel.app/v1/infos/${{ secrets.QA_ID }}"
method: "PATCH"
bearerToken: ${{ fromJson(steps.fmodel_auth.outputs.response).accessToken }}
data: '{"version": "${{ steps.package_version.outputs.version }}-dev+${{ github.sha }}", "downloadUrl": "https://github.com/4sval/FModel/releases/download/qa/${{ github.sha }}.zip"}'

3
.gitmodules vendored
View File

@ -1,6 +1,3 @@
[submodule "CUE4Parse"]
path = CUE4Parse
url = https://github.com/FabianFG/CUE4Parse
[submodule "EpicManifestParser"]
path = EpicManifestParser
url = https://github.com/FModel/EpicManifestParser

@ -1 +1 @@
Subproject commit f3ad3b3093f2693604415fa94ce9a53bb5a5c3fa
Subproject commit 455b72e5e38bfe9476b5823bc642fc8ef488347f

@ -1 +0,0 @@
Subproject commit 21df8a55d474f14148a35bc943e06f3fdc20c997

View File

@ -50,7 +50,14 @@ public partial class App
var createMe = false;
if (!Directory.Exists(UserSettings.Default.OutputDirectory))
{
UserSettings.Default.OutputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Output");
var currentDir = Directory.GetCurrentDirectory();
var dirInfo = new DirectoryInfo(currentDir);
if (dirInfo.Attributes.HasFlag(FileAttributes.Archive))
throw new Exception("FModel cannot be run from an archive file. Please extract it and try again.");
if (dirInfo.Attributes.HasFlag(FileAttributes.ReadOnly))
throw new Exception("FModel cannot be run from a read-only directory. Please move it to a writable location.");
UserSettings.Default.OutputDirectory = Path.Combine(currentDir, "Output");
}
if (!Directory.Exists(UserSettings.Default.RawDataDirectory))
@ -90,14 +97,16 @@ public partial class App
Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data"));
#if DEBUG
Log.Logger = new LoggerConfiguration().WriteTo.Console(theme: AnsiConsoleTheme.Literate).CreateLogger();
Log.Logger = new LoggerConfiguration().WriteTo.Console(theme: AnsiConsoleTheme.Literate).WriteTo.File(
Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Debug-Log-{DateTime.Now:yyyy-MM-dd}.txt"),
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [FModel] [{Level:u3}] {Message:lj}{NewLine}{Exception}").CreateLogger();
#else
Log.Logger = new LoggerConfiguration().WriteTo.Console(theme: AnsiConsoleTheme.Literate).WriteTo.File(
Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Log-{DateTime.Now:yyyy-MM-dd}.txt"),
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [FModel] [{Level:u3}] {Message:lj}{NewLine}{Exception}").CreateLogger();
#endif
Log.Information("Version {Version}", Constants.APP_VERSION);
Log.Information("Version {Version} ({CommitId})", Constants.APP_VERSION, Constants.APP_COMMIT_ID);
Log.Information("{OS}", GetOperatingSystemProductName());
Log.Information("{RuntimeVer}", RuntimeInformation.FrameworkDescription);
Log.Information("Culture {SysLang}", CultureInfo.CurrentCulture);
@ -133,7 +142,7 @@ public partial class App
if (messageBox.Result == MessageBoxResult.Custom && (EErrorKind) messageBox.ButtonPressed.Id != EErrorKind.Ignore)
{
if ((EErrorKind) messageBox.ButtonPressed.Id == EErrorKind.ResetSettings)
UserSettings.Default = new UserSettings();
UserSettings.Delete();
ApplicationService.ApplicationView.Restart();
}

View File

@ -1,12 +1,20 @@
using System.Numerics;
using System;
using System.Diagnostics;
using System.IO;
using System.Numerics;
using System.Reflection;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Extensions;
namespace FModel;
public static class Constants
{
public static readonly string APP_VERSION = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
public static readonly string APP_PATH = Path.GetFullPath(Environment.GetCommandLineArgs()[0]);
public static readonly string APP_VERSION = FileVersionInfo.GetVersionInfo(APP_PATH).FileVersion;
public static readonly string APP_COMMIT_ID = FileVersionInfo.GetVersionInfo(APP_PATH).ProductVersion.SubstringAfter('+');
public static readonly string APP_SHORT_COMMIT_ID = APP_COMMIT_ID[..7];
public const string ZERO_64_CHAR = "0000000000000000000000000000000000000000000000000000000000000000";
public static readonly FGuid ZERO_GUID = new(0U);
@ -20,7 +28,10 @@ public static class Constants
public const string YELLOW = "#E5C07B";
public const string BLUE = "#528BCC";
public const string ISSUE_LINK = "https://github.com/iAmAsval/FModel/issues/new/choose";
public const string ISSUE_LINK = "https://github.com/4sval/FModel/discussions/categories/q-a";
public const string GH_REPO = "https://api.github.com/repos/4sval/FModel";
public const string GH_COMMITS_HISTORY = GH_REPO + "/commits";
public const string GH_RELEASES = GH_REPO + "/releases";
public const string DONATE_LINK = "https://fmodel.app/donate";
public const string DISCORD_LINK = "https://fmodel.app/discord";

View File

@ -26,7 +26,7 @@ public class BaseBundle : UCreator
{
_quests = new List<BaseQuest>();
if (Object.TryGetValue(out FText displayName, "DisplayName"))
if (Object.TryGetValue(out FText displayName, "DisplayName", "ItemName"))
DisplayName = displayName.Text.ToUpperInvariant();
if (Object.TryGetValue(out FStructFallback[] quests, "QuestInfos")) // prout :)

View File

@ -1,5 +1,7 @@
using System.Linq;
using CUE4Parse.GameTypes.FN.Enums;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.GameplayTags;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.UE4.Versions;
@ -32,10 +34,27 @@ public class BaseCommunity : BaseIcon
{
ParseForReward(UserSettings.Default.CosmeticDisplayAsset);
if (Object.TryGetValue(out FPackageIndex series, "Series") && Utils.TryGetPackageIndexExport(series, out UObject export))
_rarityName = export.Name;
if (Object.TryGetValue(out FPackageIndex series, "Series"))
{
_rarityName = series.Name;
}
else if (Object.TryGetValue(out FInstancedStruct[] dataList, "DataList") &&
dataList.FirstOrDefault(d => d.NonConstStruct?.TryGetValue(out FPackageIndex _, "Series") == true) is { } dl)
{
_rarityName = dl.NonConstStruct?.Get<FPackageIndex>("Series").Name;
}
else if (Object.TryGetValue(out FStructFallback componentContainer, "ComponentContainer") &&
componentContainer.TryGetValue(out FPackageIndex[] components, "Components") &&
components.FirstOrDefault(c => c.Name.Contains("Series")) is { } seriesDef &&
seriesDef.TryLoad(out var seriesDefObj) && seriesDefObj is not null &&
seriesDefObj.TryGetValue(out series, "Series"))
{
_rarityName = series.Name;
}
else
{
_rarityName = Object.GetOrDefault("Rarity", EFortRarity.Uncommon).GetDescription();
}
if (Object.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags"))
CheckGameplayTags(gameplayTags);

View File

@ -1,298 +1,325 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using CUE4Parse.GameTypes.FN.Enums;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Core.i18N;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.GameplayTags;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse_Conversion.Textures;
using FModel.Settings;
using SkiaSharp;
namespace FModel.Creator.Bases.FN;
public class BaseIcon : UCreator
{
public SKBitmap SeriesBackground { get; protected set; }
protected string ShortDescription { get; set; }
protected string CosmeticSource { get; set; }
protected Dictionary<string, SKBitmap> UserFacingFlags { get; set; }
public BaseIcon(UObject uObject, EIconStyle style) : base(uObject, style) { }
public void ParseForReward(bool isUsingDisplayAsset)
{
// rarity
if (Object.TryGetValue(out FPackageIndex series, "Series")) GetSeries(series);
else GetRarity(Object.GetOrDefault("Rarity", EFortRarity.Uncommon)); // default is uncommon
// preview
if (isUsingDisplayAsset && Utils.TryGetDisplayAsset(Object, out var preview))
Preview = preview;
else if (Object.TryGetValue(out FPackageIndex itemDefinition, "HeroDefinition", "WeaponDefinition"))
Preview = Utils.GetBitmap(itemDefinition);
else if (Object.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "EntryListIcon", "SmallPreviewImage", "ItemDisplayAsset", "LargeIcon", "ToastIcon", "SmallIcon"))
Preview = Utils.GetBitmap(largePreview);
else if (Object.TryGetValue(out string s, "LargePreviewImage") && !string.IsNullOrEmpty(s))
Preview = Utils.GetBitmap(s);
else if (Object.TryGetValue(out FPackageIndex otherPreview, "SmallPreviewImage", "ToastIcon", "access_item"))
Preview = Utils.GetBitmap(otherPreview);
else if (Object.TryGetValue(out UMaterialInstanceConstant materialInstancePreview, "EventCalloutImage"))
Preview = Utils.GetBitmap(materialInstancePreview);
else if (Object.TryGetValue(out FStructFallback brush, "IconBrush") && brush.TryGetValue(out UTexture2D res, "ResourceObject"))
Preview = Utils.GetBitmap(res);
// text
if (Object.TryGetValue(out FText displayName, "DisplayName", "DefaultHeaderText", "UIDisplayName", "EntryName", "EventCalloutTitle"))
DisplayName = displayName.Text;
if (Object.TryGetValue(out FText description, "Description", "GeneralDescription", "DefaultBodyText", "UIDescription", "UIDisplayDescription", "EntryDescription", "EventCalloutDescription"))
Description = description.Text;
else if (Object.TryGetValue(out FText[] descriptions, "Description"))
Description = string.Join('\n', descriptions.Select(x => x.Text));
if (Object.TryGetValue(out FText shortDescription, "ShortDescription", "UIDisplaySubName"))
ShortDescription = shortDescription.Text;
else if (Object.ExportType.Equals("AthenaItemWrapDefinition", StringComparison.OrdinalIgnoreCase))
ShortDescription = Utils.GetLocalizedResource("Fort.Cosmetics", "ItemWrapShortDescription", "Wrap");
// Only works on non-cataba designs
if (Object.TryGetValue(out FStructFallback eventArrowColor, "EventArrowColor") &&
eventArrowColor.TryGetValue(out FLinearColor specifiedArrowColor, "SpecifiedColor") &&
Object.TryGetValue(out FStructFallback eventArrowShadowColor, "EventArrowShadowColor") &&
eventArrowShadowColor.TryGetValue(out FLinearColor specifiedShadowColor, "SpecifiedColor"))
{
Background = new[] { SKColor.Parse(specifiedArrowColor.Hex), SKColor.Parse(specifiedShadowColor.Hex) };
Border = new[] { SKColor.Parse(specifiedShadowColor.Hex), SKColor.Parse(specifiedArrowColor.Hex) };
}
Description = Utils.RemoveHtmlTags(Description);
}
public override void ParseForInfo()
{
ParseForReward(UserSettings.Default.CosmeticDisplayAsset);
if (Object.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags"))
CheckGameplayTags(gameplayTags);
if (Object.TryGetValue(out FPackageIndex cosmeticItem, "cosmetic_item"))
CosmeticSource = cosmeticItem.Name;
}
protected void Draw(SKCanvas c)
{
switch (Style)
{
case EIconStyle.NoBackground:
DrawPreview(c);
break;
case EIconStyle.NoText:
DrawBackground(c);
DrawPreview(c);
DrawUserFacingFlags(c);
break;
default:
DrawBackground(c);
DrawPreview(c);
DrawTextBackground(c);
DrawDisplayName(c);
DrawDescription(c);
DrawToBottom(c, SKTextAlign.Right, CosmeticSource);
if (Description != ShortDescription)
DrawToBottom(c, SKTextAlign.Left, ShortDescription);
DrawUserFacingFlags(c);
break;
}
}
public override SKBitmap[] Draw()
{
var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(ret);
Draw(c);
return new[] { ret };
}
private void GetSeries(FPackageIndex s)
{
if (!Utils.TryGetPackageIndexExport(s, out UObject export)) return;
GetSeries(export);
}
protected void GetSeries(UObject uObject)
{
if (uObject is UTexture2D texture2D)
{
SeriesBackground = texture2D.Decode();
return;
}
if (uObject.TryGetValue(out FSoftObjectPath backgroundTexture, "BackgroundTexture"))
{
SeriesBackground = Utils.GetBitmap(backgroundTexture);
}
if (uObject.TryGetValue(out FStructFallback colors, "Colors") &&
colors.TryGetValue(out FLinearColor color1, "Color1") &&
colors.TryGetValue(out FLinearColor color2, "Color2") &&
colors.TryGetValue(out FLinearColor color3, "Color3"))
{
Background = new[] { SKColor.Parse(color1.Hex), SKColor.Parse(color3.Hex) };
Border = new[] { SKColor.Parse(color2.Hex), SKColor.Parse(color1.Hex) };
}
if (uObject.Name.Equals("PlatformSeries") &&
uObject.TryGetValue(out FSoftObjectPath itemCardMaterial, "ItemCardMaterial") &&
Utils.TryLoadObject(itemCardMaterial.AssetPathName.Text, out UMaterialInstanceConstant material))
{
foreach (var vectorParameter in material.VectorParameterValues)
{
if (vectorParameter.ParameterValue == null || !vectorParameter.ParameterInfo.Name.Text.Equals("ColorCircuitBackground"))
continue;
Background[0] = SKColor.Parse(vectorParameter.ParameterValue.Value.Hex);
}
}
}
private void GetRarity(EFortRarity r)
{
if (!Utils.TryLoadObject("FortniteGame/Content/Balance/RarityData.RarityData", out UObject export)) return;
if (export.GetByIndex<FStructFallback>((int) r) is { } data &&
data.TryGetValue(out FLinearColor color1, "Color1") &&
data.TryGetValue(out FLinearColor color2, "Color2") &&
data.TryGetValue(out FLinearColor color3, "Color3"))
{
Background = new[] { SKColor.Parse(color1.Hex), SKColor.Parse(color3.Hex) };
Border = new[] { SKColor.Parse(color2.Hex), SKColor.Parse(color1.Hex) };
}
}
protected string GetCosmeticSet(string setName)
{
if (!Utils.TryLoadObject("FortniteGame/Content/Athena/Items/Cosmetics/Metadata/CosmeticSets.CosmeticSets", out UDataTable cosmeticSets))
return string.Empty;
if (!cosmeticSets.TryGetDataTableRow(setName, StringComparison.OrdinalIgnoreCase, out var uObject))
return string.Empty;
var name = string.Empty;
if (uObject.TryGetValue(out FText displayName, "DisplayName"))
name = displayName.Text;
var format = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_SetMembership_NotRich", "\nPart of the {0} set.");
return string.Format(format, name);
}
protected (int, int) GetInternalSID(int number)
{
static int GetSeasonsInChapter(int chapter) => chapter switch
{
1 => 10,
2 => 8,
3 => 4,
_ => 10
};
var chapterIdx = 0;
var seasonIdx = 0;
while (number > 0)
{
var seasonsInChapter = GetSeasonsInChapter(++chapterIdx);
if (number > seasonsInChapter)
number -= seasonsInChapter;
else
{
seasonIdx = number;
number = 0;
}
}
return (chapterIdx, seasonIdx);
}
protected string GetCosmeticSeason(string seasonNumber)
{
var s = seasonNumber["Cosmetics.Filter.Season.".Length..];
var initial = int.Parse(s);
(int chapterIdx, int seasonIdx) = GetInternalSID(initial);
var season = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "SeasonTextFormat", "Season {0}");
var introduced = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_Season", "\nIntroduced in <SeasonText>{0}</>.");
if (initial <= 10) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, s)));
var chapter = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterTextFormat", "Chapter {0}");
var chapterFormat = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterSeasonTextFormat", "{0}, {1}");
var d = string.Format(chapterFormat, string.Format(chapter, chapterIdx), string.Format(season, seasonIdx));
return Utils.RemoveHtmlTags(string.Format(introduced, d));
}
private void CheckGameplayTags(FGameplayTagContainer gameplayTags)
{
if (gameplayTags.TryGetGameplayTag("Cosmetics.Source.", out var source))
CosmeticSource = source.Text["Cosmetics.Source.".Length..];
else if (gameplayTags.TryGetGameplayTag("Athena.ItemAction.", out var action))
CosmeticSource = action.Text["Athena.ItemAction.".Length..];
if (gameplayTags.TryGetGameplayTag("Cosmetics.Set.", out var set))
Description += GetCosmeticSet(set.Text);
if (gameplayTags.TryGetGameplayTag("Cosmetics.Filter.Season.", out var season))
Description += GetCosmeticSeason(season.Text);
GetUserFacingFlags(gameplayTags.GetAllGameplayTags(
"Cosmetics.UserFacingFlags.", "Homebase.Class.", "NPC.CharacterType.Survivor.Defender."));
}
protected void GetUserFacingFlags(IList<string> userFacingFlags)
{
if (userFacingFlags.Count < 1 || !Utils.TryLoadObject("FortniteGame/Content/Items/ItemCategories.ItemCategories", out UObject itemCategories))
return;
if (!itemCategories.TryGetValue(out FStructFallback[] tertiaryCategories, "TertiaryCategories"))
return;
UserFacingFlags = new Dictionary<string, SKBitmap>(userFacingFlags.Count);
foreach (var flag in userFacingFlags)
{
if (flag.Equals("Cosmetics.UserFacingFlags.HasUpgradeQuests", StringComparison.OrdinalIgnoreCase))
{
if (Object.ExportType.Equals("AthenaPetCarrierItemDefinition", StringComparison.OrdinalIgnoreCase))
UserFacingFlags[flag] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Pets-64.png"))?.Stream);
else UserFacingFlags[flag] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Quests-64.png"))?.Stream);
}
else
{
foreach (var category in tertiaryCategories)
{
if (category.TryGetValue(out FGameplayTagContainer tagContainer, "TagContainer") && tagContainer.TryGetGameplayTag(flag, out _) &&
category.TryGetValue(out FStructFallback categoryBrush, "CategoryBrush") && categoryBrush.TryGetValue(out FStructFallback brushXxs, "Brush_XXS") &&
brushXxs.TryGetValue(out FPackageIndex resourceObject, "ResourceObject") && Utils.TryGetPackageIndexExport(resourceObject, out UTexture2D texture))
{
UserFacingFlags[flag] = Utils.GetBitmap(texture);
}
}
}
}
}
private void DrawUserFacingFlags(SKCanvas c)
{
if (UserFacingFlags == null) return;
const int size = 25;
var x = Margin * (int) 2.5;
foreach (var flag in UserFacingFlags.Values.Where(flag => flag != null))
{
c.DrawBitmap(flag.Resize(size), new SKPoint(x, Margin * (int) 2.5), ImagePaint);
x += size;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using CUE4Parse.GameTypes.FN.Enums;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Core.i18N;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.GameplayTags;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse_Conversion.Textures;
using FModel.Settings;
using SkiaSharp;
namespace FModel.Creator.Bases.FN;
public class BaseIcon : UCreator
{
public SKBitmap SeriesBackground { get; protected set; }
protected string ShortDescription { get; set; }
protected string CosmeticSource { get; set; }
protected Dictionary<string, SKBitmap> UserFacingFlags { get; set; }
public BaseIcon(UObject uObject, EIconStyle style) : base(uObject, style) { }
public void ParseForReward(bool isUsingDisplayAsset)
{
// rarity
if (Object.TryGetValue(out FPackageIndex series, "Series")) GetSeries(series);
else if (Object.TryGetValue(out FStructFallback componentContainer, "ComponentContainer")) GetSeries(componentContainer);
else GetRarity(Object.GetOrDefault("Rarity", EFortRarity.Uncommon)); // default is uncommon
if (Object.TryGetValue(out FInstancedStruct[] dataList, "DataList"))
{
GetSeries(dataList);
Preview = Utils.GetBitmap(dataList);
}
// preview
if (Preview is null)
{
if (isUsingDisplayAsset && Utils.TryGetDisplayAsset(Object, out var preview))
Preview = preview;
else if (Object.TryGetValue(out FPackageIndex itemDefinition, "HeroDefinition", "WeaponDefinition"))
Preview = Utils.GetBitmap(itemDefinition);
else if (Object.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "EntryListIcon", "SmallPreviewImage", "BundleImage", "ItemDisplayAsset", "LargeIcon", "ToastIcon", "SmallIcon"))
Preview = Utils.GetBitmap(largePreview);
else if (Object.TryGetValue(out string s, "LargePreviewImage") && !string.IsNullOrEmpty(s))
Preview = Utils.GetBitmap(s);
else if (Object.TryGetValue(out FPackageIndex otherPreview, "SmallPreviewImage", "ToastIcon", "access_item"))
Preview = Utils.GetBitmap(otherPreview);
else if (Object.TryGetValue(out UMaterialInstanceConstant materialInstancePreview, "EventCalloutImage"))
Preview = Utils.GetBitmap(materialInstancePreview);
else if (Object.TryGetValue(out FStructFallback brush, "IconBrush") && brush.TryGetValue(out UTexture2D res, "ResourceObject"))
Preview = Utils.GetBitmap(res);
}
// text
if (Object.TryGetValue(out FText displayName, "DisplayName", "ItemName", "BundleName", "DefaultHeaderText", "UIDisplayName", "EntryName", "EventCalloutTitle"))
DisplayName = displayName.Text;
if (Object.TryGetValue(out FText description, "Description", "ItemDescription", "BundleDescription", "GeneralDescription", "DefaultBodyText", "UIDescription", "UIDisplayDescription", "EntryDescription", "EventCalloutDescription"))
Description = description.Text;
else if (Object.TryGetValue(out FText[] descriptions, "Description"))
Description = string.Join('\n', descriptions.Select(x => x.Text));
if (Object.TryGetValue(out FText shortDescription, "ShortDescription", "UIDisplaySubName"))
ShortDescription = shortDescription.Text;
else if (Object.ExportType.Equals("AthenaItemWrapDefinition", StringComparison.OrdinalIgnoreCase))
ShortDescription = Utils.GetLocalizedResource("Fort.Cosmetics", "ItemWrapShortDescription", "Wrap");
// Only works on non-cataba designs
if (Object.TryGetValue(out FStructFallback eventArrowColor, "EventArrowColor") &&
eventArrowColor.TryGetValue(out FLinearColor specifiedArrowColor, "SpecifiedColor") &&
Object.TryGetValue(out FStructFallback eventArrowShadowColor, "EventArrowShadowColor") &&
eventArrowShadowColor.TryGetValue(out FLinearColor specifiedShadowColor, "SpecifiedColor"))
{
Background = new[] { SKColor.Parse(specifiedArrowColor.Hex), SKColor.Parse(specifiedShadowColor.Hex) };
Border = new[] { SKColor.Parse(specifiedShadowColor.Hex), SKColor.Parse(specifiedArrowColor.Hex) };
}
Description = Utils.RemoveHtmlTags(Description);
}
public override void ParseForInfo()
{
ParseForReward(UserSettings.Default.CosmeticDisplayAsset);
if (Object.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags"))
CheckGameplayTags(gameplayTags);
if (Object.TryGetValue(out FPackageIndex cosmeticItem, "cosmetic_item"))
CosmeticSource = cosmeticItem.Name;
}
protected void Draw(SKCanvas c)
{
switch (Style)
{
case EIconStyle.NoBackground:
DrawPreview(c);
break;
case EIconStyle.NoText:
DrawBackground(c);
DrawPreview(c);
DrawUserFacingFlags(c);
break;
default:
DrawBackground(c);
DrawPreview(c);
DrawTextBackground(c);
DrawDisplayName(c);
DrawDescription(c);
DrawToBottom(c, SKTextAlign.Right, CosmeticSource);
if (Description != ShortDescription)
DrawToBottom(c, SKTextAlign.Left, ShortDescription);
DrawUserFacingFlags(c);
break;
}
}
public override SKBitmap[] Draw()
{
var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(ret);
Draw(c);
return new[] { ret };
}
private void GetSeries(FPackageIndex s)
{
if (!Utils.TryGetPackageIndexExport(s, out UObject export)) return;
GetSeries(export);
}
private void GetSeries(FInstancedStruct[] s)
{
if (s.FirstOrDefault(d => d.NonConstStruct?.TryGetValue(out FPackageIndex _, "Series") == true) is { } dl)
GetSeries(dl.NonConstStruct?.Get<FPackageIndex>("Series"));
}
private void GetSeries(FStructFallback s)
{
if (!s.TryGetValue(out FPackageIndex[] components, "Components")) return;
if (components.FirstOrDefault(c => c.Name.Contains("Series")) is not { } seriesDef ||
!seriesDef.TryLoad(out var seriesDefObj) || seriesDefObj is null ||
!seriesDefObj.TryGetValue(out UObject series, "Series")) return;
GetSeries(series);
}
protected void GetSeries(UObject uObject)
{
if (uObject is UTexture2D texture2D)
{
SeriesBackground = texture2D.Decode();
return;
}
if (uObject.TryGetValue(out FSoftObjectPath backgroundTexture, "BackgroundTexture"))
{
SeriesBackground = Utils.GetBitmap(backgroundTexture);
}
if (uObject.TryGetValue(out FStructFallback colors, "Colors") &&
colors.TryGetValue(out FLinearColor color1, "Color1") &&
colors.TryGetValue(out FLinearColor color2, "Color2") &&
colors.TryGetValue(out FLinearColor color3, "Color3"))
{
Background = new[] { SKColor.Parse(color1.Hex), SKColor.Parse(color3.Hex) };
Border = new[] { SKColor.Parse(color2.Hex), SKColor.Parse(color1.Hex) };
}
if (uObject.Name.Equals("PlatformSeries") &&
uObject.TryGetValue(out FSoftObjectPath itemCardMaterial, "ItemCardMaterial") &&
Utils.TryLoadObject(itemCardMaterial.AssetPathName.Text, out UMaterialInstanceConstant material))
{
foreach (var vectorParameter in material.VectorParameterValues)
{
if (vectorParameter.ParameterValue == null || !vectorParameter.ParameterInfo.Name.Text.Equals("ColorCircuitBackground"))
continue;
Background[0] = SKColor.Parse(vectorParameter.ParameterValue.Value.Hex);
}
}
}
private void GetRarity(EFortRarity r)
{
if (!Utils.TryLoadObject("FortniteGame/Content/Balance/RarityData.RarityData", out UObject export)) return;
if (export.GetByIndex<FStructFallback>((int) r) is { } data &&
data.TryGetValue(out FLinearColor color1, "Color1") &&
data.TryGetValue(out FLinearColor color2, "Color2") &&
data.TryGetValue(out FLinearColor color3, "Color3"))
{
Background = new[] { SKColor.Parse(color1.Hex), SKColor.Parse(color3.Hex) };
Border = new[] { SKColor.Parse(color2.Hex), SKColor.Parse(color1.Hex) };
}
}
protected string GetCosmeticSet(string setName)
{
if (!Utils.TryLoadObject("FortniteGame/Content/Athena/Items/Cosmetics/Metadata/CosmeticSets.CosmeticSets", out UDataTable cosmeticSets))
return string.Empty;
if (!cosmeticSets.TryGetDataTableRow(setName, StringComparison.OrdinalIgnoreCase, out var uObject))
return string.Empty;
var name = string.Empty;
if (uObject.TryGetValue(out FText displayName, "DisplayName"))
name = displayName.Text;
var format = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_SetMembership_NotRich", "\nPart of the {0} set.");
return string.Format(format, name);
}
protected (int, int) GetInternalSID(int number)
{
static int GetSeasonsInChapter(int chapter) => chapter switch
{
1 => 10,
2 => 8,
3 => 4,
4 => 5,
_ => 10
};
var chapterIdx = 0;
var seasonIdx = 0;
while (number > 0)
{
var seasonsInChapter = GetSeasonsInChapter(++chapterIdx);
if (number > seasonsInChapter)
number -= seasonsInChapter;
else
{
seasonIdx = number;
number = 0;
}
}
return (chapterIdx, seasonIdx);
}
protected string GetCosmeticSeason(string seasonNumber)
{
var s = seasonNumber["Cosmetics.Filter.Season.".Length..];
var initial = int.Parse(s);
(int chapterIdx, int seasonIdx) = GetInternalSID(initial);
var season = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "SeasonTextFormat", "Season {0}");
var introduced = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_Season", "\nIntroduced in <SeasonText>{0}</>.");
if (initial <= 10) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, s)));
var chapter = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterTextFormat", "Chapter {0}");
var chapterFormat = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterSeasonTextFormat", "{0}, {1}");
var d = string.Format(chapterFormat, string.Format(chapter, chapterIdx), string.Format(season, seasonIdx));
return Utils.RemoveHtmlTags(string.Format(introduced, d));
}
private void CheckGameplayTags(FGameplayTagContainer gameplayTags)
{
if (gameplayTags.TryGetGameplayTag("Cosmetics.Source.", out var source))
CosmeticSource = source.Text["Cosmetics.Source.".Length..];
else if (gameplayTags.TryGetGameplayTag("Athena.ItemAction.", out var action))
CosmeticSource = action.Text["Athena.ItemAction.".Length..];
if (gameplayTags.TryGetGameplayTag("Cosmetics.Set.", out var set))
Description += GetCosmeticSet(set.Text);
if (gameplayTags.TryGetGameplayTag("Cosmetics.Filter.Season.", out var season))
Description += GetCosmeticSeason(season.Text);
GetUserFacingFlags(gameplayTags.GetAllGameplayTags(
"Cosmetics.UserFacingFlags.", "Homebase.Class.", "NPC.CharacterType.Survivor.Defender."));
}
protected void GetUserFacingFlags(IList<string> userFacingFlags)
{
if (userFacingFlags.Count < 1 || !Utils.TryLoadObject("FortniteGame/Content/Items/ItemCategories.ItemCategories", out UObject itemCategories))
return;
if (!itemCategories.TryGetValue(out FStructFallback[] tertiaryCategories, "TertiaryCategories"))
return;
UserFacingFlags = new Dictionary<string, SKBitmap>(userFacingFlags.Count);
foreach (var flag in userFacingFlags)
{
if (flag.Equals("Cosmetics.UserFacingFlags.HasUpgradeQuests", StringComparison.OrdinalIgnoreCase))
{
if (Object.ExportType.Equals("AthenaPetCarrierItemDefinition", StringComparison.OrdinalIgnoreCase))
UserFacingFlags[flag] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Pets-64.png"))?.Stream);
else UserFacingFlags[flag] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Quests-64.png"))?.Stream);
}
else
{
foreach (var category in tertiaryCategories)
{
if (category.TryGetValue(out FGameplayTagContainer tagContainer, "TagContainer") && tagContainer.TryGetGameplayTag(flag, out _) &&
category.TryGetValue(out FStructFallback categoryBrush, "CategoryBrush") && categoryBrush.TryGetValue(out FStructFallback brushXxs, "Brush_XXS") &&
brushXxs.TryGetValue(out FPackageIndex resourceObject, "ResourceObject") && Utils.TryGetPackageIndexExport(resourceObject, out UTexture2D texture))
{
UserFacingFlags[flag] = Utils.GetBitmap(texture);
}
}
}
}
}
private void DrawUserFacingFlags(SKCanvas c)
{
if (UserFacingFlags == null) return;
const int size = 25;
var x = Margin * (int) 2.5;
foreach (var flag in UserFacingFlags.Values.Where(flag => flag != null))
{
c.DrawBitmap(flag.Resize(size), new SKPoint(x, Margin * (int) 2.5), ImagePaint);
x += size;
}
}
}

View File

@ -52,7 +52,7 @@ public class BaseIconStats : BaseIcon
foreach (var poi in challengeMapPoiData)
{
if (!poi.TryGetValue(out FStructFallback locationTag, "LocationTag") || !locationTag.TryGetValue(out FName tagName, "TagName") ||
!tagName.Text.Equals(location.Text, StringComparison.OrdinalIgnoreCase) || !poi.TryGetValue(out FText text, "Text")) continue;
tagName != location.TagName || !poi.TryGetValue(out FText text, "Text")) continue;
locationName = text.Text;
break;
@ -92,7 +92,11 @@ public class BaseIconStats : BaseIcon
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "35D04D1B45737BEA25B69686D9E085B9", "Damage"), dmgPb * multiplier, 200));
}
if (weaponRowValue.TryGetValue(out float dmgCritical, "DamageZone_Critical"))
if (weaponRowValue.TryGetValue(out float mdpc, "MaxDamagePerCartridge") && mdpc >= 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "0DEF2455463B008C4499FEA03D149EDF", "Headshot Damage"), mdpc, 200));
}
else if (weaponRowValue.TryGetValue(out float dmgCritical, "DamageZone_Critical"))
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "0DEF2455463B008C4499FEA03D149EDF", "Headshot Damage"), dmgPb * dmgCritical * multiplier, 200));
}

View File

@ -29,12 +29,12 @@ public class BaseItemAccessToken : UCreator
_icon.ParseForReward(false);
}
if (Object.TryGetValue(out FText displayName, "DisplayName") && displayName.Text != "TBD")
if (Object.TryGetValue(out FText displayName, "DisplayName", "ItemName") && displayName.Text != "TBD")
DisplayName = displayName.Text;
else
DisplayName = _icon?.DisplayName;
Description = Object.TryGetValue(out FText description, "Description") ? description.Text : _icon?.Description;
Description = Object.TryGetValue(out FText description, "Description", "ItemDescription") ? description.Text : _icon?.Description;
if (Object.TryGetValue(out FText unlockDescription, "UnlockDescription")) _unlockedDescription = unlockDescription.Text;
}

View File

@ -0,0 +1,49 @@
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.UObject;
using SkiaSharp;
namespace FModel.Creator.Bases.FN;
public class BaseJuno : BaseIcon
{
private BaseIcon _character;
public BaseJuno(UObject uObject, EIconStyle style) : base(uObject, style)
{
}
public override void ParseForInfo()
{
if (Object.TryGetValue(out FSoftObjectPath baseCid, "BaseAthenaCharacterItemDefinition") &&
Utils.TryLoadObject(baseCid.AssetPathName.Text, out UObject cid))
{
_character = new BaseIcon(cid, Style);
_character.ParseForInfo();
if (Object.TryGetValue(out FSoftObjectPath assembledMeshSchema, "AssembledMeshSchema", "LowDetailsAssembledMeshSchema") &&
Utils.TryLoadObject(assembledMeshSchema.AssetPathName.Text, out UObject meshSchema) &&
meshSchema.TryGetValue(out FInstancedStruct[] additionalData, "AdditionalData"))
{
foreach (var data in additionalData)
{
if (data.NonConstStruct?.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "SmallPreviewImage") ?? false)
{
_character.Preview = Utils.GetBitmap(largePreview);
break;
}
}
}
}
if (Object.TryGetValue(out FSoftObjectPath baseEid, "BaseAthenaDanceItemDefinition") &&
Utils.TryLoadObject(baseEid.AssetPathName.Text, out UObject eid))
{
_character = new BaseIcon(eid, Style);
_character.ParseForInfo();
}
}
public override SKBitmap[] Draw() => _character.Draw();
}

View File

@ -31,6 +31,7 @@ public class BaseMaterialInstance : BaseIcon
case "TextureA":
case "TextureB":
case "OfferImage":
case "CarTexture":
Preview = Utils.GetBitmap(texture);
break;
}
@ -88,4 +89,4 @@ public class BaseMaterialInstance : BaseIcon
return new[] { ret };
}
}
}

View File

@ -17,10 +17,9 @@ public class BaseMtxOffer : UCreator
public override void ParseForInfo()
{
if (Object.TryGetValue(out FStructFallback typeImage, "DetailsImage", "TileImage") &&
typeImage.TryGetValue(out FPackageIndex resource, "ResourceObject"))
if (Object.TryGetValue(out FSoftObjectPath image, "SoftDetailsImage", "SoftTileImage"))
{
Preview = Utils.GetBitmap(resource);
Preview = Utils.GetBitmap(image);
}
if (Object.TryGetValue(out FStructFallback gradient, "Gradient") &&
@ -81,4 +80,4 @@ public class BaseMtxOffer : UCreator
return new[] { ret };
}
}
}

View File

@ -1,39 +1,45 @@
using System.Collections.Generic;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.UObject;
using SkiaSharp;
namespace FModel.Creator.Bases.FN;
public class BaseOfferDisplayData : UCreator
{
private BaseMaterialInstance[] _offerImages;
private readonly List<BaseMaterialInstance> _offerImages;
public BaseOfferDisplayData(UObject uObject, EIconStyle style) : base(uObject, style)
{
_offerImages = new List<BaseMaterialInstance>();
}
public override void ParseForInfo()
{
if (!Object.TryGetValue(out UMaterialInterface[] presentations, "Presentations"))
if (!Object.TryGetValue(out FStructFallback[] contextualPresentations, "ContextualPresentations"))
return;
_offerImages = new BaseMaterialInstance[presentations.Length];
for (var i = 0; i < _offerImages.Length; i++)
for (var i = 0; i < contextualPresentations.Length; i++)
{
var offerImage = new BaseMaterialInstance(presentations[i], Style);
if (!contextualPresentations[i].TryGetValue(out FSoftObjectPath material, "Material") ||
!material.TryLoad(out UMaterialInterface presentation)) continue;
var offerImage = new BaseMaterialInstance(presentation, Style);
offerImage.ParseForInfo();
_offerImages[i] = offerImage;
_offerImages.Add(offerImage);
}
}
public override SKBitmap[] Draw()
{
var ret = new SKBitmap[_offerImages.Length];
var ret = new SKBitmap[_offerImages.Count];
for (var i = 0; i < ret.Length; i++)
{
ret[i] = _offerImages[i].Draw()[0];
ret[i] = _offerImages[i]?.Draw()[0];
}
return ret;
}
}
}

View File

@ -34,7 +34,7 @@ public class BasePlaylist : UCreator
return;
var playlist = _apiEndpointView.FortniteApi.GetPlaylist(playlistName.Text);
if (!playlist.IsSuccess || !playlist.Data.Images.HasShowcase ||
if (!playlist.IsSuccess || playlist.Data.Images is not { HasShowcase: true } ||
!_apiEndpointView.FortniteApi.TryGetBytes(playlist.Data.Images.Showcase, out var image))
return;
@ -74,4 +74,4 @@ public class BasePlaylist : UCreator
if (_missionIcon == null) return;
c.DrawBitmap(_missionIcon, new SKPoint(5, 5), ImagePaint);
}
}
}

View File

@ -69,13 +69,19 @@ public class BaseQuest : BaseIcon
}
else
{
Description = ShortDescription;
if (Object.TryGetValue(out FText completionText, "CompletionText"))
Description += "\n" + completionText.Text;
if (Object.TryGetValue(out FSoftObjectPath tandemCharacterData, "TandemCharacterData") &&
if (!string.IsNullOrEmpty(ShortDescription))
Description = ShortDescription;
if (string.IsNullOrEmpty(DisplayName) && !string.IsNullOrEmpty(Description))
DisplayName = Description;
if (DisplayName == Description)
Description = string.Empty;
if ((Object.TryGetValue(out FSoftObjectPath icon, "QuestGiverWidgetIcon", "NotificationIconOverride") &&
Utils.TryLoadObject(icon.AssetPathName.Text, out UObject iconObject)) ||
(Object.TryGetValue(out FSoftObjectPath tandemCharacterData, "TandemCharacterData") &&
Utils.TryLoadObject(tandemCharacterData.AssetPathName.Text, out UObject uObject) &&
uObject.TryGetValue(out FSoftObjectPath tandemIcon, "EntryListIcon", "ToastIcon") &&
Utils.TryLoadObject(tandemIcon.AssetPathName.Text, out UObject iconObject))
Utils.TryLoadObject(tandemIcon.AssetPathName.Text, out iconObject)))
{
Preview = iconObject switch
{
@ -127,9 +133,16 @@ public class BaseQuest : BaseIcon
}
}
if (_reward == null && Object.TryGetValue(out UDataTable rewardsTable, "RewardsTable"))
if (_reward == null)
{
if (rewardsTable.TryGetDataTableRow("Default", StringComparison.InvariantCulture, out var row))
FName rowName = null;
if (Object.TryGetValue(out UDataTable rewardsTable, "RewardsTable"))
rowName = new FName("Default");
else if (Object.TryGetValue(out FStructFallback[] rewardTableRows, "IndividualRewardTableRows") &&
rewardTableRows.Length > 0 && rewardTableRows[0].TryGetValue(out rowName, "RowName") &&
rewardTableRows[0].TryGetValue(out rewardsTable, "DataTable")) {}
if (rewardsTable != null && rowName != null && rewardsTable.TryGetDataTableRow(rowName.Text, StringComparison.InvariantCulture, out var row))
{
if (row.TryGetValue(out FName templateId, "TemplateId") &&
row.TryGetValue(out int quantity, "Quantity"))

View File

@ -36,7 +36,7 @@ public class BaseSeason : UCreator
{
_bookXpSchedule = Array.Empty<Page>();
if (Object.TryGetValue(out FText displayName, "DisplayName"))
if (Object.TryGetValue(out FText displayName, "DisplayName", "ItemName"))
DisplayName = displayName.Text.ToUpperInvariant();
if (Object.TryGetValue(out FStructFallback seasonFirstWinRewards, "SeasonFirstWinRewards") &&

View File

@ -64,7 +64,7 @@ public class Reward
_rewardPaint.TextSize = 50;
if (HasReward())
{
c.DrawBitmap(_theReward.Preview.Resize((int) rect.Height), new SKPoint(rect.Left, rect.Top), _rewardPaint);
c.DrawBitmap((_theReward.Preview ?? _theReward.DefaultPreview).Resize((int) rect.Height), new SKPoint(rect.Left, rect.Top), _rewardPaint);
_rewardPaint.Color = _theReward.Border[0];
_rewardPaint.Typeface = _rewardQuantity.StartsWith("x") ? Utils.Typefaces.BundleNumber : Utils.Typefaces.Bundle;
@ -88,7 +88,7 @@ public class Reward
public void DrawSeasonWin(SKCanvas c, int size)
{
if (!HasReward()) return;
c.DrawBitmap(_theReward.Preview.Resize(size), new SKPoint(0, 0), _rewardPaint);
c.DrawBitmap((_theReward.Preview ?? _theReward.DefaultPreview).Resize(size), new SKPoint(0, 0), _rewardPaint);
}
public void DrawSeason(SKCanvas c, int x, int y, int areaSize)
@ -115,33 +115,33 @@ public class Reward
{
switch (trigger.ToLower())
{
case "athenabattlestar":
_theReward = new BaseIcon(null, EIconStyle.Default);
_theReward.Border[0] = SKColor.Parse("FFDB67");
_theReward.Background[0] = SKColor.Parse("8F4A20");
_theReward.Preview = Utils.GetBitmap("FortniteGame/Content/Athena/UI/Frontend/Art/T_UI_BP_BattleStar_L.T_UI_BP_BattleStar_L");
break;
case "athenaseasonalxp":
_theReward = new BaseIcon(null, EIconStyle.Default);
_theReward.Border[0] = SKColor.Parse("E6FDB1");
_theReward.Background[0] = SKColor.Parse("51830F");
_theReward.Preview = Utils.GetBitmap("FortniteGame/Content/UI/Foundation/Textures/Icons/Items/T-FNBR-XPUncommon-L.T-FNBR-XPUncommon-L");
break;
case "mtxgiveaway":
_theReward = new BaseIcon(null, EIconStyle.Default);
_theReward.Border[0] = SKColor.Parse("DCE6FF");
_theReward.Background[0] = SKColor.Parse("64A0AF");
_theReward.Preview = Utils.GetBitmap("FortniteGame/Content/UI/Foundation/Textures/Icons/Items/T-Items-MTX.T-Items-MTX");
break;
// case "athenabattlestar":
// _theReward = new BaseIcon(null, EIconStyle.Default);
// _theReward.Border[0] = SKColor.Parse("FFDB67");
// _theReward.Background[0] = SKColor.Parse("8F4A20");
// _theReward.Preview = Utils.GetBitmap("FortniteGame/Content/Athena/UI/Frontend/Art/T_UI_BP_BattleStar_L.T_UI_BP_BattleStar_L");
// break;
// case "athenaseasonalxp":
// _theReward = new BaseIcon(null, EIconStyle.Default);
// _theReward.Border[0] = SKColor.Parse("E6FDB1");
// _theReward.Background[0] = SKColor.Parse("51830F");
// _theReward.Preview = Utils.GetBitmap("FortniteGame/Content/UI/Foundation/Textures/Icons/Items/T-FNBR-XPUncommon-L.T-FNBR-XPUncommon-L");
// break;
// case "mtxgiveaway":
// _theReward = new BaseIcon(null, EIconStyle.Default);
// _theReward.Border[0] = SKColor.Parse("DCE6FF");
// _theReward.Background[0] = SKColor.Parse("64A0AF");
// _theReward.Preview = Utils.GetBitmap("FortniteGame/Content/UI/Foundation/Textures/Icons/Items/T-Items-MTX.T-Items-MTX");
// break;
default:
{
var path = Utils.GetFullPath($"FortniteGame/Content/Athena/.*?/{trigger}.*"); // path has no objectname and its needed so we push the trigger again as the objectname
var path = Utils.GetFullPath($"FortniteGame/(?:Content/Athena|Content/Items|Plugins/GameFeatures)/.*?/{trigger}.uasset"); // path has no objectname and its needed so we push the trigger again as the objectname
if (!string.IsNullOrWhiteSpace(path) && Utils.TryLoadObject(path.Replace("uasset", trigger), out UObject d))
{
_theReward = new BaseIcon(d, EIconStyle.Default);
_theReward.ParseForReward(false);
_theReward.Border[0] = SKColors.White;
_rewardQuantity = _theReward.DisplayName;
_rewardQuantity = $"{_theReward.DisplayName} ({_rewardQuantity})";
}
break;

View File

@ -1,281 +1,256 @@
using System;
using System.Runtime.CompilerServices;
using CUE4Parse.UE4.Assets.Exports;
using FModel.Creator.Bases;
using FModel.Creator.Bases.BB;
using FModel.Creator.Bases.FN;
using FModel.Creator.Bases.MV;
using FModel.Creator.Bases.SB;
namespace FModel.Creator;
public class CreatorPackage : IDisposable
{
private UObject _object;
private EIconStyle _style;
public CreatorPackage(UObject uObject, EIconStyle style)
{
_object = uObject;
_style = style;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public UCreator ConstructCreator()
{
TryConstructCreator(out var creator);
return creator;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryConstructCreator(out UCreator creator)
{
switch (_object.ExportType)
{
// Fortnite
case "FortCreativeWeaponMeleeItemDefinition":
case "AthenaConsumableEmoteItemDefinition":
case "AthenaSkyDiveContrailItemDefinition":
case "AthenaLoadingScreenItemDefinition":
case "AthenaVictoryPoseItemDefinition":
case "AthenaPetCarrierItemDefinition":
case "AthenaMusicPackItemDefinition":
case "AthenaBattleBusItemDefinition":
case "AthenaCharacterItemDefinition":
case "AthenaMapMarkerItemDefinition":
case "AthenaBackpackItemDefinition":
case "AthenaPickaxeItemDefinition":
case "AthenaGadgetItemDefinition":
case "AthenaGliderItemDefinition":
case "AthenaSprayItemDefinition":
case "AthenaDanceItemDefinition":
case "AthenaEmojiItemDefinition":
case "AthenaItemWrapDefinition":
case "AthenaToyItemDefinition":
case "FortHeroType":
case "FortTokenType":
case "FortAbilityKit":
case "FortWorkerType":
case "RewardGraphToken":
case "FortBannerTokenType":
case "FortVariantTokenType":
case "FortDecoItemDefinition":
case "FortStatItemDefinition":
case "FortAmmoItemDefinition":
case "FortEmoteItemDefinition":
case "FortBadgeItemDefinition":
case "FortAwardItemDefinition":
case "FortGadgetItemDefinition":
case "AthenaCharmItemDefinition":
case "FortPlaysetItemDefinition":
case "FortGiftBoxItemDefinition":
case "FortOutpostItemDefinition":
case "FortVehicleItemDefinition":
case "FortCardPackItemDefinition":
case "FortDefenderItemDefinition":
case "FortCurrencyItemDefinition":
case "FortResourceItemDefinition":
case "FortBackpackItemDefinition":
case "FortEventQuestMapDataAsset":
case "FortWeaponModItemDefinition":
case "FortCodeTokenItemDefinition":
case "FortSchematicItemDefinition":
case "FortWorldMultiItemDefinition":
case "FortAlterationItemDefinition":
case "FortExpeditionItemDefinition":
case "FortIngredientItemDefinition":
case "FortAccountBuffItemDefinition":
case "FortWeaponMeleeItemDefinition":
case "FortPlayerPerksItemDefinition":
case "FortPlaysetPropItemDefinition":
case "FortHomebaseNodeItemDefinition":
case "FortNeverPersistItemDefinition":
case "FortPlayerAugmentItemDefinition":
case "RadioContentSourceItemDefinition":
case "FortPlaysetGrenadeItemDefinition":
case "FortPersonalVehicleItemDefinition":
case "FortGameplayModifierItemDefinition":
case "FortHardcoreModifierItemDefinition":
case "FortConsumableAccountItemDefinition":
case "FortConversionControlItemDefinition":
case "FortAccountBuffCreditItemDefinition":
case "FortEventCurrencyItemDefinitionRedir":
case "FortPersistentResourceItemDefinition":
case "FortHomebaseBannerIconItemDefinition":
case "FortCampaignHeroLoadoutItemDefinition":
case "FortConditionalResourceItemDefinition":
case "FortChallengeBundleScheduleDefinition":
case "FortWeaponMeleeDualWieldItemDefinition":
case "FortDailyRewardScheduleTokenDefinition":
case "FortCreativeWeaponRangedItemDefinition":
case "FortCreativeRealEstatePlotItemDefinition":
case "AthenaDanceItemDefinition_AdHocSquadsJoin_C":
case "StWFortAccoladeItemDefinition":
case "FortSmartBuildingItemDefinition":
creator = _style switch
{
EIconStyle.Cataba => new BaseCommunity(_object, _style, "Cataba"),
_ => new BaseIcon(_object, _style)
};
return true;
case "FortTandemCharacterData":
creator = new BaseTandem(_object, _style);
return true;
case "FortTrapItemDefinition":
case "FortSpyTechItemDefinition":
case "FortAccoladeItemDefinition":
case "FortContextTrapItemDefinition":
case "FortWeaponRangedItemDefinition":
case "Daybreak_LevelExitVehicle_PartItemDefinition_C":
creator = new BaseIconStats(_object, _style);
return true;
case "FortItemSeriesDefinition":
creator = new BaseSeries(_object, _style);
return true;
case "MaterialInstanceConstant"
when _object.Owner != null &&
(_object.Owner.Name.EndsWith($"/MI_OfferImages/{_object.Name}", StringComparison.OrdinalIgnoreCase) ||
_object.Owner.Name.EndsWith($"/RenderSwitch_Materials/{_object.Name}", StringComparison.OrdinalIgnoreCase) ||
_object.Owner.Name.EndsWith($"/MI_BPTile/{_object.Name}", StringComparison.OrdinalIgnoreCase)):
creator = new BaseMaterialInstance(_object, _style);
return true;
case "AthenaItemShopOfferDisplayData":
creator = new BaseOfferDisplayData(_object, _style);
return true;
case "FortMtxOfferData":
creator = new BaseMtxOffer(_object, _style);
return true;
case "FortPlaylistAthena":
creator = new BasePlaylist(_object, _style);
return true;
case "FortFeatItemDefinition":
case "FortQuestItemDefinition":
case "FortQuestItemDefinition_Athena":
case "FortQuestItemDefinition_Campaign":
case "AthenaDailyQuestDefinition":
case "FortUrgentQuestItemDefinition":
creator = new Bases.FN.BaseQuest(_object, _style);
return true;
case "FortCompendiumItemDefinition":
case "FortChallengeBundleItemDefinition":
creator = new BaseBundle(_object, _style);
return true;
case "AthenaSeasonItemDefinition":
creator = new BaseSeason(_object, _style);
return true;
case "FortItemAccessTokenType":
creator = new BaseItemAccessToken(_object, _style);
return true;
case "FortCreativeOption":
case "PlaylistUserOptionEnum":
case "PlaylistUserOptionBool":
case "PlaylistUserOptionString":
case "PlaylistUserOptionIntEnum":
case "PlaylistUserOptionIntRange":
case "PlaylistUserOptionColorEnum":
case "PlaylistUserOptionFloatEnum":
case "PlaylistUserOptionFloatRange":
case "PlaylistUserTintedIconIntEnum":
case "PlaylistUserOptionPrimaryAsset":
case "PlaylistUserOptionCollisionProfileEnum":
creator = new BaseUserControl(_object, _style);
return true;
// PandaGame
case "CharacterData":
creator = new BaseFighter(_object, _style);
return true;
case "PerkGroup":
creator = new BasePerkGroup(_object, _style);
return true;
case "StatTrackingBundleData":
case "HydraSyncedDataAsset":
case "AnnouncerPackData":
case "CharacterGiftData":
case "ProfileIconData":
case "RingOutVfxData":
case "BannerData":
case "EmoteData":
case "TauntData":
case "SkinData":
case "PerkData":
creator = new BasePandaIcon(_object, _style);
return true;
case "QuestData":
creator = new Bases.MV.BaseQuest(_object, _style);
return true;
// Battle Breakers
case "WExpGenericAccountItemDefinition":
case "WExpGearAccountItemDefinition":
case "WExpHQWorkerLodgesDefinition":
case "WExpPersonalEventDefinition":
case "WExpUpgradePotionDefinition":
case "WExpAccountRewardDefinition":
case "WExpHQBlacksmithDefinition":
case "WExpHQSecretShopDefinition":
case "WExpHQMonsterPitDefinition":
case "WExpHQHeroTowerDefinition":
case "WExpVoucherItemDefinition":
case "WExpTreasureMapDefinition":
case "WExpHammerChestDefinition":
case "WExpHQWorkshopDefinition":
case "WExpUnlockableDefinition":
case "WExpHQSmelterDefinition":
case "WExpContainerDefinition":
case "WExpCharacterDefinition":
case "WExpHQMarketDefinition":
case "WExpGiftboxDefinition":
case "WExpStandInDefinition":
case "WExpRegionDefinition":
case "WExpHQMineDefinition":
case "WExpXpBookDefinition":
case "WExpTokenDefinition":
case "WExpItemDefinition":
case "WExpZoneDefinition":
creator = new BaseBreakersIcon(_object, EIconStyle.Default);
return true;
// Spellbreak
case "GTargetedTeleportActiveSkill":
case "GChronomasterV2ActiveSkill":
case "GShadowstepActiveSkill":
case "GGatewayActiveSkill":
case "GStealthActiveSkill":
case "GFeatherActiveSkill":
case "GCosmeticDropTrail":
case "GFlightActiveSkill":
case "GCosmeticRunTrail":
case "GCosmeticArtifact":
case "GCosmeticTriumph":
case "GWolfsbloodSkill":
case "GDashActiveSkill":
case "GCharacterPerk":
case "GCosmeticTitle":
case "GCosmeticBadge":
case "GRMTStoreOffer":
case "GCosmeticEmote":
case "GCosmeticCard":
case "GCosmeticSkin":
case "GStoreOffer":
case "GAccolade":
case "GRuneItem":
case "GQuest":
creator = new BaseSpellIcon(_object, EIconStyle.Default);
return true;
case "GLeagueTier":
creator = new BaseLeague(_object, EIconStyle.Default);
return true;
case "GLeagueDivision":
creator = new BaseDivision(_object, EIconStyle.Default);
return true;
default:
creator = null;
return false;
}
}
public override string ToString() => $"{_object.ExportType} | {_style}";
public void Dispose()
{
_object = null;
}
}
using System;
using System.Runtime.CompilerServices;
using CUE4Parse.UE4.Assets.Exports;
using FModel.Creator.Bases;
using FModel.Creator.Bases.BB;
using FModel.Creator.Bases.FN;
using FModel.Creator.Bases.MV;
using FModel.Creator.Bases.SB;
namespace FModel.Creator;
public class CreatorPackage : IDisposable
{
private UObject _object;
private EIconStyle _style;
public CreatorPackage(UObject uObject, EIconStyle style)
{
_object = uObject;
_style = style;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public UCreator ConstructCreator()
{
TryConstructCreator(out var creator);
return creator;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryConstructCreator(out UCreator creator)
{
switch (_object.ExportType)
{
// Fortnite
case "FortCreativeWeaponMeleeItemDefinition":
case "AthenaConsumableEmoteItemDefinition":
case "AthenaSkyDiveContrailItemDefinition":
case "AthenaLoadingScreenItemDefinition":
case "AthenaVictoryPoseItemDefinition":
case "AthenaPetCarrierItemDefinition":
case "AthenaMusicPackItemDefinition":
case "AthenaBattleBusItemDefinition":
case "AthenaCharacterItemDefinition":
case "AthenaMapMarkerItemDefinition":
case "AthenaBackpackItemDefinition":
case "AthenaPickaxeItemDefinition":
case "AthenaGadgetItemDefinition":
case "AthenaGliderItemDefinition":
case "AthenaSprayItemDefinition":
case "AthenaDanceItemDefinition":
case "AthenaEmojiItemDefinition":
case "AthenaItemWrapDefinition":
case "AthenaToyItemDefinition":
case "FortHeroType":
case "FortTokenType":
case "FortAbilityKit":
case "FortWorkerType":
case "RewardGraphToken":
case "JunoKnowledgeBundle":
case "FortBannerTokenType":
case "FortVariantTokenType":
case "FortDecoItemDefinition":
case "FortStatItemDefinition":
case "FortAmmoItemDefinition":
case "FortEmoteItemDefinition":
case "FortBadgeItemDefinition":
case "SparksMicItemDefinition":
case "FortAwardItemDefinition":
case "FortStackItemDefinition":
case "FortWorldItemDefinition":
case "SparksAuraItemDefinition":
case "SparksDrumItemDefinition":
case "SparksBassItemDefinition":
case "FortGadgetItemDefinition":
case "AthenaCharmItemDefinition":
case "FortPlaysetItemDefinition":
case "FortGiftBoxItemDefinition":
case "FortOutpostItemDefinition":
case "FortVehicleItemDefinition":
case "FortMissionItemDefinition":
case "FortAccountItemDefinition":
case "SparksGuitarItemDefinition":
case "FortCardPackItemDefinition":
case "FortDefenderItemDefinition":
case "FortCurrencyItemDefinition":
case "FortResourceItemDefinition":
case "FortBackpackItemDefinition":
case "FortEventQuestMapDataAsset":
case "FortBuildingItemDefinition":
case "FortWeaponModItemDefinition":
case "FortCodeTokenItemDefinition":
case "FortSchematicItemDefinition":
case "FortAlterableItemDefinition":
case "SparksKeyboardItemDefinition":
case "FortWorldMultiItemDefinition":
case "FortAlterationItemDefinition":
case "FortExpeditionItemDefinition":
case "FortIngredientItemDefinition":
case "FortConsumableItemDefinition":
case "StWFortAccoladeItemDefinition":
case "FortAccountBuffItemDefinition":
case "FortWeaponMeleeItemDefinition":
case "FortPlayerPerksItemDefinition":
case "FortPlaysetPropItemDefinition":
case "FortPrerollDataItemDefinition":
case "JunoRecipeBundleItemDefinition":
case "FortHomebaseNodeItemDefinition":
case "FortNeverPersistItemDefinition":
case "FortPlayerAugmentItemDefinition":
case "FortSmartBuildingItemDefinition":
case "FortGiftBoxUnlockItemDefinition":
case "FortWeaponModItemDefinitionOptic":
case "RadioContentSourceItemDefinition":
case "FortPlaysetGrenadeItemDefinition":
case "JunoWeaponCreatureItemDefinition":
case "FortEventDependentItemDefinition":
case "FortPersonalVehicleItemDefinition":
case "FortGameplayModifierItemDefinition":
case "FortHardcoreModifierItemDefinition":
case "FortWeaponModItemDefinitionMagazine":
case "FortConsumableAccountItemDefinition":
case "FortConversionControlItemDefinition":
case "FortAccountBuffCreditItemDefinition":
case "JunoBuildInstructionsItemDefinition":
case "FortCharacterCosmeticItemDefinition":
case "JunoBuildingSetAccountItemDefinition":
case "FortEventCurrencyItemDefinitionRedir":
case "FortPersistentResourceItemDefinition":
case "FortWeaponMeleeOffhandItemDefinition":
case "FortHomebaseBannerIconItemDefinition":
case "FortVehicleCosmeticsVariantTokenType":
case "JunoBuildingPropAccountItemDefinition":
case "FortCampaignHeroLoadoutItemDefinition":
case "FortConditionalResourceItemDefinition":
case "FortChallengeBundleScheduleDefinition":
case "FortWeaponMeleeDualWieldItemDefinition":
case "FortDailyRewardScheduleTokenDefinition":
case "FortCreativeWeaponRangedItemDefinition":
case "FortVehicleCosmeticsItemDefinition_Body":
case "FortVehicleCosmeticsItemDefinition_Skin":
case "FortVehicleCosmeticsItemDefinition_Wheel":
case "FortCreativeRealEstatePlotItemDefinition":
case "FortDeployableBaseCloudSaveItemDefinition":
case "FortVehicleCosmeticsItemDefinition_Booster":
case "AthenaDanceItemDefinition_AdHocSquadsJoin_C":
case "FortVehicleCosmeticsItemDefinition_DriftSmoke":
case "FortVehicleCosmeticsItemDefinition_EngineAudio":
creator = _style switch
{
EIconStyle.Cataba => new BaseCommunity(_object, _style, "Cataba"),
_ => new BaseIcon(_object, _style)
};
return true;
case "JunoAthenaCharacterItemOverrideDefinition":
case "JunoAthenaDanceItemOverrideDefinition":
creator = new BaseJuno(_object, _style);
return true;
case "FortTandemCharacterData":
creator = new BaseTandem(_object, _style);
return true;
case "FortTrapItemDefinition":
case "FortSpyTechItemDefinition":
case "FortAccoladeItemDefinition":
case "FortContextTrapItemDefinition":
case "FortWeaponRangedItemDefinition":
case "Daybreak_LevelExitVehicle_PartItemDefinition_C":
creator = new BaseIconStats(_object, _style);
return true;
case "FortItemSeriesDefinition":
creator = new BaseSeries(_object, _style);
return true;
case "MaterialInstanceConstant"
when _object.Owner != null &&
(_object.Owner.Name.Contains("/MI_OfferImages/", StringComparison.OrdinalIgnoreCase) ||
_object.Owner.Name.EndsWith($"/RenderSwitch_Materials/{_object.Name}", StringComparison.OrdinalIgnoreCase) ||
_object.Owner.Name.EndsWith($"/MI_BPTile/{_object.Name}", StringComparison.OrdinalIgnoreCase)):
creator = new BaseMaterialInstance(_object, _style);
return true;
case "AthenaItemShopOfferDisplayData":
creator = new BaseOfferDisplayData(_object, _style);
return true;
case "FortMtxOfferData":
creator = new BaseMtxOffer(_object, _style);
return true;
case "FortPlaylistAthena":
creator = new BasePlaylist(_object, _style);
return true;
case "FortFeatItemDefinition":
case "FortQuestItemDefinition":
case "FortQuestItemDefinition_Athena":
case "FortQuestItemDefinition_Campaign":
case "AthenaDailyQuestDefinition":
case "FortUrgentQuestItemDefinition":
creator = new Bases.FN.BaseQuest(_object, _style);
return true;
case "FortCompendiumItemDefinition":
case "FortChallengeBundleItemDefinition":
creator = new BaseBundle(_object, _style);
return true;
// case "AthenaSeasonItemDefinition":
// creator = new BaseSeason(_object, _style);
// return true;
case "FortItemAccessTokenType":
creator = new BaseItemAccessToken(_object, _style);
return true;
case "FortCreativeOption":
case "PlaylistUserOptionEnum":
case "PlaylistUserOptionBool":
case "PlaylistUserOptionString":
case "PlaylistUserOptionIntEnum":
case "PlaylistUserOptionIntRange":
case "PlaylistUserOptionColorEnum":
case "PlaylistUserOptionFloatEnum":
case "PlaylistUserOptionFloatRange":
case "PlaylistUserTintedIconIntEnum":
case "PlaylistUserOptionPrimaryAsset":
case "PlaylistUserOptionCollisionProfileEnum":
creator = new BaseUserControl(_object, _style);
return true;
// PandaGame
case "CharacterData":
creator = new BaseFighter(_object, _style);
return true;
case "PerkGroup":
creator = new BasePerkGroup(_object, _style);
return true;
case "StatTrackingBundleData":
case "HydraSyncedDataAsset":
case "AnnouncerPackData":
case "CharacterGiftData":
case "ProfileIconData":
case "RingOutVfxData":
case "BannerData":
case "EmoteData":
case "TauntData":
case "SkinData":
case "PerkData":
creator = new BasePandaIcon(_object, _style);
return true;
case "QuestData":
creator = new Bases.MV.BaseQuest(_object, _style);
return true;
default:
creator = null;
return false;
}
}
public override string ToString() => $"{_object.ExportType} | {_style}";
public void Dispose()
{
_object = null;
}
}

View File

@ -21,7 +21,6 @@ public class Typefaces
private const string _BURBANK_BIG_REGULAR_BOLD = "BurbankBigRegular-Bold"; // official fortnite ig
private const string _BURBANK_SMALL_MEDIUM = "BurbankSmall-Medium";
private const string _DROID_SANS_FORTNITE_SUBSET = "DroidSans-Fortnite-Subset";
private const string _NIS_JYAU = "NIS_JYAU"; // japanese fortnite
private const string _NOTO_COLOR_EMOJI = "NotoColorEmoji";
private const string _NOTO_SANS_BOLD = "NotoSans-Bold";
private const string _NOTO_SANS_FORTNITE_BOLD = "NotoSans-Fortnite-Bold";
@ -32,7 +31,7 @@ public class Typefaces
private const string _NOTO_SANS_ARABIC_BLACK = "NotoSansArabic-Black"; // arabic fortnite
private const string _NOTO_SANS_ARABIC_BOLD = "NotoSansArabic-Bold";
private const string _NOTO_SANS_ARABIC_REGULAR = "NotoSansArabic-Regular";
private const string _NOTO_SANS_JP_BOLD = "NotoSansJP-Bold";
private const string _NOTO_SANS_JP_BOLD = "NotoSansJP-Bold"; // japanese fortnite
private const string _NOTO_SANS_KR_REGULAR = "NotoSansKR-Regular";
private const string _NOTO_SANS_SC_BLACK = "NotoSansSC-Black"; // simplified chinese fortnite
private const string _NOTO_SANS_SC_REGULAR = "NotoSansSC-Regular";
@ -49,23 +48,6 @@ public class Typefaces
private const string _XIANGHEHEI_SC_PRO_BLACK = "XiangHeHei_SC/MXiangHeHeiSCPro-Black";
private const string _XIANGHEHEI_SC_PRO_HEAVY = "XiangHeHei_SC/MXiangHeHeiSCPro-Heavy";
// Spellbreak
private const string _SPELLBREAK_BASE_PATH = "/Game/UI/Fonts/";
private const string _MONTSERRAT_SEMIBOLD = "Montserrat-Semibold";
private const string _MONTSERRAT_SEMIBOLD_ITALIC = "Montserrat-SemiBoldItalic";
private const string _NANUM_GOTHIC = "NanumGothic";
private const string _QUADRAT_BOLD = "Quadrat_Bold";
private const string _SEGOE_BOLD_ITALIC = "Segoe_Bold_Italic";
// WorldExplorers
private const string _BATTLE_BREAKERS_BASE_PATH = "/Game/UMG/Fonts/Faces/";
private const string _HEMIHEAD426 = "HemiHead426";
private const string _NOTO_SANS_JP_REGULAR = "NotoSansJP-Regular";
private const string _LATO_BLACK = "Lato-Black";
private const string _LATO_BLACK_ITALIC = "Lato-BlackItalic";
private const string _LATO_LIGHT = "Lato-Light";
private const string _LATO_MEDIUM = "Lato-Medium";
private readonly CUE4ParseViewModel _viewModel;
public readonly SKTypeface Default; // used as a fallback font for all untranslated strings (item source, ...)
@ -85,16 +67,16 @@ public class Typefaces
Default = SKTypeface.FromStream(Application.GetResourceStream(_BURBANK_BIG_CONDENSED_BOLD)?.Stream);
switch (viewModel.Game)
switch (viewModel.Provider.InternalGameName.ToUpperInvariant())
{
case FGame.FortniteGame:
case "FORTNITEGAME":
{
DisplayName = OnTheFly(_FORTNITE_BASE_PATH +
language switch
{
ELanguage.Korean => _ASIA_ERINM,
ELanguage.Russian => _BURBANK_BIG_CONDENSED_BLACK,
ELanguage.Japanese => _NIS_JYAU,
ELanguage.Japanese => _NOTO_SANS_JP_BOLD,
ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK,
ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK,
ELanguage.Chinese => _NOTO_SANS_SC_BLACK,
@ -130,7 +112,7 @@ public class Typefaces
{
ELanguage.Korean => _ASIA_ERINM,
ELanguage.Russian => _BURBANK_BIG_CONDENSED_BLACK,
ELanguage.Japanese => _NIS_JYAU,
ELanguage.Japanese => _NOTO_SANS_JP_BOLD,
ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK,
ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK,
ELanguage.Chinese => _NOTO_SANS_SC_BLACK,
@ -142,7 +124,7 @@ public class Typefaces
{
ELanguage.Korean => _ASIA_ERINM,
ELanguage.Russian => _BURBANK_BIG_CONDENSED_BLACK,
ELanguage.Japanese => _NIS_JYAU,
ELanguage.Japanese => _NOTO_SANS_JP_BOLD,
ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK,
ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK,
ELanguage.Chinese => _NOTO_SANS_SC_BLACK,
@ -153,7 +135,7 @@ public class Typefaces
language switch
{
ELanguage.Korean => _ASIA_ERINM,
ELanguage.Japanese => _NIS_JYAU,
ELanguage.Japanese => _NOTO_SANS_JP_BOLD,
ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK,
ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK,
ELanguage.Chinese => _NOTO_SANS_SC_BLACK,
@ -164,7 +146,7 @@ public class Typefaces
language switch
{
ELanguage.Korean => _ASIA_ERINM,
ELanguage.Japanese => _NIS_JYAU,
ELanguage.Japanese => _NOTO_SANS_JP_BOLD,
ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK,
ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK,
ELanguage.Chinese => _NOTO_SANS_SC_BLACK,
@ -172,36 +154,7 @@ public class Typefaces
} + _EXT);
break;
}
case FGame.WorldExplorers:
{
DisplayName = OnTheFly(_BATTLE_BREAKERS_BASE_PATH +
language switch
{
ELanguage.Korean => _NOTO_SANS_KR_REGULAR,
ELanguage.Russian => _LATO_BLACK,
ELanguage.Japanese => _NOTO_SANS_JP_REGULAR,
ELanguage.Chinese => _NOTO_SANS_SC_REGULAR,
_ => _HEMIHEAD426
} + _EXT);
Description = OnTheFly(_BATTLE_BREAKERS_BASE_PATH +
language switch
{
ELanguage.Korean => _NOTO_SANS_KR_REGULAR,
ELanguage.Russian => _LATO_BLACK,
ELanguage.Japanese => _NOTO_SANS_JP_REGULAR,
ELanguage.Chinese => _NOTO_SANS_SC_REGULAR,
_ => _HEMIHEAD426
} + _EXT);
break;
}
case FGame.g3:
{
DisplayName = OnTheFly(_SPELLBREAK_BASE_PATH + _QUADRAT_BOLD + _EXT);
Description = OnTheFly(_SPELLBREAK_BASE_PATH + _MONTSERRAT_SEMIBOLD + _EXT);
break;
}
case FGame.MultiVersus:
case "MULTIVERSUS":
{
DisplayName = OnTheFly(_PANDAGAME_BASE_PATH + language switch
{

View File

@ -11,6 +11,7 @@ using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.UE4.Assets.Objects;
using FModel.Framework;
using FModel.Extensions;
using FModel.Services;
@ -71,6 +72,7 @@ public static class Utils
return GetBitmap(material);
default:
{
if (export.TryGetValue(out FInstancedStruct[] dataList, "DataList")) return GetBitmap(dataList);
if (export.TryGetValue(out FSoftObjectPath previewImage, "LargePreviewImage", "SmallPreviewImage")) return GetBitmap(previewImage);
if (export.TryGetValue(out string largePreview, "LargePreviewImage")) return GetBitmap(largePreview);
if (export.TryGetValue(out FPackageIndex smallPreview, "SmallPreviewImage"))
@ -85,6 +87,21 @@ public static class Utils
}
}
public static SKBitmap GetBitmap(FInstancedStruct[] structs)
{
if (structs.FirstOrDefault(d => d.NonConstStruct?.TryGetValue(out FSoftObjectPath p, "LargeIcon") == true && !p.AssetPathName.IsNone) is { NonConstStruct: not null } isl)
{
return GetBitmap(isl.NonConstStruct.Get<FSoftObjectPath>("LargeIcon"));
}
if (structs.FirstOrDefault(d => d.NonConstStruct?.TryGetValue(out FSoftObjectPath p, "Icon") == true && !p.AssetPathName.IsNone) is { NonConstStruct: not null } isi)
{
return GetBitmap(isi.NonConstStruct.Get<FSoftObjectPath>("Icon"));
}
return null;
}
public static SKBitmap GetBitmap(UMaterialInstanceConstant material)
{
if (material == null) return null;
@ -112,7 +129,7 @@ public static class Utils
public static SKBitmap GetB64Bitmap(string b64) => SKBitmap.Decode(new MemoryStream(Convert.FromBase64String(b64)) { Position = 0 });
public static SKBitmap GetBitmap(FSoftObjectPath softObjectPath) => GetBitmap(softObjectPath.AssetPathName.Text);
public static SKBitmap GetBitmap(string fullPath) => TryLoadObject(fullPath, out UTexture2D texture) ? GetBitmap(texture) : null;
public static SKBitmap GetBitmap(UTexture2D texture) => texture.IsVirtual ? null : texture.Decode(UserSettings.Default.OverridedPlatform);
public static SKBitmap GetBitmap(UTexture2D texture) => texture.Decode(UserSettings.Default.CurrentDir.TexturePlatform);
public static SKBitmap GetBitmap(byte[] data) => SKBitmap.Decode(data);
public static SKBitmap ResizeWithRatio(this SKBitmap me, double width, double height)
@ -400,4 +417,4 @@ public static class Utils
return ret;
}
}
}

View File

@ -20,8 +20,7 @@ public enum EErrorKind
public enum SettingsOut
{
ReloadLocres,
ReloadMappings,
CheckForUpdates
ReloadMappings
}
public enum EStatusKind
@ -52,60 +51,8 @@ public enum EDiscordRpc
Never
}
public enum FGame
{
[Description("Unknown")]
Unknown,
[Description("Fortnite")]
FortniteGame,
[Description("Valorant")]
ShooterGame,
[Description("Dead By Daylight")]
DeadByDaylight,
[Description("Borderlands 3")]
OakGame,
[Description("Minecraft Dungeons")]
Dungeons,
[Description("Battle Breakers")]
WorldExplorers,
[Description("Spellbreak")]
g3,
[Description("State Of Decay 2")]
StateOfDecay2,
[Description("The Cycle")]
Prospect,
[Description("The Outer Worlds")]
Indiana,
[Description("Rogue Company")]
RogueCompany,
[Description("Star Wars: Jedi Fallen Order")]
SwGame,
[Description("Core")]
Platform,
[Description("Days Gone")]
BendGame,
[Description("PLAYERUNKNOWN'S BATTLEGROUNDS")]
TslGame,
[Description("Splitgate")]
PortalWars,
[Description("GTA: The Trilogy - Definitive Edition")]
Gameface,
[Description("Sea of Thieves")]
Athena,
[Description("DEPRECATED")]
PandaGame,
[Description("MultiVersus")]
MultiVersus,
[Description("Tower of Fantasy")]
Hotta,
[Description("eFootball 2023")]
eFootball
}
public enum ELoadingMode
{
[Description("Single")]
Single,
[Description("Multiple")]
Multiple,
[Description("All")]
@ -116,13 +63,15 @@ public enum ELoadingMode
AllButModified
}
public enum EUpdateMode
{
[Description("Stable")]
Stable,
[Description("Beta")]
Beta
}
// public enum EUpdateMode
// {
// [Description("Stable")]
// Stable,
// [Description("Beta")]
// Beta,
// [Description("QA Testing")]
// Qa
// }
public enum ECompressedAudio
{

View File

@ -11,8 +11,21 @@ public static class EnumExtensions
{
var fi = value.GetType().GetField(value.ToString());
if (fi == null) return $"{value} ({value:D})";
var attributes = (DescriptionAttribute[]) fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
return attributes.Length > 0 ? attributes[0].Description : $"{value} ({value:D})";
if (attributes.Length > 0) return attributes[0].Description;
var suffix = $"{value:D}";
var current = Convert.ToInt32(suffix);
var target = current & ~0xF;
if (current != target)
{
var values = Enum.GetValues(value.GetType());
var index = Array.IndexOf(values, value);
suffix = values.GetValue(index - (current - target))?.ToString();
}
return $"{value} ({suffix})";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -43,4 +56,4 @@ public static class EnumExtensions
var i = Array.IndexOf(values, value) - 1;
return i == -1 ? (T) values.GetValue(values.Length - 1) : (T) values.GetValue(i);
}
}
}

View File

@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using ICSharpCode.AvalonEdit.Document;
namespace FModel.Extensions;
@ -94,7 +95,7 @@ public static class StringExtensions
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetLineNumber(this string s, string lineToFind)
public static int GetNameLineNumber(this string s, string lineToFind)
{
if (int.TryParse(lineToFind, out var index))
return s.GetLineNumber(index);
@ -113,6 +114,25 @@ public static class StringExtensions
return 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetParentExportType(this TextDocument doc, int startOffset)
{
var line = doc.GetLineByOffset(startOffset);
var lineNumber = line.LineNumber - 1;
while (doc.GetText(line.Offset, line.Length) is { } content)
{
if (content.StartsWith(" \"Type\": \"", StringComparison.OrdinalIgnoreCase))
return content.Split("\"")[3];
lineNumber--;
if (lineNumber < 1) break;
line = doc.GetLineByNumber(lineNumber);
}
return string.Empty;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetKismetLineNumber(this string s, string input)
{

View File

@ -2,12 +2,12 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<ApplicationIcon>FModel.ico</ApplicationIcon>
<Version>4.4.3.2</Version>
<AssemblyVersion>4.4.3.2</AssemblyVersion>
<FileVersion>4.4.3.2</FileVersion>
<Version>4.4.4.0</Version>
<AssemblyVersion>4.4.4.0</AssemblyVersion>
<FileVersion>4.4.4.0</FileVersion>
<IsPackable>false</IsPackable>
<IsPublishable>true</IsPublishable>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
@ -45,6 +45,12 @@
<None Remove="Resources\gear.png" />
<None Remove="Resources\localization.png" />
<None Remove="Resources\materialicon.png" />
<None Remove="Resources\square.png" />
<None Remove="Resources\square_off.png" />
<None Remove="Resources\cube.png" />
<None Remove="Resources\cube_off.png" />
<None Remove="Resources\light.png" />
<None Remove="Resources\light_off.png" />
<None Remove="Resources\pc.png" />
<None Remove="Resources\puzzle.png" />
<None Remove="Resources\roguecompany.png" />
@ -77,6 +83,7 @@
<None Remove="Resources\linux.png" />
<None Remove="Resources\stateofdecay2.png" />
<None Remove="Resources\T_Placeholder_Item_Image.png" />
<None Remove="Resources\checker.png" />
<None Remove="Resources\T_ClipSize_Weapon_Stats.png" />
<None Remove="Resources\T_DamagePerBullet_Weapon_Stats.png" />
<None Remove="Resources\T_ReloadTime_Weapon_Stats.png" />
@ -107,6 +114,9 @@
<None Remove="Resources\picking.vert" />
<None Remove="Resources\light.frag" />
<None Remove="Resources\light.vert" />
<None Remove="Resources\bone.frag" />
<None Remove="Resources\bone.vert" />
<None Remove="Resources\collision.vert" />
</ItemGroup>
<ItemGroup>
@ -130,34 +140,36 @@
<EmbeddedResource Include="Resources\picking.vert" />
<EmbeddedResource Include="Resources\light.frag" />
<EmbeddedResource Include="Resources\light.vert" />
<EmbeddedResource Include="Resources\bone.frag" />
<EmbeddedResource Include="Resources\bone.vert" />
<EmbeddedResource Include="Resources\collision.vert" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AdonisUI" Version="1.17.1" />
<PackageReference Include="AdonisUI.ClassicTheme" Version="1.17.1" />
<PackageReference Include="Autoupdater.NET.Official" Version="1.8.2" />
<PackageReference Include="Autoupdater.NET.Official" Version="1.9.2" />
<PackageReference Include="AvalonEdit" Version="6.3.0.90" />
<PackageReference Include="CSCore" Version="1.2.1.2" />
<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
<PackageReference Include="ImGui.NET" Version="1.89.5" />
<PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.3.5" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="EpicManifestParser" Version="2.3.3" />
<PackageReference Include="ImGui.NET" Version="1.91.0.1" />
<PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.3.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NVorbis" Version="0.10.5" />
<PackageReference Include="Oodle.NET" Version="1.0.1" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
<PackageReference Include="OpenTK" Version="4.7.7" />
<PackageReference Include="RestSharp" Version="110.2.0" />
<PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.3" />
<PackageReference Include="OpenTK" Version="4.8.2" />
<PackageReference Include="RestSharp" Version="112.1.0" />
<PackageReference Include="Serilog" Version="4.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.8" />
<PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CUE4Parse\CUE4Parse-Conversion\CUE4Parse-Conversion.csproj" />
<ProjectReference Include="..\CUE4Parse\CUE4Parse\CUE4Parse.csproj" />
<ProjectReference Include="..\EpicManifestParser\src\EpicManifestParser\EpicManifestParser.csproj" />
</ItemGroup>
<ItemGroup>
@ -177,6 +189,12 @@
<Resource Include="Resources\gear.png" />
<Resource Include="Resources\localization.png" />
<Resource Include="Resources\materialicon.png" />
<Resource Include="Resources\square.png" />
<Resource Include="Resources\square_off.png" />
<Resource Include="Resources\cube.png" />
<Resource Include="Resources\cube_off.png" />
<Resource Include="Resources\light.png" />
<Resource Include="Resources\light_off.png" />
<Resource Include="Resources\pc.png" />
<Resource Include="Resources\puzzle.png" />
<Resource Include="Resources\roguecompany.png" />
@ -203,6 +221,7 @@
<Resource Include="Resources\linux.png" />
<Resource Include="Resources\stateofdecay2.png" />
<Resource Include="Resources\T_Placeholder_Item_Image.png" />
<Resource Include="Resources\checker.png" />
<Resource Include="Resources\T_ClipSize_Weapon_Stats.png" />
<Resource Include="Resources\T_DamagePerBullet_Weapon_Stats.png" />
<Resource Include="Resources\T_ReloadTime_Weapon_Stats.png" />

View File

@ -9,8 +9,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUE4Parse", "..\CUE4Parse\C
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUE4Parse-Conversion", "..\CUE4Parse\CUE4Parse-Conversion\CUE4Parse-Conversion.csproj", "{D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EpicManifestParser", "..\EpicManifestParser\src\EpicManifestParser\EpicManifestParser.csproj", "{D4958A8B-815B-421D-A988-2A4E8E2B582D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -29,10 +27,6 @@ Global
{D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Release|Any CPU.Build.0 = Release|Any CPU
{D4958A8B-815B-421D-A988-2A4E8E2B582D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4958A8B-815B-421D-A988-2A4E8E2B582D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4958A8B-815B-421D-A988-2A4E8E2B582D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4958A8B-815B-421D-A988-2A4E8E2B582D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -1,19 +1,19 @@
using System;
using System;
using RestSharp;
namespace FModel.Framework;
public class FRestRequest : RestRequest
{
private const int _timeout = 3 * 1000;
private const int TimeoutSeconds = 5;
public FRestRequest(string url, Method method = Method.Get) : base(url, method)
{
Timeout = _timeout;
Timeout = TimeSpan.FromSeconds(TimeoutSeconds);
}
public FRestRequest(Uri uri, Method method = Method.Get) : base(uri, method)
{
Timeout = _timeout;
Timeout = TimeSpan.FromSeconds(TimeoutSeconds);
}
}

View File

@ -38,8 +38,8 @@ public class FStatus : ViewModel
UpdateStatusLabel(label);
}
public void UpdateStatusLabel(string label)
public void UpdateStatusLabel(string label, string prefix = null)
{
Label = Kind == EStatusKind.Loading ? $"{Kind} {label}".Trim() : Kind.ToString();
Label = Kind == EStatusKind.Loading ? $"{prefix ?? Kind.ToString()} {label}".Trim() : Kind.ToString();
}
}

View File

@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Forms;
using FModel.Settings;
using ImGuiNET;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Windowing.Desktop;
@ -34,7 +37,6 @@ public class ImGuiController : IDisposable
private int _windowWidth;
private int _windowHeight;
// private string _iniPath;
public ImFontPtr FontNormal;
public ImFontPtr FontBold;
@ -49,7 +51,6 @@ public class ImGuiController : IDisposable
{
_windowWidth = width;
_windowHeight = height;
// _iniPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "imgui.ini");
int major = GL.GetInteger(GetPName.MajorVersion);
int minor = GL.GetInteger(GetPName.MinorVersion);
@ -58,9 +59,13 @@ public class ImGuiController : IDisposable
IntPtr context = ImGui.CreateContext();
ImGui.SetCurrentContext(context);
// ImGui.LoadIniSettingsFromDisk(_iniPath);
var io = ImGui.GetIO();
unsafe
{
var iniFileNamePtr = Marshal.StringToCoTaskMemUTF8(Path.Combine(UserSettings.Default.OutputDirectory, ".data", "imgui.ini"));
io.NativePtr->IniFilename = (byte*)iniFileNamePtr;
}
FontNormal = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", 16 * DpiScale);
FontBold = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeuib.ttf", 16 * DpiScale);
FontSemiBold = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\seguisb.ttf", 16 * DpiScale);
@ -71,7 +76,6 @@ public class ImGuiController : IDisposable
io.Fonts.Flags |= ImFontAtlasFlags.NoBakedLines;
CreateDeviceResources();
SetKeyMappings();
SetPerFrameImGuiData(1f / 60f);
@ -271,8 +275,8 @@ outputColor = color * texture(in_fontTexture, texCoord);
foreach (Keys key in Enum.GetValues(typeof(Keys)))
{
if (key == Keys.Unknown || io.KeyMap[(int) key] == -1) continue;
io.AddKeyEvent((ImGuiKey) io.KeyMap[(int) key], kState.IsKeyDown(key));
if (key == Keys.Unknown) continue;
io.AddKeyEvent(TranslateKey(key), kState.IsKeyDown(key));
}
foreach (var c in PressedChars)
@ -292,115 +296,6 @@ outputColor = color * texture(in_fontTexture, texCoord);
PressedChars.Add(keyChar);
}
private static void SetKeyMappings()
{
ImGuiIOPtr io = ImGui.GetIO();
io.KeyMap[(int)ImGuiKey.LeftShift] = (int)Keys.LeftShift;
io.KeyMap[(int)ImGuiKey.RightShift] = (int)Keys.RightShift;
io.KeyMap[(int)ImGuiKey.LeftCtrl] = (int)Keys.LeftControl;
io.KeyMap[(int)ImGuiKey.RightCtrl] = (int)Keys.RightControl;
io.KeyMap[(int)ImGuiKey.LeftAlt] = (int)Keys.LeftAlt;
io.KeyMap[(int)ImGuiKey.RightAlt] = (int)Keys.RightAlt;
io.KeyMap[(int)ImGuiKey.LeftSuper] = (int)Keys.LeftSuper;
io.KeyMap[(int)ImGuiKey.RightSuper] = (int)Keys.RightSuper;
io.KeyMap[(int)ImGuiKey.Menu] = (int)Keys.Menu;
io.KeyMap[(int)ImGuiKey.UpArrow] = (int)Keys.Up;
io.KeyMap[(int)ImGuiKey.DownArrow] = (int)Keys.Down;
io.KeyMap[(int)ImGuiKey.LeftArrow] = (int)Keys.Left;
io.KeyMap[(int)ImGuiKey.RightArrow] = (int)Keys.Right;
io.KeyMap[(int)ImGuiKey.Enter] = (int)Keys.Enter;
io.KeyMap[(int)ImGuiKey.Escape] = (int)Keys.Escape;
io.KeyMap[(int)ImGuiKey.Space] = (int)Keys.Space;
io.KeyMap[(int)ImGuiKey.Tab] = (int)Keys.Tab;
io.KeyMap[(int)ImGuiKey.Backspace] = (int)Keys.Backspace;
io.KeyMap[(int)ImGuiKey.Insert] = (int)Keys.Insert;
io.KeyMap[(int)ImGuiKey.Delete] = (int)Keys.Delete;
io.KeyMap[(int)ImGuiKey.PageUp] = (int)Keys.PageUp;
io.KeyMap[(int)ImGuiKey.PageDown] = (int)Keys.PageDown;
io.KeyMap[(int)ImGuiKey.Home] = (int)Keys.Home;
io.KeyMap[(int)ImGuiKey.End] = (int)Keys.End;
io.KeyMap[(int)ImGuiKey.CapsLock] = (int)Keys.CapsLock;
io.KeyMap[(int)ImGuiKey.ScrollLock] = (int)Keys.ScrollLock;
io.KeyMap[(int)ImGuiKey.PrintScreen] = (int)Keys.PrintScreen;
io.KeyMap[(int)ImGuiKey.Pause] = (int)Keys.Pause;
io.KeyMap[(int)ImGuiKey.NumLock] = (int)Keys.NumLock;
io.KeyMap[(int)ImGuiKey.KeypadDivide] = (int)Keys.KeyPadDivide;
io.KeyMap[(int)ImGuiKey.KeypadMultiply] = (int)Keys.KeyPadMultiply;
io.KeyMap[(int)ImGuiKey.KeypadSubtract] = (int)Keys.KeyPadSubtract;
io.KeyMap[(int)ImGuiKey.KeypadAdd] = (int)Keys.KeyPadAdd;
io.KeyMap[(int)ImGuiKey.KeypadDecimal] = (int)Keys.KeyPadDecimal;
io.KeyMap[(int)ImGuiKey.KeypadEnter] = (int)Keys.KeyPadEnter;
io.KeyMap[(int)ImGuiKey.GraveAccent] = (int)Keys.GraveAccent;
io.KeyMap[(int)ImGuiKey.Minus] = (int)Keys.Minus;
io.KeyMap[(int)ImGuiKey.Equal] = (int)Keys.Equal;
io.KeyMap[(int)ImGuiKey.LeftBracket] = (int)Keys.LeftBracket;
io.KeyMap[(int)ImGuiKey.RightBracket] = (int)Keys.RightBracket;
io.KeyMap[(int)ImGuiKey.Semicolon] = (int)Keys.Semicolon;
io.KeyMap[(int)ImGuiKey.Apostrophe] = (int)Keys.Apostrophe;
io.KeyMap[(int)ImGuiKey.Comma] = (int)Keys.Comma;
io.KeyMap[(int)ImGuiKey.Period] = (int)Keys.Period;
io.KeyMap[(int)ImGuiKey.Slash] = (int)Keys.Slash;
io.KeyMap[(int)ImGuiKey.Backslash] = (int)Keys.Backslash;
io.KeyMap[(int)ImGuiKey.F1] = (int)Keys.F1;
io.KeyMap[(int)ImGuiKey.F2] = (int)Keys.F2;
io.KeyMap[(int)ImGuiKey.F3] = (int)Keys.F3;
io.KeyMap[(int)ImGuiKey.F4] = (int)Keys.F4;
io.KeyMap[(int)ImGuiKey.F5] = (int)Keys.F5;
io.KeyMap[(int)ImGuiKey.F6] = (int)Keys.F6;
io.KeyMap[(int)ImGuiKey.F7] = (int)Keys.F7;
io.KeyMap[(int)ImGuiKey.F8] = (int)Keys.F8;
io.KeyMap[(int)ImGuiKey.F9] = (int)Keys.F9;
io.KeyMap[(int)ImGuiKey.F10] = (int)Keys.F10;
io.KeyMap[(int)ImGuiKey.F11] = (int)Keys.F11;
io.KeyMap[(int)ImGuiKey.F12] = (int)Keys.F12;
io.KeyMap[(int)ImGuiKey.Keypad0] = (int)Keys.KeyPad0;
io.KeyMap[(int)ImGuiKey.Keypad1] = (int)Keys.KeyPad1;
io.KeyMap[(int)ImGuiKey.Keypad2] = (int)Keys.KeyPad2;
io.KeyMap[(int)ImGuiKey.Keypad3] = (int)Keys.KeyPad3;
io.KeyMap[(int)ImGuiKey.Keypad4] = (int)Keys.KeyPad4;
io.KeyMap[(int)ImGuiKey.Keypad5] = (int)Keys.KeyPad5;
io.KeyMap[(int)ImGuiKey.Keypad6] = (int)Keys.KeyPad6;
io.KeyMap[(int)ImGuiKey.Keypad7] = (int)Keys.KeyPad7;
io.KeyMap[(int)ImGuiKey.Keypad8] = (int)Keys.KeyPad8;
io.KeyMap[(int)ImGuiKey.Keypad9] = (int)Keys.KeyPad9;
io.KeyMap[(int)ImGuiKey._0] = (int)Keys.D0;
io.KeyMap[(int)ImGuiKey._1] = (int)Keys.D1;
io.KeyMap[(int)ImGuiKey._2] = (int)Keys.D2;
io.KeyMap[(int)ImGuiKey._3] = (int)Keys.D3;
io.KeyMap[(int)ImGuiKey._4] = (int)Keys.D4;
io.KeyMap[(int)ImGuiKey._5] = (int)Keys.D5;
io.KeyMap[(int)ImGuiKey._6] = (int)Keys.D6;
io.KeyMap[(int)ImGuiKey._7] = (int)Keys.D7;
io.KeyMap[(int)ImGuiKey._8] = (int)Keys.D8;
io.KeyMap[(int)ImGuiKey._9] = (int)Keys.D9;
io.KeyMap[(int)ImGuiKey.A] = (int)Keys.A;
io.KeyMap[(int)ImGuiKey.B] = (int)Keys.B;
io.KeyMap[(int)ImGuiKey.C] = (int)Keys.C;
io.KeyMap[(int)ImGuiKey.D] = (int)Keys.D;
io.KeyMap[(int)ImGuiKey.E] = (int)Keys.E;
io.KeyMap[(int)ImGuiKey.F] = (int)Keys.F;
io.KeyMap[(int)ImGuiKey.G] = (int)Keys.G;
io.KeyMap[(int)ImGuiKey.H] = (int)Keys.H;
io.KeyMap[(int)ImGuiKey.I] = (int)Keys.I;
io.KeyMap[(int)ImGuiKey.J] = (int)Keys.J;
io.KeyMap[(int)ImGuiKey.K] = (int)Keys.K;
io.KeyMap[(int)ImGuiKey.L] = (int)Keys.L;
io.KeyMap[(int)ImGuiKey.M] = (int)Keys.M;
io.KeyMap[(int)ImGuiKey.N] = (int)Keys.N;
io.KeyMap[(int)ImGuiKey.O] = (int)Keys.O;
io.KeyMap[(int)ImGuiKey.P] = (int)Keys.P;
io.KeyMap[(int)ImGuiKey.Q] = (int)Keys.Q;
io.KeyMap[(int)ImGuiKey.R] = (int)Keys.R;
io.KeyMap[(int)ImGuiKey.S] = (int)Keys.S;
io.KeyMap[(int)ImGuiKey.T] = (int)Keys.T;
io.KeyMap[(int)ImGuiKey.U] = (int)Keys.U;
io.KeyMap[(int)ImGuiKey.V] = (int)Keys.V;
io.KeyMap[(int)ImGuiKey.W] = (int)Keys.W;
io.KeyMap[(int)ImGuiKey.X] = (int)Keys.X;
io.KeyMap[(int)ImGuiKey.Y] = (int)Keys.Y;
io.KeyMap[(int)ImGuiKey.Z] = (int)Keys.Z;
}
private void RenderImDrawData(ImDrawDataPtr draw_data)
{
if (draw_data.CmdListsCount == 0)
@ -440,7 +335,7 @@ outputColor = color * texture(in_fontTexture, texCoord);
GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBuffer);
for (int i = 0; i < draw_data.CmdListsCount; i++)
{
ImDrawListPtr cmd_list = draw_data.CmdListsRange[i];
ImDrawListPtr cmd_list = draw_data.CmdLists[i];
int vertexSize = cmd_list.VtxBuffer.Size * Unsafe.SizeOf<ImDrawVert>();
if (vertexSize > _vertexBufferSize)
@ -490,7 +385,7 @@ outputColor = color * texture(in_fontTexture, texCoord);
// Render command lists
for (int n = 0; n < draw_data.CmdListsCount; n++)
{
ImDrawListPtr cmd_list = draw_data.CmdListsRange[n];
ImDrawListPtr cmd_list = draw_data.CmdLists[n];
GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, cmd_list.VtxBuffer.Size * Unsafe.SizeOf<ImDrawVert>(), cmd_list.VtxBuffer.Data);
CheckGLError($"Data Vert {n}");
@ -643,4 +538,71 @@ outputColor = color * texture(in_fontTexture, texCoord);
{
return Math.Max((float)(Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth), (float)(Screen.PrimaryScreen.Bounds.Height / SystemParameters.PrimaryScreenHeight));
}
public static ImGuiKey TranslateKey(Keys key)
{
if (key is >= Keys.D0 and <= Keys.D9)
return key - Keys.D0 + ImGuiKey._0;
if (key is >= Keys.A and <= Keys.Z)
return key - Keys.A + ImGuiKey.A;
if (key is >= Keys.KeyPad0 and <= Keys.KeyPad9)
return key - Keys.KeyPad0 + ImGuiKey.Keypad0;
if (key is >= Keys.F1 and <= Keys.F24)
return key - Keys.F1 + ImGuiKey.F24;
return key switch
{
Keys.Tab => ImGuiKey.Tab,
Keys.Left => ImGuiKey.LeftArrow,
Keys.Right => ImGuiKey.RightArrow,
Keys.Up => ImGuiKey.UpArrow,
Keys.Down => ImGuiKey.DownArrow,
Keys.PageUp => ImGuiKey.PageUp,
Keys.PageDown => ImGuiKey.PageDown,
Keys.Home => ImGuiKey.Home,
Keys.End => ImGuiKey.End,
Keys.Insert => ImGuiKey.Insert,
Keys.Delete => ImGuiKey.Delete,
Keys.Backspace => ImGuiKey.Backspace,
Keys.Space => ImGuiKey.Space,
Keys.Enter => ImGuiKey.Enter,
Keys.Escape => ImGuiKey.Escape,
Keys.Apostrophe => ImGuiKey.Apostrophe,
Keys.Comma => ImGuiKey.Comma,
Keys.Minus => ImGuiKey.Minus,
Keys.Period => ImGuiKey.Period,
Keys.Slash => ImGuiKey.Slash,
Keys.Semicolon => ImGuiKey.Semicolon,
Keys.Equal => ImGuiKey.Equal,
Keys.LeftBracket => ImGuiKey.LeftBracket,
Keys.Backslash => ImGuiKey.Backslash,
Keys.RightBracket => ImGuiKey.RightBracket,
Keys.GraveAccent => ImGuiKey.GraveAccent,
Keys.CapsLock => ImGuiKey.CapsLock,
Keys.ScrollLock => ImGuiKey.ScrollLock,
Keys.NumLock => ImGuiKey.NumLock,
Keys.PrintScreen => ImGuiKey.PrintScreen,
Keys.Pause => ImGuiKey.Pause,
Keys.KeyPadDecimal => ImGuiKey.KeypadDecimal,
Keys.KeyPadDivide => ImGuiKey.KeypadDivide,
Keys.KeyPadMultiply => ImGuiKey.KeypadMultiply,
Keys.KeyPadSubtract => ImGuiKey.KeypadSubtract,
Keys.KeyPadAdd => ImGuiKey.KeypadAdd,
Keys.KeyPadEnter => ImGuiKey.KeypadEnter,
Keys.KeyPadEqual => ImGuiKey.KeypadEqual,
Keys.LeftShift => ImGuiKey.LeftShift,
Keys.LeftControl => ImGuiKey.LeftCtrl,
Keys.LeftAlt => ImGuiKey.LeftAlt,
Keys.LeftSuper => ImGuiKey.LeftSuper,
Keys.RightShift => ImGuiKey.RightShift,
Keys.RightControl => ImGuiKey.RightCtrl,
Keys.RightAlt => ImGuiKey.RightAlt,
Keys.RightSuper => ImGuiKey.RightSuper,
Keys.Menu => ImGuiKey.Menu,
_ => ImGuiKey.None
};
}
}

View File

@ -1,30 +1,32 @@
using System;
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Windows;
namespace FModel;
public static class Helper
{
[StructLayout(LayoutKind.Explicit)]
private struct NanUnion
{
[FieldOffset(0)]
internal double DoubleValue;
[FieldOffset(0)]
internal readonly ulong UlongValue;
}
public static string FixKey(string key)
{
if (string.IsNullOrEmpty(key))
return string.Empty;
if (key.StartsWith("0x"))
key = key[2..];
var keySpan = key.AsSpan().Trim();
if (keySpan.Length > sizeof(char) * (2 /* 0x */ + 32 /* FAES = 256 bit */)) // maybe strictly check for length?
return string.Empty; // bullshit key
return "0x" + key.ToUpper().Trim();
Span<char> resultSpan = stackalloc char[keySpan.Length + 2 /* pad for 0x */];
keySpan.ToUpperInvariant(resultSpan[2..]);
if (resultSpan[2..].StartsWith("0X"))
resultSpan = resultSpan[2..];
else
resultSpan[0] = '0';
resultSpan[1] = 'x';
return new string(resultSpan);
}
public static void OpenWindow<T>(string windowName, Action action) where T : Window
@ -74,9 +76,9 @@ public static class Helper
public static bool IsNaN(double value)
{
var t = new NanUnion { DoubleValue = value };
var exp = t.UlongValue & 0xfff0000000000000;
var man = t.UlongValue & 0x000fffffffffffff;
var ulongValue = Unsafe.As<double, ulong>(ref value);
var exp = ulongValue & 0xfff0000000000000;
var man = ulongValue & 0x000fffffffffffff;
return exp is 0x7ff0000000000000 or 0xfff0000000000000 && man != 0;
}
@ -96,13 +98,17 @@ public static class Helper
return -d < n && d > n;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float DegreesToRadians(float degrees)
{
return MathF.PI / 180f * degrees;
const float ratio = MathF.PI / 180f;
return ratio * degrees;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float RadiansToDegrees(float radians)
{
return radians* 180f / MathF.PI;
const float ratio = 180f / MathF.PI;
return radians * ratio;
}
}

View File

@ -94,7 +94,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Directories" ItemsSource="{Binding CustomDirectories.Directories}" IsEnabled="{Binding Status.IsReady}">
<MenuItem Header="Favorite Directories" ItemsSource="{Binding CustomDirectories.Directories}" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -126,25 +126,6 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Map Viewer" Command="{Binding MenuCommand}" CommandParameter="Views_MapViewer" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource MapIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
<MenuItem.Style>
<Style TargetType="MenuItem" BasedOn="{StaticResource {x:Type MenuItem}}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding CUE4Parse.Game}" Value="{x:Static local:FGame.FortniteGame}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
<MenuItem Header="Image Merger" Command="{Binding MenuCommand}" CommandParameter="Views_ImageMerger">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
@ -166,11 +147,11 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Changelog" Command="{Binding MenuCommand}" CommandParameter="Help_Changelog">
<MenuItem Header="Releases" Command="{Binding MenuCommand}" CommandParameter="Help_Releases">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource NoteIcon}" />
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource GitHubIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
@ -340,16 +321,16 @@
<TreeView Grid.Row="2" x:Name="AssetsFolderName" Style="{StaticResource AssetsFolderTreeView}" PreviewMouseDoubleClick="OnAssetsTreeMouseDoubleClick">
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Extract Folder's Packages" Click="OnFolderExtractClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ExtractIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<!-- <MenuItem Header="Extract Folder's Packages" Click="OnFolderExtractClick"> -->
<!-- <MenuItem.Icon> -->
<!-- <Viewbox Width="16" Height="16"> -->
<!-- <Canvas Width="24" Height="24"> -->
<!-- <Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ExtractIcon}" /> -->
<!-- </Canvas> -->
<!-- </Viewbox> -->
<!-- </MenuItem.Icon> -->
<!-- </MenuItem> -->
<!-- <Separator /> -->
<MenuItem Header="Export Folder's Packages Raw Data (.uasset)" Click="OnFolderExportClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
@ -368,7 +349,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Textures (.png)" Click="OnFolderTextureClick">
<MenuItem Header="Save Folder's Packages Textures" Click="OnFolderTextureClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -377,7 +358,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Models (.psk)" Click="OnFolderModelClick">
<MenuItem Header="Save Folder's Packages Models" Click="OnFolderModelClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -386,7 +367,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Animations (.psa)" Click="OnFolderAnimationClick">
<MenuItem Header="Save Folder's Packages Animations" Click="OnFolderAnimationClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -396,7 +377,7 @@
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Save Directory" Click="OnSaveDirectoryClick">
<MenuItem Header="Favorite Directory" Click="OnFavoriteDirectoryClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -536,7 +517,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Texture (.png)" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem Header="Save Texture" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Textures" />
@ -551,7 +532,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Model (.psk)" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem Header="Save Model" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Models" />
@ -566,7 +547,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Animation (.psa)" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem Header="Save Animation" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Animations" />
@ -816,13 +797,17 @@
<Style TargetType="StatusBarItem">
<Style.Triggers>
<DataTrigger Binding="{Binding IsAutoOpenSounds, Source={x:Static settings:UserSettings.Default}}" Value="False">
<Setter Property="Visibility" Value="Hidden" />
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</StatusBarItem.Style>
<TextBlock HorizontalAlignment="Center" FontWeight="SemiBold" Text="SND" />
</StatusBarItem>
<StatusBarItem Margin="10 0 0 0">
<TextBlock Text="{Binding LastUpdateCheck, Source={x:Static local:Settings.UserSettings.Default}, Converter={x:Static converters:RelativeDateTimeConverter.Instance}, StringFormat=Last Refresh: {0}}" />
</StatusBarItem>
</StackPanel>
</StatusBarItem>
</StatusBar>

View File

@ -40,7 +40,6 @@ public partial class MainWindow
private void OnClosing(object sender, CancelEventArgs e)
{
_applicationView.CustomDirectories.Save();
_discordHandler.Dispose();
}
@ -48,7 +47,7 @@ public partial class MainWindow
{
var newOrUpdated = UserSettings.Default.ShowChangelog;
#if !DEBUG
ApplicationService.ApiEndpointView.FModelApi.CheckForUpdates(UserSettings.Default.UpdateMode);
ApplicationService.ApiEndpointView.FModelApi.CheckForUpdates(true);
#endif
switch (UserSettings.Default.AesReload)
@ -56,12 +55,14 @@ public partial class MainWindow
case EAesReload.Always:
await _applicationView.CUE4Parse.RefreshAes();
break;
case EAesReload.OncePerDay when UserSettings.Default.LastAesReload != DateTime.Today:
UserSettings.Default.LastAesReload = DateTime.Today;
case EAesReload.OncePerDay when UserSettings.Default.CurrentDir.LastAesReload != DateTime.Today:
UserSettings.Default.CurrentDir.LastAesReload = DateTime.Today;
await _applicationView.CUE4Parse.RefreshAes();
break;
}
await ApplicationViewModel.InitOodle();
await ApplicationViewModel.InitZlib();
await _applicationView.CUE4Parse.Initialize();
await _applicationView.AesManager.InitAes();
await _applicationView.UpdateProvider(true);
@ -70,12 +71,10 @@ public partial class MainWindow
#endif
await Task.WhenAll(
_applicationView.CUE4Parse.VerifyConsoleVariables(),
_applicationView.CUE4Parse.VerifyVirtualCache(),
_applicationView.CUE4Parse.VerifyContentBuildManifest(),
_applicationView.CUE4Parse.VerifyOnDemandArchives(),
_applicationView.CUE4Parse.InitMappings(),
_applicationView.InitImGuiSettings(newOrUpdated),
_applicationView.InitVgmStream(),
_applicationView.InitOodle(),
ApplicationViewModel.InitVgmStream(),
ApplicationViewModel.InitImGuiSettings(newOrUpdated),
Task.Run(() =>
{
if (UserSettings.Default.DiscordRpc == EDiscordRpc.Always)
@ -86,7 +85,10 @@ public partial class MainWindow
#if DEBUG
// await _threadWorkerView.Begin(cancellationToken =>
// _applicationView.CUE4Parse.Extract(cancellationToken,
// "fortnitegame/Content/Characters/Player/Female/Medium/Bodies/F_MED_Ballerina/Meshes/F_MED_Ballerina.uasset"));
// "FortniteGame/Content/Athena/Apollo/Maps/UI/Apollo_Terrain_Minimap.uasset"));
// await _threadWorkerView.Begin(cancellationToken =>
// _applicationView.CUE4Parse.Extract(cancellationToken,
// "FortniteGame/Content/Environments/Helios/Props/GlacierHotel/GlacierHotel_Globe_A/Meshes/SM_GlacierHotel_Globe_A.uasset"));
#endif
}
@ -140,7 +142,7 @@ public partial class MainWindow
private async void OnMappingsReload(object sender, ExecutedRoutedEventArgs e)
{
await _applicationView.CUE4Parse.InitMappings();
await _applicationView.CUE4Parse.InitMappings(true);
}
private void OnOpenAvalonFinder()
@ -228,13 +230,13 @@ public partial class MainWindow
}
}
private void OnSaveDirectoryClick(object sender, RoutedEventArgs e)
private void OnFavoriteDirectoryClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is not TreeItem folder) return;
_applicationView.CustomDirectories.Add(new CustomDirectory(folder.Header, folder.PathAtThisPoint));
FLogger.Append(ELog.Information, () =>
FLogger.Text($"Successfully saved '{folder.PathAtThisPoint}' as a new custom directory", Constants.WHITE, true));
FLogger.Text($"Successfully saved '{folder.PathAtThisPoint}' as a new favorite directory", Constants.WHITE, true));
}
private void OnCopyDirectoryPathClick(object sender, RoutedEventArgs e)

View File

@ -0,0 +1,11 @@
#version 460 core
in vec3 fPos;
in vec3 fColor;
out vec4 FragColor;
void main()
{
FragColor = vec4(fColor, 1.0);
}

View File

@ -0,0 +1,19 @@
#version 460 core
layout (location = 0) in vec3 vPos;
layout (location = 1) in vec3 vColor;
uniform mat4 uView;
uniform mat4 uProjection;
uniform mat4 uInstanceMatrix;
out vec3 fPos;
out vec3 fColor;
void main()
{
gl_PointSize = 7.5f;
gl_Position = uProjection * uView * uInstanceMatrix * vec4(vPos, 1.0);
fPos = vec3(uInstanceMatrix * vec4(vPos, 1.0));
fColor = vColor;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

@ -0,0 +1,20 @@
#version 460 core
layout (location = 0) in vec3 vPos;
uniform mat4 uView;
uniform mat4 uProjection;
uniform mat4 uInstanceMatrix;
uniform mat4 uCollisionMatrix;
uniform float uScaleDown;
out vec3 fPos;
out vec3 fColor;
void main()
{
gl_PointSize = 7.5f;
gl_Position = uProjection * uView * uInstanceMatrix * uCollisionMatrix * vec4(vPos.xzy * uScaleDown, 1.0);
fPos = vec3(uInstanceMatrix * uCollisionMatrix * vec4(vPos.xzy * uScaleDown, 1.0));
fColor = vec3(1.0);
}

BIN
FModel/Resources/cube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -8,7 +8,7 @@ in vec3 fPos;
in vec3 fNormal;
in vec3 fTangent;
in vec2 fTexCoords;
in float fTexLayer;
flat in int fTexLayer;
in vec4 fColor;
struct Texture
@ -89,6 +89,7 @@ uniform Parameters uParameters;
uniform Light uLights[MAX_LIGHT_COUNT];
uniform int uNumLights;
uniform int uUvCount;
uniform float uOpacity;
uniform bool uHasVertexColors;
uniform vec3 uSectionColor;
uniform bool bVertexColors[6];
@ -98,7 +99,7 @@ out vec4 FragColor;
int LayerToIndex()
{
return clamp(int(fTexLayer), 0, uUvCount - 1);
return clamp(fTexLayer, 0, uUvCount - 1);
}
vec4 SamplerToVector(sampler2D s, vec2 coords)
@ -148,6 +149,11 @@ vec3 CalcLight(int layer, vec3 normals, vec3 position, vec3 color, float attenua
{
vec3 fLambert = SamplerToVector(uParameters.Diffuse[layer].Sampler).rgb * uParameters.Diffuse[layer].Color.rgb;
vec3 specular_masks = SamplerToVector(uParameters.SpecularMasks[layer].Sampler).rgb;
float cavity = specular_masks.g;
if (uParameters.HasAo)
{
cavity = SamplerToVector(uParameters.Ao.Sampler).g;
}
float roughness = mix(uParameters.RoughnessMin, uParameters.RoughnessMax, specular_masks.b);
vec3 l = normalize(uViewPos - fPos);
@ -165,7 +171,7 @@ vec3 CalcLight(int layer, vec3 normals, vec3 position, vec3 color, float attenua
vec3 kS = f;
vec3 kD = 1.0 - kS;
kD *= 1.0 - specular_masks.g;
kD *= 1.0 - cavity;
vec3 specBrdfNom = ggxDistribution(roughness, nDotH) * geomSmith(roughness, nDotL) * geomSmith(roughness, nDotV) * f;
float specBrdfDenom = 4.0 * nDotV * nDotL + 0.0001;
@ -212,30 +218,32 @@ vec3 CalcSpotLight(int layer, vec3 normals, Light light)
void main()
{
int layer = LayerToIndex();
vec3 normals = ComputeNormals(layer);
vec3 lightDir = normalize(uViewPos - fPos);
float diffuseFactor = max(dot(normals, lightDir), 0.4);
if (bVertexColors[1])
{
FragColor = vec4(uSectionColor, 1.0);
FragColor = vec4(diffuseFactor * uSectionColor, uOpacity);
}
else if (bVertexColors[2] && uHasVertexColors)
{
FragColor = fColor;
FragColor = vec4(diffuseFactor * fColor.rgb, fColor.a);
}
else if (bVertexColors[3])
{
int layer = LayerToIndex();
vec3 normals = ComputeNormals(layer);
FragColor = vec4(normals, 1.0);
FragColor = vec4(normals, uOpacity);
}
else if (bVertexColors[4])
{
FragColor = vec4(fTexCoords, 0.0, 1.0);
vec4 diffuse = SamplerToVector(uParameters.Diffuse[0].Sampler);
FragColor = vec4(diffuseFactor * diffuse.rgb, diffuse.a);
}
else
{
int layer = LayerToIndex();
vec3 normals = ComputeNormals(layer);
vec4 diffuse = SamplerToVector(uParameters.Diffuse[layer].Sampler);
vec3 result = uParameters.Diffuse[layer].Color.rgb * diffuse.rgb;
vec3 result = diffuseFactor * diffuse.rgb * uParameters.Diffuse[layer].Color.rgb;
if (uParameters.HasAo)
{
@ -245,7 +253,7 @@ void main()
vec3 color = uParameters.Ao.ColorBoost.Color * uParameters.Ao.ColorBoost.Exponent;
result = mix(result, result * color, m.b);
}
result = vec3(uParameters.Ao.AmbientOcclusion) * result * m.r;
result *= m.r;
}
vec2 coords = fTexCoords;
@ -263,7 +271,7 @@ void main()
}
{
result += CalcLight(layer, normals, uViewPos, vec3(0.75), 1.0, false);
result += CalcLight(layer, normals, uViewPos, vec3(1.0), 1.0, false);
vec3 lights = vec3(uNumLights > 0 ? 0 : 1);
for (int i = 0; i < uNumLights; i++)
@ -281,6 +289,6 @@ void main()
}
result = result / (result + vec3(1.0));
FragColor = vec4(pow(result, vec3(1.0 / 2.2)), 1.0);
FragColor = vec4(pow(result, vec3(1.0 / 2.2)), uOpacity);
}
}

View File

@ -4,10 +4,10 @@ layout (location = 1) in vec3 vPos;
layout (location = 2) in vec3 vNormal;
layout (location = 3) in vec3 vTangent;
layout (location = 4) in vec2 vTexCoords;
layout (location = 5) in float vTexLayer;
layout (location = 6) in vec4 vColor;
layout (location = 7) in vec4 vBoneIds;
layout (location = 8) in vec4 vBoneWeights;
layout (location = 5) in int vTexLayer;
layout (location = 6) in float vColor;
layout (location = 7) in vec4 vBoneInfluence;
layout (location = 8) in vec4 vBoneInfluenceExtra;
layout (location = 9) in mat4 vInstanceMatrix;
layout (location = 13) in vec3 vMorphTargetPos;
layout (location = 14) in vec3 vMorphTargetTangent;
@ -16,18 +16,37 @@ layout(std430, binding = 1) buffer BoneMatrices
{
mat4 uFinalBonesMatrix[];
};
layout(std430, binding = 2) buffer RestBoneMatrices
{
mat4 uRestBonesMatrix[];
};
uniform mat4 uView;
uniform mat4 uProjection;
uniform float uMorphTime;
uniform bool uIsAnimated;
out vec3 fPos;
out vec3 fNormal;
out vec3 fTangent;
out vec2 fTexCoords;
out float fTexLayer;
flat out int fTexLayer;
out vec4 fColor;
vec4 unpackARGB(int color)
{
float a = float((color >> 24) & 0xFF);
float r = float((color >> 16) & 0xFF);
float g = float((color >> 8) & 0xFF);
float b = float((color >> 0) & 0xFF);
return vec4(r, g, b, a);
}
vec2 unpackBoneIDsAndWeights(int packedData)
{
return vec2(float((packedData >> 16) & 0xFFFF), float(packedData & 0xFFFF));
}
void main()
{
vec4 bindPos = vec4(mix(vPos, vMorphTargetPos, uMorphTime), 1.0);
@ -37,21 +56,30 @@ void main()
vec4 finalPos = vec4(0.0);
vec4 finalNormal = vec4(0.0);
vec4 finalTangent = vec4(0.0);
if (vBoneIds != vBoneWeights)
if (uIsAnimated)
{
for(int i = 0 ; i < 4; i++)
vec4 boneInfluences[2];
boneInfluences[0] = vBoneInfluence;
boneInfluences[1] = vBoneInfluenceExtra;
for (int i = 0; i < 2; i++)
{
int boneIndex = int(vBoneIds[i]);
if(boneIndex < 0) break;
for(int j = 0 ; j < 4; j++)
{
vec2 boneInfluence = unpackBoneIDsAndWeights(int(boneInfluences[i][j]));
int boneIndex = int(boneInfluence.x);
float weight = boneInfluence.y;
mat4 boneMatrix = uFinalBonesMatrix[boneIndex];
mat4 inverseBoneMatrix = transpose(inverse(boneMatrix));
float weight = vBoneWeights[i];
mat4 boneMatrix = uFinalBonesMatrix[boneIndex] * inverse(uRestBonesMatrix[boneIndex]);
mat4 inverseBoneMatrix = transpose(inverse(boneMatrix));
finalPos += boneMatrix * bindPos * weight;
finalNormal += inverseBoneMatrix * bindNormal * weight;
finalTangent += inverseBoneMatrix * bindTangent * weight;
finalPos += boneMatrix * bindPos * weight;
finalNormal += inverseBoneMatrix * bindNormal * weight;
finalTangent += inverseBoneMatrix * bindTangent * weight;
}
}
finalPos = normalize(finalPos);
finalNormal = normalize(finalNormal);
finalTangent = normalize(finalTangent);
}
else
{
@ -67,5 +95,5 @@ void main()
fTangent = vec3(transpose(inverse(vInstanceMatrix)) * finalTangent);
fTexCoords = vTexCoords;
fTexLayer = vTexLayer;
fColor = vColor;
fColor = unpackARGB(int(vColor)) / 255.0;
}

BIN
FModel/Resources/light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -2,8 +2,8 @@
layout (location = 1) in vec3 vPos;
layout (location = 2) in vec3 vNormal;
layout (location = 7) in vec4 vBoneIds;
layout (location = 8) in vec4 vBoneWeights;
layout (location = 7) in vec4 vBoneInfluence;
layout (location = 8) in vec4 vBoneInfluenceExtra;
layout (location = 9) in mat4 vInstanceMatrix;
layout (location = 13) in vec3 vMorphTargetPos;
@ -11,31 +11,55 @@ layout(std430, binding = 1) buffer BoneMatrices
{
mat4 uFinalBonesMatrix[];
};
layout(std430, binding = 2) buffer RestBoneMatrices
{
mat4 uRestBonesMatrix[];
};
uniform mat4 uView;
uniform vec3 uViewPos;
uniform mat4 uProjection;
uniform float uMorphTime;
uniform bool uIsAnimated;
vec2 unpackBoneIDsAndWeights(int packedData)
{
return vec2(float((packedData >> 16) & 0xFFFF), float(packedData & 0xFFFF));
}
vec4 calculateScale(vec4 bindPos, vec4 bindNormal)
{
vec4 worldPos = vInstanceMatrix * bindPos;
float scaleFactor = length(uViewPos - worldPos.xyz) * 0.0035;
return transpose(inverse(vInstanceMatrix)) * bindNormal * scaleFactor;
}
void main()
{
vec4 bindPos = vec4(mix(vPos, vMorphTargetPos, uMorphTime), 1.0);
vec4 bindNormal = vec4(vNormal, 1.0);
bindPos.xyz += calculateScale(bindPos, bindNormal).xyz;
vec4 finalPos = vec4(0.0);
vec4 finalNormal = vec4(0.0);
if (vBoneIds != vBoneWeights)
if (uIsAnimated)
{
for(int i = 0 ; i < 4; i++)
vec4 boneInfluences[2];
boneInfluences[0] = vBoneInfluence;
boneInfluences[1] = vBoneInfluenceExtra;
for(int i = 0 ; i < 2; i++)
{
int boneIndex = int(vBoneIds[i]);
if(boneIndex < 0) break;
for(int j = 0; j < 4; j++)
{
vec2 boneInfluence = unpackBoneIDsAndWeights(int(boneInfluences[i][j]));
int boneIndex = int(boneInfluence.x);
float weight = boneInfluence.y;
mat4 boneMatrix = uFinalBonesMatrix[boneIndex];
float weight = vBoneWeights[i];
mat4 boneMatrix = uFinalBonesMatrix[boneIndex] * inverse(uRestBonesMatrix[boneIndex]);
finalPos += boneMatrix * bindPos * weight;
finalNormal += transpose(inverse(boneMatrix)) * bindNormal * weight;
finalPos += boneMatrix * bindPos * weight;
finalNormal += transpose(inverse(boneMatrix)) * bindNormal * weight;
}
}
}
else
@ -44,10 +68,5 @@ void main()
finalNormal = bindNormal;
}
finalPos = vInstanceMatrix * finalPos;
float scaleFactor = distance(vec3(finalPos), uViewPos) * 0.0035;
vec4 nor = transpose(inverse(vInstanceMatrix)) * normalize(finalNormal) * scaleFactor;
finalPos.xyz += nor.xyz;
gl_Position = uProjection * uView * finalPos;
gl_Position = uProjection * uView * vInstanceMatrix * finalPos;
}

View File

@ -1,8 +1,8 @@
#version 460 core
layout (location = 1) in vec3 vPos;
layout (location = 7) in vec4 vBoneIds;
layout (location = 8) in vec4 vBoneWeights;
layout (location = 7) in vec4 vBoneInfluence;
layout (location = 8) in vec4 vBoneInfluenceExtra;
layout (location = 9) in mat4 vInstanceMatrix;
layout (location = 13) in vec3 vMorphTargetPos;
@ -10,24 +10,41 @@ layout(std430, binding = 1) buffer BoneMatrices
{
mat4 uFinalBonesMatrix[];
};
layout(std430, binding = 2) buffer RestBoneMatrices
{
mat4 uRestBonesMatrix[];
};
uniform mat4 uView;
uniform mat4 uProjection;
uniform float uMorphTime;
uniform bool uIsAnimated;
vec2 unpackBoneIDsAndWeights(int packedData)
{
return vec2(float((packedData >> 16) & 0xFFFF), float(packedData & 0xFFFF));
}
void main()
{
vec4 bindPos = vec4(mix(vPos, vMorphTargetPos, uMorphTime), 1.0);
vec4 finalPos = vec4(0.0);
if (vBoneIds != vBoneWeights)
if (uIsAnimated)
{
for(int i = 0 ; i < 4; i++)
vec4 boneInfluences[2];
boneInfluences[0] = vBoneInfluence;
boneInfluences[1] = vBoneInfluenceExtra;
for(int i = 0 ; i < 2; i++)
{
int boneIndex = int(vBoneIds[i]);
if(boneIndex < 0) break;
for(int j = 0; j < 4; j++)
{
vec2 boneInfluence = unpackBoneIDsAndWeights(int(boneInfluences[i][j]));
int boneIndex = int(boneInfluence.x);
float weight = boneInfluence.y;
finalPos += uFinalBonesMatrix[boneIndex] * bindPos * vBoneWeights[i];
finalPos += uFinalBonesMatrix[boneIndex] * inverse(uRestBonesMatrix[boneIndex]) * bindPos * weight;
}
}
}
else finalPos = bindPos;

BIN
FModel/Resources/square.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -41,14 +41,14 @@ namespace FModel.Services
Details = $"{gameName} - Idling"
};
_client.OnReady += (_, args) => Log.Information("{Username}#{Discriminator} ({UserId}) is now ready", args.User.Username, args.User.Discriminator, args.User.ID);
_client.OnReady += (_, args) => Log.Information("@{Username} ({UserId}) is now ready", args.User.Username, args.User.ID);
_client.SetPresence(_currentPresence);
_client.Initialize();
}
public void UpdatePresence(CUE4ParseViewModel viewModel) =>
UpdatePresence(
$"{viewModel.Provider.GameDisplayName ?? viewModel.Provider.GameName} - {viewModel.Provider.MountedVfs.Count}/{viewModel.Provider.MountedVfs.Count + viewModel.Provider.UnloadedVfs.Count} Packages",
$"{viewModel.Provider.GameDisplayName ?? viewModel.Provider.InternalGameName} - {viewModel.Provider.MountedVfs.Count}/{viewModel.Provider.MountedVfs.Count + viewModel.Provider.UnloadedVfs.Count} Packages",
$"Mode: {UserSettings.Default.LoadingMode.GetDescription()} - {viewModel.SearchVm.ResultsCount:### ### ###} Loaded Assets".Trim());
public void UpdatePresence(string details, string state)

View File

@ -0,0 +1,65 @@
using System.Collections.Generic;
using FModel.Framework;
namespace FModel.Settings;
public class CustomDirectory : ViewModel
{
public static IList<CustomDirectory> Default(string gameName)
{
switch (gameName)
{
case "Fortnite":
case "Fortnite [LIVE]":
return new List<CustomDirectory>
{
new("Cosmetics", "FortniteGame/Plugins/GameFeatures/BRCosmetics/Content/Athena/Items/Cosmetics/"),
new("Emotes [AUDIO]", "FortniteGame/Plugins/GameFeatures/BRCosmetics/Content/Athena/Sounds/Emotes/"),
new("Music Packs [AUDIO]", "FortniteGame/Plugins/GameFeatures/BRCosmetics/Content/Athena/Sounds/MusicPacks/"),
new("Weapons", "FortniteGame/Content/Athena/Items/Weapons/"),
new("Strings", "FortniteGame/Content/Localization/")
};
case "VALORANT":
case "VALORANT [LIVE]":
return new List<CustomDirectory>
{
new("Audio", "ShooterGame/Content/WwiseAudio/Media/"),
new("Characters", "ShooterGame/Content/Characters/"),
new("Gun Buddies", "ShooterGame/Content/Equippables/Buddies/"),
new("Cards and Sprays", "ShooterGame/Content/Personalization/"),
new("Shop Backgrounds", "ShooterGame/Content/UI/OutOfGame/MainMenu/Store/Shared/Textures/"),
new("Weapon Renders", "ShooterGame/Content/UI/Screens/OutOfGame/MainMenu/Collection/Assets/Large/")
};
default:
return new List<CustomDirectory>();
}
}
private string _header;
public string Header
{
get => _header;
set => SetProperty(ref _header, value);
}
private string _directoryPath;
public string DirectoryPath
{
get => _directoryPath;
set => SetProperty(ref _directoryPath, value);
}
public CustomDirectory()
{
Header = string.Empty;
DirectoryPath = string.Empty;
}
public CustomDirectory(string header, string path)
{
Header = header;
DirectoryPath = path;
}
public override string ToString() => Header;
}

View File

@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Versions;
using FModel.Framework;
using FModel.ViewModels.ApiEndpoints.Models;
namespace FModel.Settings;
public class DirectorySettings : ViewModel, ICloneable
{
public static DirectorySettings Default(
string gameName, string gameDir, bool manual = false, EGame ue = EGame.GAME_UE4_LATEST, string aes = "")
{
UserSettings.Default.PerDirectory.TryGetValue(gameDir, out var old);
return new DirectorySettings
{
GameName = gameName,
GameDirectory = gameDir,
IsManual = manual,
UeVersion = old?.UeVersion ?? ue,
TexturePlatform = old?.TexturePlatform ?? ETexturePlatform.DesktopMobile,
Versioning = old?.Versioning ?? new VersioningSettings(),
Endpoints = old?.Endpoints ?? EndpointSettings.Default(gameName),
Directories = old?.Directories ?? CustomDirectory.Default(gameName),
AesKeys = old?.AesKeys ?? new AesResponse { MainKey = aes, DynamicKeys = null },
LastAesReload = old?.LastAesReload ?? DateTime.Today.AddDays(-1)
};
}
private string _gameName;
public string GameName
{
get => _gameName;
set => SetProperty(ref _gameName, value);
}
private string _gameDirectory;
public string GameDirectory
{
get => _gameDirectory;
set => SetProperty(ref _gameDirectory, value);
}
private bool _isManual;
public bool IsManual
{
get => _isManual;
set => SetProperty(ref _isManual, value);
}
private EGame _ueVersion;
public EGame UeVersion
{
get => _ueVersion;
set => SetProperty(ref _ueVersion, value);
}
private ETexturePlatform _texturePlatform;
public ETexturePlatform TexturePlatform
{
get => _texturePlatform;
set => SetProperty(ref _texturePlatform, value);
}
private VersioningSettings _versioning;
public VersioningSettings Versioning
{
get => _versioning;
set => SetProperty(ref _versioning, value);
}
private EndpointSettings[] _endpoints;
public EndpointSettings[] Endpoints
{
get => _endpoints;
set => SetProperty(ref _endpoints, value);
}
private IList<CustomDirectory> _directories;
public IList<CustomDirectory> Directories
{
get => _directories;
set => SetProperty(ref _directories, value);
}
private AesResponse _aesKeys;
public AesResponse AesKeys
{
get => _aesKeys;
set => SetProperty(ref _aesKeys, value);
}
private DateTime _lastAesReload;
public DateTime LastAesReload
{
get => _lastAesReload;
set => SetProperty(ref _lastAesReload, value);
}
private bool Equals(DirectorySettings other)
{
return GameDirectory == other.GameDirectory && UeVersion == other.UeVersion;
}
public override bool Equals(object obj)
{
return obj is DirectorySettings other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(GameDirectory, (int) UeVersion);
}
public override string ToString()
{
return GameName;
}
public object Clone()
{
return this.MemberwiseClone();
}
}

View File

@ -1,92 +1,109 @@
using System.Linq;
using FModel.ViewModels.ApiEndpoints;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace FModel.Framework;
public class FEndpoint : ViewModel
{
private string _url;
public string Url
{
get => _url;
set => SetProperty(ref _url, value);
}
private string _path;
public string Path
{
get => _path;
set => SetProperty(ref _path, value);
}
private bool _overwrite;
public bool Overwrite
{
get => _overwrite;
set => SetProperty(ref _overwrite, value);
}
private string _filePath;
public string FilePath
{
get => _filePath;
set => SetProperty(ref _filePath, value);
}
private bool _isValid;
public bool IsValid
{
get => _isValid;
set
{
SetProperty(ref _isValid, value);
RaisePropertyChanged(nameof(Label));
}
}
[JsonIgnore]
public string Label => IsValid ?
"Your endpoint configuration is valid! Please, avoid any unnecessary modifications!" :
"Your endpoint configuration DOES NOT seem to be valid yet! Please, test it out!";
public FEndpoint() {}
public FEndpoint(string url, string path)
{
Url = url;
Path = path;
IsValid = !string.IsNullOrEmpty(url) && !string.IsNullOrEmpty(path); // be careful with this
}
public void TryValidate(DynamicApiEndpoint endpoint, EEndpointType type, out JToken response)
{
response = null;
if (string.IsNullOrEmpty(Url) || string.IsNullOrEmpty(Path))
{
IsValid = false;
}
else switch (type)
{
case EEndpointType.Aes:
{
var r = endpoint.GetAesKeys(default, Url, Path);
response = JToken.FromObject(r);
IsValid = r.IsValid;
break;
}
case EEndpointType.Mapping:
{
var r = endpoint.GetMappings(default, Url, Path);
response = JToken.FromObject(r);
IsValid = r.Any(x => x.IsValid);
break;
}
default:
{
IsValid = false;
break;
}
}
}
}
using System.Linq;
using FModel.Framework;
using FModel.ViewModels.ApiEndpoints;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace FModel.Settings;
public class EndpointSettings : ViewModel
{
public static EndpointSettings[] Default(string gameName)
{
switch (gameName)
{
case "Fortnite":
case "Fortnite [LIVE]":
return new EndpointSettings[]
{
new("https://fortnitecentral.genxgames.gg/api/v1/aes", "$.['mainKey','dynamicKeys']"),
new("https://fortnitecentral.genxgames.gg/api/v1/mappings", "$.[0].['url','fileName']") // just get the first available, not just oodle! (Unfortunately not default except when resetting settings)
};
default:
return new EndpointSettings[] { new(), new() };
}
}
private string _url;
public string Url
{
get => _url;
set => SetProperty(ref _url, value);
}
private string _path;
public string Path
{
get => _path;
set => SetProperty(ref _path, value);
}
private bool _overwrite;
public bool Overwrite
{
get => _overwrite;
set => SetProperty(ref _overwrite, value);
}
private string _filePath;
public string FilePath
{
get => _filePath;
set => SetProperty(ref _filePath, value);
}
private bool _isValid;
public bool IsValid
{
get => _isValid;
set
{
SetProperty(ref _isValid, value);
RaisePropertyChanged(nameof(Label));
}
}
[JsonIgnore]
public string Label => IsValid ?
"Your endpoint configuration is valid! Please, avoid any unnecessary modifications!" :
"Your endpoint configuration DOES NOT seem to be valid yet! Please, test it out!";
public EndpointSettings() {}
public EndpointSettings(string url, string path)
{
Url = url;
Path = path;
IsValid = !string.IsNullOrEmpty(url) && !string.IsNullOrEmpty(path); // be careful with this
}
public void TryValidate(DynamicApiEndpoint endpoint, EEndpointType type, out JToken response)
{
response = null;
if (string.IsNullOrEmpty(Url) || string.IsNullOrEmpty(Path))
{
IsValid = false;
}
else switch (type)
{
case EEndpointType.Aes:
{
var r = endpoint.GetAesKeys(default, Url, Path);
response = JToken.FromObject(r);
IsValid = r.IsValid;
break;
}
case EEndpointType.Mapping:
{
var r = endpoint.GetMappings(default, Url, Path);
response = JToken.FromObject(r);
IsValid = r.Any(x => x.IsValid);
break;
}
default:
{
IsValid = false;
break;
}
}
}
}

View File

@ -3,11 +3,12 @@ using System.Collections.Generic;
using System.IO;
using System.Windows;
using System.Windows.Input;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Objects.Core.Serialization;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Animations;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Textures;
using CUE4Parse_Conversion.UEFormat.Enums;
using CUE4Parse.UE4.Assets.Exports.Material;
using FModel.Framework;
using FModel.ViewModels;
@ -31,26 +32,48 @@ namespace FModel.Settings
Default = new UserSettings();
}
private static bool _bSave = true;
public static void Save()
{
if (!_bSave || Default == null) return;
Default.PerDirectory[Default.CurrentDir.GameDirectory] = Default.CurrentDir;
File.WriteAllText(FilePath, JsonConvert.SerializeObject(Default, Formatting.Indented));
}
public static void Delete()
{
if (File.Exists(FilePath)) File.Delete(FilePath);
if (File.Exists(FilePath))
{
_bSave = false;
File.Delete(FilePath);
}
}
public static bool IsEndpointValid(FGame game, EEndpointType type, out FEndpoint endpoint)
public static bool IsEndpointValid(EEndpointType type, out EndpointSettings endpoint)
{
endpoint = null;
if (!Default.CustomEndpoints.TryGetValue(game, out var endpoints))
return false;
endpoint = endpoints[(int) type];
endpoint = Default.CurrentDir.Endpoints[(int) type];
return endpoint.Overwrite || endpoint.IsValid;
}
[JsonIgnore]
public ExporterOptions ExportOptions => new()
{
LodFormat = Default.LodExportFormat,
MeshFormat = Default.MeshExportFormat,
AnimFormat = Default.MeshExportFormat switch
{
EMeshFormat.UEFormat => EAnimFormat.UEFormat,
_ => EAnimFormat.ActorX
},
MaterialFormat = Default.MaterialExportFormat,
TextureFormat = Default.TextureExportFormat,
SocketFormat = Default.SocketExportFormat,
CompressionFormat = Default.CompressionFormat,
Platform = Default.CurrentDir.TexturePlatform,
ExportMorphTargets = Default.SaveMorphTargets,
ExportMaterials = Default.SaveEmbeddedMaterials
};
private bool _showChangelog = true;
public bool ShowChangelog
{
@ -100,7 +123,7 @@ namespace FModel.Settings
set => SetProperty(ref _modelDirectory, value);
}
private string _gameDirectory;
private string _gameDirectory = string.Empty;
public string GameDirectory
{
get => _gameDirectory;
@ -135,13 +158,6 @@ namespace FModel.Settings
set => SetProperty(ref _avalonImageSize, value);
}
private IDictionary<FGame, AesResponse> _aesKeys = new Dictionary<FGame, AesResponse>();
public IDictionary<FGame, AesResponse> AesKeys
{
get => _aesKeys;
set => SetProperty(ref _aesKeys, value);
}
private string _audioDeviceId;
public string AudioDeviceId
{
@ -156,18 +172,25 @@ namespace FModel.Settings
set => SetProperty(ref _audioPlayerVolume, value);
}
private ELoadingMode _loadingMode = ELoadingMode.Multiple;
private ELoadingMode _loadingMode = ELoadingMode.All;
public ELoadingMode LoadingMode
{
get => _loadingMode;
set => SetProperty(ref _loadingMode, value);
}
private EUpdateMode _updateMode = EUpdateMode.Beta;
public EUpdateMode UpdateMode
private DateTime _lastUpdateCheck = DateTime.MinValue;
public DateTime LastUpdateCheck
{
get => _updateMode;
set => SetProperty(ref _updateMode, value);
get => _lastUpdateCheck;
set => SetProperty(ref _lastUpdateCheck, value);
}
private DateTime _nextUpdateCheck = DateTime.Now;
public DateTime NextUpdateCheck
{
get => _nextUpdateCheck;
set => SetProperty(ref _nextUpdateCheck, value);
}
private bool _keepDirectoryStructure = true;
@ -233,9 +256,19 @@ namespace FModel.Settings
set => SetProperty(ref _readScriptData, value);
}
// <gameDirectory as string, settings>
// can't refactor to use this data layout for everything
// because it will wipe old user settings that relies on FGame
private IDictionary<string, DirectorySettings> _perDirectory = new Dictionary<string, DirectorySettings>();
public IDictionary<string, DirectorySettings> PerDirectory
{
get => _perDirectory;
set => SetProperty(ref _perDirectory, value);
}
[JsonIgnore]
public DirectorySettings CurrentDir { get; set; }
/// <summary>
/// TO DELETEEEEEEEEEEEEE
/// </summary>
private IDictionary<string, GameSelectorViewModel.DetectedGame> _manualGames = new Dictionary<string, GameSelectorViewModel.DetectedGame>();
public IDictionary<string, GameSelectorViewModel.DetectedGame> ManualGames
{
@ -243,291 +276,6 @@ namespace FModel.Settings
set => SetProperty(ref _manualGames, value);
}
private ETexturePlatform _overridedPlatform = ETexturePlatform.DesktopMobile;
public ETexturePlatform OverridedPlatform
{
get => _overridedPlatform;
set => SetProperty(ref _overridedPlatform, value);
}
private IDictionary<FGame, string> _presets = new Dictionary<FGame, string>
{
{FGame.Unknown, Constants._NO_PRESET_TRIGGER},
{FGame.FortniteGame, Constants._NO_PRESET_TRIGGER},
{FGame.ShooterGame, Constants._NO_PRESET_TRIGGER},
{FGame.DeadByDaylight, Constants._NO_PRESET_TRIGGER},
{FGame.OakGame, Constants._NO_PRESET_TRIGGER},
{FGame.Dungeons, Constants._NO_PRESET_TRIGGER},
{FGame.WorldExplorers, Constants._NO_PRESET_TRIGGER},
{FGame.g3, Constants._NO_PRESET_TRIGGER},
{FGame.StateOfDecay2, Constants._NO_PRESET_TRIGGER},
{FGame.Prospect, Constants._NO_PRESET_TRIGGER},
{FGame.Indiana, Constants._NO_PRESET_TRIGGER},
{FGame.RogueCompany, Constants._NO_PRESET_TRIGGER},
{FGame.SwGame, Constants._NO_PRESET_TRIGGER},
{FGame.Platform, Constants._NO_PRESET_TRIGGER},
{FGame.BendGame, Constants._NO_PRESET_TRIGGER},
{FGame.TslGame, Constants._NO_PRESET_TRIGGER},
{FGame.PortalWars, Constants._NO_PRESET_TRIGGER},
{FGame.Gameface, Constants._NO_PRESET_TRIGGER},
{FGame.Athena, Constants._NO_PRESET_TRIGGER},
{FGame.MultiVersus, Constants._NO_PRESET_TRIGGER},
{FGame.Hotta, Constants._NO_PRESET_TRIGGER},
{FGame.eFootball, Constants._NO_PRESET_TRIGGER}
};
public IDictionary<FGame, string> Presets
{
get => _presets;
set => SetProperty(ref _presets, value);
}
private IDictionary<FGame, EGame> _overridedGame = new Dictionary<FGame, EGame>
{
{FGame.Unknown, EGame.GAME_UE4_LATEST},
{FGame.FortniteGame, EGame.GAME_UE5_2},
{FGame.ShooterGame, EGame.GAME_Valorant},
{FGame.DeadByDaylight, EGame.GAME_UE4_27},
{FGame.OakGame, EGame.GAME_Borderlands3},
{FGame.Dungeons, EGame.GAME_UE4_22},
{FGame.WorldExplorers, EGame.GAME_UE4_24},
{FGame.g3, EGame.GAME_UE4_22},
{FGame.StateOfDecay2, EGame.GAME_StateOfDecay2},
{FGame.Prospect, EGame.GAME_Splitgate},
{FGame.Indiana, EGame.GAME_UE4_21},
{FGame.RogueCompany, EGame.GAME_RogueCompany},
{FGame.SwGame, EGame.GAME_UE4_LATEST},
{FGame.Platform, EGame.GAME_UE4_26},
{FGame.BendGame, EGame.GAME_UE4_11},
{FGame.TslGame, EGame.GAME_PlayerUnknownsBattlegrounds},
{FGame.PortalWars, EGame.GAME_UE4_27},
{FGame.Gameface, EGame.GAME_GTATheTrilogyDefinitiveEdition},
{FGame.Athena, EGame.GAME_SeaOfThieves},
{FGame.MultiVersus, EGame.GAME_UE4_26},
{FGame.Hotta, EGame.GAME_TowerOfFantasy},
{FGame.eFootball, EGame.GAME_UE4_26}
};
public IDictionary<FGame, EGame> OverridedGame
{
get => _overridedGame;
set => SetProperty(ref _overridedGame, value);
}
private IDictionary<FGame, List<FCustomVersion>> _overridedCustomVersions = new Dictionary<FGame, List<FCustomVersion>>
{
{FGame.Unknown, null},
{FGame.FortniteGame, null},
{FGame.ShooterGame, null},
{FGame.DeadByDaylight, null},
{FGame.OakGame, null},
{FGame.Dungeons, null},
{FGame.WorldExplorers, null},
{FGame.g3, null},
{FGame.StateOfDecay2, null},
{FGame.Prospect, null},
{FGame.Indiana, null},
{FGame.RogueCompany, null},
{FGame.SwGame, null},
{FGame.Platform, null},
{FGame.BendGame, null},
{FGame.TslGame, null},
{FGame.PortalWars, null},
{FGame.Gameface, null},
{FGame.Athena, null},
{FGame.MultiVersus, null},
{FGame.Hotta, null},
{FGame.eFootball, null}
};
public IDictionary<FGame, List<FCustomVersion>> OverridedCustomVersions
{
get => _overridedCustomVersions;
set => SetProperty(ref _overridedCustomVersions, value);
}
private IDictionary<FGame, Dictionary<string, bool>> _overridedOptions = new Dictionary<FGame, Dictionary<string, bool>>
{
{FGame.Unknown, null},
{FGame.FortniteGame, null},
{FGame.ShooterGame, null},
{FGame.DeadByDaylight, null},
{FGame.OakGame, null},
{FGame.Dungeons, null},
{FGame.WorldExplorers, null},
{FGame.g3, null},
{FGame.StateOfDecay2, null},
{FGame.Prospect, null},
{FGame.Indiana, null},
{FGame.RogueCompany, null},
{FGame.SwGame, null},
{FGame.Platform, null},
{FGame.BendGame, null},
{FGame.TslGame, null},
{FGame.PortalWars, null},
{FGame.Gameface, null},
{FGame.Athena, null},
{FGame.MultiVersus, null},
{FGame.Hotta, null},
{FGame.eFootball, null}
};
private IDictionary<FGame, Dictionary<string, KeyValuePair<string, string>>> _overridedMapStructTypes = new Dictionary<FGame, Dictionary<string, KeyValuePair<string, string>>>
{
{FGame.Unknown, null},
{FGame.FortniteGame, null},
{FGame.ShooterGame, null},
{FGame.DeadByDaylight, null},
{FGame.OakGame, null},
{FGame.Dungeons, null},
{FGame.WorldExplorers, null},
{FGame.g3, null},
{FGame.StateOfDecay2, null},
{FGame.Prospect, null},
{FGame.Indiana, null},
{FGame.RogueCompany, null},
{FGame.SwGame, null},
{FGame.Platform, null},
{FGame.BendGame, null},
{FGame.TslGame, null},
{FGame.PortalWars, null},
{FGame.Gameface, null},
{FGame.Athena, null},
{FGame.MultiVersus, null},
{FGame.Hotta, null},
{FGame.eFootball, null}
};
public IDictionary<FGame, Dictionary<string, bool>> OverridedOptions
{
get => _overridedOptions;
set => SetProperty(ref _overridedOptions, value);
}
public IDictionary<FGame, Dictionary<string, KeyValuePair<string, string>>> OverridedMapStructTypes
{
get => _overridedMapStructTypes;
set => SetProperty(ref _overridedMapStructTypes, value);
}
private IDictionary<FGame, FEndpoint[]> _customEndpoints = new Dictionary<FGame, FEndpoint[]>
{
{FGame.Unknown, new FEndpoint[]{new (), new ()}},
{
FGame.FortniteGame, new []
{
new FEndpoint("https://fortnitecentral.genxgames.gg/api/v1/aes", "$.['mainKey','dynamicKeys']"),
new FEndpoint("https://fortnitecentral.genxgames.gg/api/v1/mappings", "$.[?(@.meta.compressionMethod=='Oodle')].['url','fileName']") // && @.meta.platform=='Windows'
}
},
{FGame.ShooterGame, new FEndpoint[]{new (), new ()}},
{FGame.DeadByDaylight, new FEndpoint[]{new (), new ()}},
{FGame.OakGame, new FEndpoint[]{new (), new ()}},
{FGame.Dungeons, new FEndpoint[]{new (), new ()}},
{FGame.WorldExplorers, new FEndpoint[]{new (), new ()}},
{FGame.g3, new FEndpoint[]{new (), new ()}},
{FGame.StateOfDecay2, new FEndpoint[]{new (), new ()}},
{FGame.Prospect, new FEndpoint[]{new (), new ()}},
{FGame.Indiana, new FEndpoint[]{new (), new ()}},
{FGame.RogueCompany, new FEndpoint[]{new (), new ()}},
{FGame.SwGame, new FEndpoint[]{new (), new ()}},
{FGame.Platform, new FEndpoint[]{new (), new ()}},
{FGame.BendGame, new FEndpoint[]{new (), new ()}},
{FGame.TslGame, new FEndpoint[]{new (), new ()}},
{FGame.PortalWars, new FEndpoint[]{new (), new ()}},
{FGame.Gameface, new FEndpoint[]{new (), new ()}},
{FGame.Athena, new FEndpoint[]{new (), new ()}},
{FGame.MultiVersus, new FEndpoint[]{new (), new ()}},
{FGame.Hotta, new FEndpoint[]{new (), new ()}},
{FGame.eFootball, new FEndpoint[]{new (), new ()}}
};
public IDictionary<FGame, FEndpoint[]> CustomEndpoints
{
get => _customEndpoints;
set => SetProperty(ref _customEndpoints, value);
}
private IDictionary<FGame, IList<CustomDirectory>> _customDirectories = new Dictionary<FGame, IList<CustomDirectory>>
{
{FGame.Unknown, new List<CustomDirectory>()},
{
FGame.FortniteGame, new List<CustomDirectory>
{
new("Cosmetics", "FortniteGame/Content/Athena/Items/Cosmetics/"),
new("Emotes [AUDIO]", "FortniteGame/Content/Athena/Sounds/Emotes/"),
new("Music Packs [AUDIO]", "FortniteGame/Content/Athena/Sounds/MusicPacks/"),
new("Weapons", "FortniteGame/Content/Athena/Items/Weapons/"),
new("Strings", "FortniteGame/Content/Localization/")
}
},
{
FGame.ShooterGame, new List<CustomDirectory>
{
new("Audio", "ShooterGame/Content/WwiseAudio/Media/"),
new("Characters", "ShooterGame/Content/Characters/"),
new("Gun Buddies", "ShooterGame/Content/Equippables/Buddies/"),
new("Cards and Sprays", "ShooterGame/Content/Personalization/"),
new("Shop Backgrounds", "ShooterGame/Content/UI/OutOfGame/MainMenu/Store/Shared/Textures/"),
new("Weapon Renders", "ShooterGame/Content/UI/Screens/OutOfGame/MainMenu/Collection/Assets/Large/")
}
},
{
FGame.DeadByDaylight, new List<CustomDirectory>
{
new("Audio", "DeadByDaylight/Content/WwiseAudio/Windows/"),
new("Characters", "DeadByDaylight/Content/Characters/"),
new("Icons", "DeadByDaylight/Content/UI/UMGAssets/Icons/"),
new("Strings", "DeadByDaylight/Content/Localization/")
}
},
{FGame.OakGame, new List<CustomDirectory>()},
{
FGame.Dungeons, new List<CustomDirectory>
{
new("Levels", "Dungeons/Content/data/Lovika/Levels"),
new("Friendlies", "Dungeons/Content/Actor/Characters/Friendlies"),
new("Skins", "Dungeons/Content/Actor/Characters/Player/Master/Skins"),
new("Strings", "Dungeons/Content/Localization/")
}
},
{
FGame.WorldExplorers, new List<CustomDirectory>
{
new("Loot", "WorldExplorers/Content/Loot/"),
new("Strings", "WorldExplorers/Content/Localization/")
}
},
{
FGame.g3, new List<CustomDirectory>
{
new("Cosmetics", "g3/Content/Blueprints/Cosmetics/"),
new("Strings", "g3/Content/Localization/")
}
},
{FGame.StateOfDecay2, new List<CustomDirectory>()},
{FGame.Prospect, new List<CustomDirectory>()},
{FGame.Indiana, new List<CustomDirectory>()},
{FGame.RogueCompany, new List<CustomDirectory>()},
{FGame.SwGame, new List<CustomDirectory>()},
{FGame.Platform, new List<CustomDirectory>()},
{FGame.BendGame, new List<CustomDirectory>()},
{FGame.TslGame, new List<CustomDirectory>()},
{FGame.PortalWars, new List<CustomDirectory>()},
{FGame.Gameface, new List<CustomDirectory>()},
{FGame.Athena, new List<CustomDirectory>()},
{FGame.MultiVersus, new List<CustomDirectory>()},
{FGame.Hotta, new List<CustomDirectory>()},
{FGame.eFootball, new List<CustomDirectory>()}
};
public IDictionary<FGame, IList<CustomDirectory>> CustomDirectories
{
get => _customDirectories;
set => SetProperty(ref _customDirectories, value);
}
private DateTime _lastAesReload = DateTime.Today.AddDays(-1);
public DateTime LastAesReload
{
get => _lastAesReload;
set => SetProperty(ref _lastAesReload, value);
}
private AuthResponse _lastAuthResponse = new() {AccessToken = "", ExpiresAt = DateTime.Now};
public AuthResponse LastAuthResponse
{
@ -633,6 +381,13 @@ namespace FModel.Settings
set => SetProperty(ref _socketExportFormat, value);
}
private EFileCompressionFormat _compressionFormat = EFileCompressionFormat.ZSTD;
public EFileCompressionFormat CompressionFormat
{
get => _compressionFormat;
set => SetProperty(ref _compressionFormat, value);
}
private ELodFormat _lodExportFormat = ELodFormat.FirstLod;
public ELodFormat LodExportFormat
{
@ -710,6 +465,13 @@ namespace FModel.Settings
set => SetProperty(ref _saveMorphTargets, value);
}
private bool _saveEmbeddedMaterials = true;
public bool SaveEmbeddedMaterials
{
get => _saveEmbeddedMaterials;
set => SetProperty(ref _saveEmbeddedMaterials, value);
}
private bool _saveSkeletonAsMesh;
public bool SaveSkeletonAsMesh
{

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
using CUE4Parse.UE4.Objects.Core.Serialization;
using FModel.Framework;
namespace FModel.Settings;
public class VersioningSettings : ViewModel
{
private IList<FCustomVersion> _customVersions;
public IList<FCustomVersion> CustomVersions
{
get => _customVersions;
set => SetProperty(ref _customVersions, value);
}
private IDictionary<string, bool> _options;
public IDictionary<string, bool> Options
{
get => _options;
set => SetProperty(ref _options, value);
}
private IDictionary<string, KeyValuePair<string, string>> _mapStructTypes;
public IDictionary<string, KeyValuePair<string, string>> MapStructTypes
{
get => _mapStructTypes;
set => SetProperty(ref _mapStructTypes, value);
}
public VersioningSettings() {}
}

View File

@ -0,0 +1,70 @@
using System.Text;
using System.Threading.Tasks;
using FModel.Framework;
using FModel.Services;
using FModel.ViewModels.ApiEndpoints.Models;
namespace FModel.ViewModels;
public class AboutViewModel : ViewModel
{
private ApiEndpointViewModel _apiEndpointView => ApplicationService.ApiEndpointView;
private string _descriptionLabel;
public string DescriptionLabel
{
get => _descriptionLabel;
set => SetProperty(ref _descriptionLabel, value);
}
private string _contributorsLabel;
public string ContributorsLabel
{
get => _contributorsLabel;
set => SetProperty(ref _contributorsLabel, value);
}
private string _donatorsLabel;
public string DonatorsLabel
{
get => _donatorsLabel;
set => SetProperty(ref _donatorsLabel, value);
}
private string _referencesLabel;
public string ReferencesLabel
{
get => _referencesLabel;
set => SetProperty(ref _referencesLabel, value);
}
public AboutViewModel()
{
}
public async Task Initialize()
{
await Task.WhenAll(
Task.Run(() =>
{
DescriptionLabel = "FModel is an archive explorer for Unreal Engine games that uses CUE4Parse as its core parsing library, providing robust support for the latest UE4 and UE5 archive formats. It aims to deliver a modern and intuitive user interface, powerful features, and a comprehensive set of tools for previewing and converting game packages, empowering YOU to understand games' inner workings with ease.";
ContributorsLabel = $"FModel owes its continued existence to the passionate individuals who have generously contributed their time and expertise. Contributions from individuals such as {string.Join(", ", "GMatrixGames", "amr", "LongerWarrior", "MinshuG", "InTheShade", "Officer")}, and countless others, both in the past and those yet to come, ensure the continuous development and success of this project. If you are benefiting from FModel and would like to support its continued improvements, please consider making a donation.";
ReferencesLabel = string.Join(", ",
"Adonis UI", "AutoUpdater.NET", "AvalonEdit", "CSCore", "CUE4Parse", "DiscordRichPresence",
"EpicManifestParser", "ImGui.NET", "K4os.Compression.LZ4", "Newtonsoft.Json", "NVorbis", "Oodle.NET",
"Ookii.Dialogs.Wpf", "OpenTK", "RestSharp", "Serilog", "SixLabors.ImageSharp", "SkiaSharp");
}),
Task.Run(() =>
{
var donators = _apiEndpointView.FModelApi.GetDonators();
if (donators == null) return;
var sb = new StringBuilder();
sb.AppendJoin<Donator>(", ", donators);
sb.Append('.');
DonatorsLabel = sb.ToString();
})
).ConfigureAwait(false);
}
}

View File

@ -8,7 +8,6 @@ using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels.ApiEndpoints.Models;
using Serilog;
namespace FModel.ViewModels;
@ -35,22 +34,7 @@ public class AesManagerViewModel : ViewModel
{
await _threadWorkerView.Begin(_ =>
{
if (_cue4Parse.Game == FGame.Unknown &&
UserSettings.Default.ManualGames.TryGetValue(UserSettings.Default.GameDirectory, out var settings))
{
_keysFromSettings = settings.AesKeys;
}
else
{
UserSettings.Default.AesKeys.TryGetValue(_cue4Parse.Game, out _keysFromSettings);
}
_keysFromSettings ??= new AesResponse
{
MainKey = string.Empty,
DynamicKeys = null
};
_keysFromSettings = UserSettings.Default.CurrentDir.AesKeys;
_mainKey.Key = Helper.FixKey(_keysFromSettings.MainKey);
AesKeys = new FullyObservableCollection<FileItem>(EnumerateAesKeys());
AesKeys.ItemPropertyChanged += AesKeysOnItemPropertyChanged;
@ -105,9 +89,7 @@ public class AesManagerViewModel : ViewModel
public void SetAesKeys()
{
if (_cue4Parse.Game == FGame.Unknown && UserSettings.Default.ManualGames.ContainsKey(UserSettings.Default.GameDirectory))
UserSettings.Default.ManualGames[UserSettings.Default.GameDirectory].AesKeys = _keysFromSettings;
else UserSettings.Default.AesKeys[_cue4Parse.Game] = _keysFromSettings;
UserSettings.Default.CurrentDir.AesKeys = _keysFromSettings;
// Log.Information("{@Json}", UserSettings.Default);
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Threading.Tasks;
using FModel.Framework;
@ -12,7 +12,7 @@ public class ApiEndpointViewModel
private readonly RestClient _client = new (new RestClientOptions
{
UserAgent = $"FModel/{Constants.APP_VERSION}",
MaxTimeout = 3 * 1000
Timeout = TimeSpan.FromSeconds(5)
}, configureSerialization: s => s.UseSerializer<JsonNetSerializer>());
public FortniteApiEndpoint FortniteApi { get; }
@ -20,6 +20,7 @@ public class ApiEndpointViewModel
public FortniteCentralApiEndpoint CentralApi { get; }
public EpicApiEndpoint EpicApi { get; }
public FModelApiEndpoint FModelApi { get; }
public GitHubApiEndpoint GitHubApi { get; }
public DynamicApiEndpoint DynamicApi { get; }
public ApiEndpointViewModel()
@ -29,6 +30,7 @@ public class ApiEndpointViewModel
CentralApi = new FortniteCentralApiEndpoint(_client);
EpicApi = new EpicApiEndpoint(_client);
FModelApi = new FModelApiEndpoint(_client);
GitHubApi = new GitHubApiEndpoint(_client);
DynamicApi = new DynamicApiEndpoint(_client);
}

View File

@ -1,10 +1,14 @@
using System.Threading;
using System.Threading.Tasks;
using EpicManifestParser.Objects;
using EpicManifestParser.Api;
using FModel.Framework;
using FModel.Settings;
using FModel.ViewModels.ApiEndpoints.Models;
using RestSharp;
using Serilog;
namespace FModel.ViewModels.ApiEndpoints;
@ -14,45 +18,18 @@ public class EpicApiEndpoint : AbstractApiProvider
private const string _OAUTH_URL = "https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token";
private const string _BASIC_TOKEN = "basic MzQ0NmNkNzI2OTRjNGE0NDg1ZDgxYjc3YWRiYjIxNDE6OTIwOWQ0YTVlMjVhNDU3ZmI5YjA3NDg5ZDMxM2I0MWE=";
private const string _APP_URL = "https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/v2/platform/Windows/namespace/fn/catalogItem/4fe75bbc5a674f4f9b356b5c90567da5/app/Fortnite/label/Live";
private const string _CBM_URL = "https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/Windows/5cb97847cee34581afdbc445400e2f77/FortniteContentBuilds";
public EpicApiEndpoint(RestClient client) : base(client) { }
public async Task<ManifestInfo> GetManifestAsync(CancellationToken token)
{
if (await IsExpired().ConfigureAwait(false))
{
var auth = await GetAuthAsync(token).ConfigureAwait(false);
if (auth != null)
{
UserSettings.Default.LastAuthResponse = auth;
}
}
await VerifyAuth(token).ConfigureAwait(false);
var request = new FRestRequest(_APP_URL);
request.AddHeader("Authorization", $"bearer {UserSettings.Default.LastAuthResponse.AccessToken}");
var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.IsSuccessful ? new ManifestInfo(response.Content) : null;
}
public async Task<ContentBuildManifestInfo> GetContentBuildManifestAsync(CancellationToken token, string label)
{
if (await IsExpired().ConfigureAwait(false))
{
var auth = await GetAuthAsync(token).ConfigureAwait(false);
if (auth != null)
{
UserSettings.Default.LastAuthResponse = auth;
}
}
var request = new FRestRequest(_CBM_URL);
request.AddHeader("Authorization", $"bearer {UserSettings.Default.LastAuthResponse.AccessToken}");
request.AddQueryParameter("label", label);
var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.IsSuccessful ? new ContentBuildManifestInfo(response.Content) : null;
return response.IsSuccessful ? ManifestInfo.Deserialize(response.RawBytes) : null;
}
public ManifestInfo GetManifest(CancellationToken token)
@ -60,9 +37,16 @@ public class EpicApiEndpoint : AbstractApiProvider
return GetManifestAsync(token).GetAwaiter().GetResult();
}
public ContentBuildManifestInfo GetContentBuildManifest(CancellationToken token, string label)
public async Task VerifyAuth(CancellationToken token)
{
return GetContentBuildManifestAsync(token, label).GetAwaiter().GetResult();
if (await IsExpired().ConfigureAwait(false))
{
var auth = await GetAuthAsync(token).ConfigureAwait(false);
if (auth != null)
{
UserSettings.Default.LastAuthResponse = auth;
}
}
}
private async Task<AuthResponse> GetAuthAsync(CancellationToken token)

View File

@ -10,13 +10,13 @@ using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels.ApiEndpoints.Models;
using FModel.Views;
using Newtonsoft.Json;
using RestSharp;
using Serilog;
using MessageBox = AdonisUI.Controls.MessageBox;
using MessageBoxButton = AdonisUI.Controls.MessageBoxButton;
using MessageBoxImage = AdonisUI.Controls.MessageBoxImage;
using MessageBoxResult = AdonisUI.Controls.MessageBoxResult;
namespace FModel.ViewModels.ApiEndpoints;
@ -24,6 +24,7 @@ public class FModelApiEndpoint : AbstractApiProvider
{
private News _news;
private Info _infos;
private Donator[] _donators;
private Backup[] _backups;
private Game _game;
private readonly IDictionary<string, CommunityDesign> _communityDesigns = new Dictionary<string, CommunityDesign>();
@ -45,17 +46,17 @@ public class FModelApiEndpoint : AbstractApiProvider
return _news ??= GetNewsAsync(token, game).GetAwaiter().GetResult();
}
public async Task<Info> GetInfosAsync(CancellationToken token, EUpdateMode updateMode)
public async Task<Donator[]> GetDonatorsAsync()
{
var request = new FRestRequest($"https://api.fmodel.app/v1/infos/{updateMode}");
var response = await _client.ExecuteAsync<Info>(request, token).ConfigureAwait(false);
var request = new FRestRequest($"https://api.fmodel.app/v1/donations/donators");
var response = await _client.ExecuteAsync<Donator[]>(request).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.Data;
}
public Info GetInfos(CancellationToken token, EUpdateMode updateMode)
public Donator[] GetDonators()
{
return _infos ?? GetInfosAsync(token, updateMode).GetAwaiter().GetResult();
return _donators ??= GetDonatorsAsync().GetAwaiter().GetResult();
}
public async Task<Backup[]> GetBackupsAsync(CancellationToken token, string gameName)
@ -102,11 +103,16 @@ public class FModelApiEndpoint : AbstractApiProvider
return communityDesign;
}
public void CheckForUpdates(EUpdateMode updateMode)
public void CheckForUpdates(bool launch = false)
{
AutoUpdater.ParseUpdateInfoEvent += ParseUpdateInfoEvent;
AutoUpdater.CheckForUpdateEvent += CheckForUpdateEvent;
AutoUpdater.Start($"https://api.fmodel.app/v1/infos/{updateMode}");
if (DateTime.Now < UserSettings.Default.NextUpdateCheck) return;
if (launch)
{
AutoUpdater.ParseUpdateInfoEvent += ParseUpdateInfoEvent;
AutoUpdater.CheckForUpdateEvent += CheckForUpdateEvent;
}
AutoUpdater.Start("https://api.fmodel.app/v1/infos/Qa");
}
private void ParseUpdateInfoEvent(ParseUpdateInfoEventArgs args)
@ -116,9 +122,13 @@ public class FModelApiEndpoint : AbstractApiProvider
{
args.UpdateInfo = new UpdateInfoEventArgs
{
CurrentVersion = _infos.Version,
CurrentVersion = _infos.Version.SubstringBefore('-'),
ChangelogURL = _infos.ChangelogUrl,
DownloadURL = _infos.DownloadUrl
DownloadURL = _infos.DownloadUrl,
Mandatory = new CustomMandatory
{
CommitHash = _infos.Version.SubstringAfter('+')
}
};
}
}
@ -127,40 +137,21 @@ public class FModelApiEndpoint : AbstractApiProvider
{
if (args is { CurrentVersion: { } })
{
var currentVersion = new System.Version(args.CurrentVersion);
if (currentVersion == args.InstalledVersion)
UserSettings.Default.LastUpdateCheck = DateTime.Now;
if (((CustomMandatory)args.Mandatory).CommitHash == Constants.APP_COMMIT_ID)
{
if (UserSettings.Default.ShowChangelog)
ShowChangelog(args);
return;
}
var downgrade = currentVersion < args.InstalledVersion;
var messageBox = new MessageBoxModel
{
Text = $"The latest version of FModel {UserSettings.Default.UpdateMode} is {args.CurrentVersion}. You are using version {args.InstalledVersion}. Do you want to {(downgrade ? "downgrade" : "update")} the application now?",
Caption = $"{(downgrade ? "Downgrade" : "Update")} Available",
Icon = MessageBoxImage.Question,
Buttons = MessageBoxButtons.YesNo(),
IsSoundEnabled = false
};
var currentVersion = new System.Version(args.CurrentVersion);
UserSettings.Default.ShowChangelog = currentVersion != args.InstalledVersion;
MessageBox.Show(messageBox);
if (messageBox.Result != MessageBoxResult.Yes) return;
try
{
if (AutoUpdater.DownloadUpdate(args))
{
UserSettings.Default.ShowChangelog = true;
Application.Current.Shutdown();
}
}
catch (Exception exception)
{
UserSettings.Default.ShowChangelog = false;
MessageBox.Show(exception.Message, exception.GetType().ToString(), MessageBoxButton.OK, MessageBoxImage.Error);
}
const string message = "A new update is available!";
Helper.OpenWindow<AdonisWindow>(message, () => new UpdateView { Title = message, ResizeMode = ResizeMode.NoResize }.ShowDialog());
}
else
{
@ -174,7 +165,7 @@ public class FModelApiEndpoint : AbstractApiProvider
{
var request = new FRestRequest(args.ChangelogURL);
var response = _client.Execute(request);
if (string.IsNullOrEmpty(response.Content)) return;
if (!response.IsSuccessful || string.IsNullOrEmpty(response.Content)) return;
_applicationView.CUE4Parse.TabControl.AddTab($"Release Notes: {args.CurrentVersion}");
_applicationView.CUE4Parse.TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("changelog");
@ -182,3 +173,9 @@ public class FModelApiEndpoint : AbstractApiProvider
UserSettings.Default.ShowChangelog = false;
}
}
public class CustomMandatory : Mandatory
{
public string CommitHash { get; set; }
public string ShortCommitHash => CommitHash[..7];
}

View File

@ -0,0 +1,28 @@
using System.Threading.Tasks;
using FModel.Framework;
using FModel.ViewModels.ApiEndpoints.Models;
using RestSharp;
namespace FModel.ViewModels.ApiEndpoints;
public class GitHubApiEndpoint : AbstractApiProvider
{
public GitHubApiEndpoint(RestClient client) : base(client) { }
public async Task<GitHubCommit[]> GetCommitHistoryAsync(string branch = "dev", int page = 1, int limit = 20)
{
var request = new FRestRequest(Constants.GH_COMMITS_HISTORY);
request.AddParameter("sha", branch);
request.AddParameter("page", page);
request.AddParameter("per_page", limit);
var response = await _client.ExecuteAsync<GitHubCommit[]>(request).ConfigureAwait(false);
return response.Data;
}
public async Task<GitHubRelease> GetReleaseAsync(string tag)
{
var request = new FRestRequest($"{Constants.GH_RELEASES}/tags/{tag}");
var response = await _client.ExecuteAsync<GitHubRelease>(request).ConfigureAwait(false);
return response.Data;
}
}

View File

@ -25,6 +25,14 @@ public class Backup
[J] public long FileSize { get; private set; }
}
public class Donator
{
[J] public string Username { get; private set; }
[J] public int Count { get; private set; }
public override string ToString() => $"{Username}{(Count > 5 ? " " : "")}";
}
[DebuggerDisplay("{" + nameof(DisplayName) + "}")]
public class Game
{

View File

@ -0,0 +1,125 @@
using System;
using System.Windows;
using AdonisUI.Controls;
using AutoUpdaterDotNET;
using FModel.Framework;
using FModel.Settings;
using MessageBox = AdonisUI.Controls.MessageBox;
using MessageBoxButton = AdonisUI.Controls.MessageBoxButton;
using MessageBoxImage = AdonisUI.Controls.MessageBoxImage;
using MessageBoxResult = AdonisUI.Controls.MessageBoxResult;
using J = Newtonsoft.Json.JsonPropertyAttribute;
namespace FModel.ViewModels.ApiEndpoints.Models;
public class GitHubRelease
{
[J("assets")] public GitHubAsset[] Assets { get; private set; }
}
public class GitHubAsset : ViewModel
{
[J("name")] public string Name { get; private set; }
[J("size")] public int Size { get; private set; }
[J("download_count")] public int DownloadCount { get; private set; }
[J("browser_download_url")] public string BrowserDownloadUrl { get; private set; }
[J("created_at")] public DateTime CreatedAt { get; private set; }
[J("uploader")] public Author Uploader { get; private set; }
private bool _isLatest;
public bool IsLatest
{
get => _isLatest;
set => SetProperty(ref _isLatest, value);
}
}
public class GitHubCommit : ViewModel
{
private string _sha;
[J("sha")]
public string Sha
{
get => _sha;
set
{
SetProperty(ref _sha, value);
RaisePropertyChanged(nameof(IsCurrent));
RaisePropertyChanged(nameof(ShortSha));
}
}
[J("commit")] public Commit Commit { get; set; }
[J("author")] public Author Author { get; set; }
private GitHubAsset _asset;
public GitHubAsset Asset
{
get => _asset;
set
{
SetProperty(ref _asset, value);
RaisePropertyChanged(nameof(IsDownloadable));
}
}
public bool IsCurrent => Sha == Constants.APP_COMMIT_ID;
public string ShortSha => Sha[..7];
public bool IsDownloadable => Asset != null;
public void Download()
{
if (IsCurrent)
{
MessageBox.Show(new MessageBoxModel
{
Text = "You are already on the latest version.",
Caption = "Update FModel",
Icon = MessageBoxImage.Information,
Buttons = [MessageBoxButtons.Ok()],
IsSoundEnabled = false
});
return;
}
var messageBox = new MessageBoxModel
{
Text = $"Are you sure you want to update to version '{ShortSha}'?{(!Asset.IsLatest ? "\nThis is not the latest version." : "")}",
Caption = "Update FModel",
Icon = MessageBoxImage.Question,
Buttons = MessageBoxButtons.YesNo(),
IsSoundEnabled = false
};
MessageBox.Show(messageBox);
if (messageBox.Result != MessageBoxResult.Yes) return;
try
{
if (AutoUpdater.DownloadUpdate(new UpdateInfoEventArgs { DownloadURL = Asset.BrowserDownloadUrl }))
{
Application.Current.Shutdown();
}
}
catch (Exception exception)
{
UserSettings.Default.ShowChangelog = false;
MessageBox.Show(exception.Message, exception.GetType().ToString(), MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
public class Commit
{
[J("author")] public Author Author { get; set; }
[J("message")] public string Message { get; set; }
}
public class Author
{
[J("name")] public string Name { get; set; }
[J("login")] public string Login { get; set; }
[J("date")] public DateTime Date { get; set; }
[J("avatar_url")] public string AvatarUrl { get; set; }
[J("html_url")] public string HtmlUrl { get; set; }
}

View File

@ -1,8 +1,3 @@
using CUE4Parse.UE4.Exceptions;
using CUE4Parse.UE4.Readers;
using FModel.Settings;
using Ionic.Zlib;
using RestSharp;
using System;
using System.Collections.Generic;
using System.IO;
@ -13,7 +8,15 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CUE4Parse.Compression;
using CUE4Parse.UE4.Exceptions;
using CUE4Parse.UE4.Readers;
using FModel.Framework;
using FModel.Settings;
using OffiUtils;
using RestSharp;
namespace FModel.ViewModels.ApiEndpoints;
@ -40,26 +43,22 @@ public class VManifest
public readonly VChunk[] Chunks;
public readonly VPak[] Paks;
public VManifest(byte[] data) : this(new FByteArchive("CompressedValorantManifest", data))
{
}
public VManifest(byte[] data) : this(new FByteArchive("CompressedValorantManifest", data)) { }
private VManifest(FArchive Ar)
{
using (Ar)
{
Header = new VHeader(Ar);
var compressedBuffer = Ar.ReadBytes((int) Header.CompressedSize);
var uncompressedBuffer = ZlibStream.UncompressBuffer(compressedBuffer);
if (uncompressedBuffer.Length != Header.UncompressedSize)
throw new ParserException(Ar, $"Decompression failed, {uncompressedBuffer.Length} != {Header.UncompressedSize}");
var uncompressedBuffer = new byte[(int)Header.UncompressedSize];
ZlibHelper.Decompress(compressedBuffer, 0, compressedBuffer.Length, uncompressedBuffer, 0, uncompressedBuffer.Length);
using var manifest = new FByteArchive("UncompressedValorantManifest", uncompressedBuffer);
Chunks = manifest.ReadArray<VChunk>((int) Header.ChunkCount);
Paks = manifest.ReadArray((int) Header.PakCount, () => new VPak(manifest));
var manifestAr = new FByteArchive("UncompressedValorantManifest", uncompressedBuffer);
Chunks = manifestAr.ReadArray<VChunk>((int) Header.ChunkCount);
Paks = manifestAr.ReadArray((int) Header.PakCount, () => new VPak(manifestAr));
if (manifest.Position != manifest.Length)
throw new ParserException(manifest, $"Parsing failed, {manifest.Position} != {manifest.Length}");
if (manifestAr.Position != manifestAr.Length)
throw new ParserException(manifestAr, $"Parsing failed, {manifestAr.Position} != {manifestAr.Length}");
}
_client = new HttpClient(new HttpClientHandler
@ -118,7 +117,7 @@ public class VManifest
return chunkBytes;
}
public Stream GetPakStream(int index) => new VPakStream(this, index);
public VPakStream GetPakStream(int index) => new VPakStream(this, index);
}
public readonly struct VHeader
@ -180,7 +179,7 @@ public readonly struct VChunk
public string GetUrl() => $"https://fmodel.fortnite-api.com/valorant/v2/chunks/{Id}";
}
public class VPakStream : Stream, ICloneable
public class VPakStream : Stream, IRandomAccessStream, ICloneable
{
private readonly VManifest _manifest;
private readonly int _pakIndex;
@ -204,11 +203,22 @@ public class VPakStream : Stream, ICloneable
public object Clone() => new VPakStream(_manifest, _pakIndex, _position);
public override int Read(byte[] buffer, int offset, int count) => ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
public override int Read(byte[] buffer, int offset, int count) =>
ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
public int ReadAt(long position, byte[] buffer, int offset, int count) =>
ReadAtAsync(position, buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var (i, startPos) = GetChunkIndex(_position);
var bytesRead = await ReadAtAsync(_position, buffer, offset, count, cancellationToken);
_position += bytesRead;
return bytesRead;
}
public async Task<int> ReadAtAsync(long position, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var (i, startPos) = GetChunkIndex(position);
if (i == -1) return 0;
await PrefetchAsync(i, startPos, count, cancellationToken).ConfigureAwait(false);
@ -235,10 +245,14 @@ public class VPakStream : Stream, ICloneable
if (++i == _chunks.Length) break;
}
_position += bytesRead;
return bytesRead;
}
public Task<int> ReadAtAsync(long position, Memory<byte> memory, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
private async Task PrefetchAsync(int i, uint startPos, long count, CancellationToken cancellationToken, int concurrentDownloads = 4)
{
var tasks = new List<Task>();

View File

@ -1,21 +1,24 @@
using FModel.Extensions;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using CUE4Parse.Compression;
using CUE4Parse.Encryption.Aes;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.UE4.VirtualFileSystem;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels.Commands;
using FModel.Views;
using FModel.Views.Resources.Controls;
using Ionic.Zip;
using Oodle.NET;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using MessageBox = AdonisUI.Controls.MessageBox;
using MessageBoxButton = AdonisUI.Controls.MessageBoxButton;
using MessageBoxImage = AdonisUI.Controls.MessageBoxImage;
using OodleCUE4 = CUE4Parse.Compression.Oodle;
namespace FModel.ViewModels;
@ -46,11 +49,9 @@ public class ApplicationViewModel : ViewModel
public CopyCommand CopyCommand => _copyCommand ??= new CopyCommand(this);
private CopyCommand _copyCommand;
public string InitialWindowTitle => $"FModel {UserSettings.Default.UpdateMode}";
public string InitialWindowTitle => $"FModel ({Constants.APP_SHORT_COMMIT_ID})";
public string GameDisplayName => CUE4Parse.Provider.GameDisplayName ?? "Unknown";
public string TitleExtra =>
$"({(CUE4Parse.Game == FGame.Unknown && UserSettings.Default.ManualGames.TryGetValue(UserSettings.Default.GameDirectory, out var settings) ? settings.OverridedGame : UserSettings.Default.OverridedGame[CUE4Parse.Game])})" +
$"{(Build != EBuildKind.Release ? $" ({Build})" : "")}";
public string TitleExtra => $"({UserSettings.Default.CurrentDir.UeVersion}){(Build != EBuildKind.Release ? $" ({Build})" : "")}";
public LoadingModesViewModel LoadingModes { get; }
public CustomDirectoriesViewModel CustomDirectories { get; }
@ -58,8 +59,6 @@ public class ApplicationViewModel : ViewModel
public SettingsViewModel SettingsView { get; }
public AesManagerViewModel AesManager { get; }
public AudioPlayerViewModel AudioPlayer { get; }
public MapViewerViewModel MapViewer { get; }
private OodleCompressor _oodle;
public ApplicationViewModel()
{
@ -73,50 +72,59 @@ public class ApplicationViewModel : ViewModel
#endif
LoadingModes = new LoadingModesViewModel();
AvoidEmptyGameDirectoryAndSetEGame(false);
if (UserSettings.Default.GameDirectory is null)
UserSettings.Default.CurrentDir = AvoidEmptyGameDirectory(false);
if (UserSettings.Default.CurrentDir is null)
{
//If no game is selected, many things will break before a shutdown request is processed in the normal way.
//A hard exit is preferable to an unhandled expection in this case
Environment.Exit(0);
}
CUE4Parse = new CUE4ParseViewModel(UserSettings.Default.GameDirectory);
CustomDirectories = new CustomDirectoriesViewModel(CUE4Parse.Game, UserSettings.Default.GameDirectory);
SettingsView = new SettingsViewModel(CUE4Parse.Game);
CUE4Parse = new CUE4ParseViewModel();
CUE4Parse.Provider.VfsRegistered += (sender, count) =>
{
if (sender is not IAesVfsReader reader) return;
Status.UpdateStatusLabel($"{count} Archives ({reader.Name})", "Registered");
CUE4Parse.GameDirectory.Add(reader);
};
CUE4Parse.Provider.VfsMounted += (sender, count) =>
{
if (sender is not IAesVfsReader reader) return;
Status.UpdateStatusLabel($"{count:N0} Packages ({reader.Name})", "Mounted");
CUE4Parse.GameDirectory.Verify(reader);
};
CUE4Parse.Provider.VfsUnmounted += (sender, _) =>
{
if (sender is not IAesVfsReader reader) return;
CUE4Parse.GameDirectory.Disable(reader);
};
CustomDirectories = new CustomDirectoriesViewModel();
SettingsView = new SettingsViewModel();
AesManager = new AesManagerViewModel(CUE4Parse);
MapViewer = new MapViewerViewModel(CUE4Parse);
AudioPlayer = new AudioPlayerViewModel();
Status.SetStatus(EStatusKind.Ready);
}
public void AvoidEmptyGameDirectoryAndSetEGame(bool bAlreadyLaunched)
public DirectorySettings AvoidEmptyGameDirectory(bool bAlreadyLaunched)
{
var gameDirectory = UserSettings.Default.GameDirectory;
if (!string.IsNullOrEmpty(gameDirectory) && !bAlreadyLaunched) return;
if (!bAlreadyLaunched && UserSettings.Default.PerDirectory.TryGetValue(gameDirectory, out var currentDir))
return currentDir;
var gameLauncherViewModel = new GameSelectorViewModel(gameDirectory);
var result = new DirectorySelector(gameLauncherViewModel).ShowDialog();
if (!result.HasValue || !result.Value) return;
if (!result.HasValue || !result.Value) return null;
UserSettings.Default.GameDirectory = gameLauncherViewModel.SelectedDetectedGame.GameDirectory;
if (!bAlreadyLaunched || gameDirectory == gameLauncherViewModel.SelectedDetectedGame.GameDirectory) return;
UserSettings.Default.GameDirectory = gameLauncherViewModel.SelectedDirectory.GameDirectory;
if (!bAlreadyLaunched || UserSettings.Default.CurrentDir.Equals(gameLauncherViewModel.SelectedDirectory))
return gameLauncherViewModel.SelectedDirectory;
// UserSettings.Save(); // ??? change key then change game, key saved correctly what?
UserSettings.Default.CurrentDir = gameLauncherViewModel.SelectedDirectory;
RestartWithWarning();
}
public async Task UpdateProvider(bool isLaunch)
{
if (!isLaunch && !AesManager.HasChange) return;
CUE4Parse.ClearProvider();
await ApplicationService.ThreadWorkerView.Begin(cancellationToken =>
{
CUE4Parse.LoadVfs(cancellationToken, AesManager.AesKeys);
CUE4Parse.Provider.LoadIniConfigs();
AesManager.SetAesKeys();
});
RaisePropertyChanged(nameof(GameDisplayName));
return null;
}
public void RestartWithWarning()
@ -135,7 +143,7 @@ public class ApplicationViewModel : ViewModel
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"\"{Path.GetFullPath(Environment.GetCommandLineArgs()[0])}\"",
Arguments = $"\"{path}\"",
UseShellExecute = false,
RedirectStandardOutput = false,
RedirectStandardError = false,
@ -161,7 +169,30 @@ public class ApplicationViewModel : ViewModel
Application.Current.Shutdown();
}
public async Task InitVgmStream()
public async Task UpdateProvider(bool isLaunch)
{
if (!isLaunch && !AesManager.HasChange) return;
CUE4Parse.ClearProvider();
await ApplicationService.ThreadWorkerView.Begin(cancellationToken =>
{
// TODO: refactor after release, select updated keys only
var aes = AesManager.AesKeys.Select(x =>
{
cancellationToken.ThrowIfCancellationRequested(); // cancel if needed
var k = x.Key.Trim();
if (k.Length != 66) k = Constants.ZERO_64_CHAR;
return new KeyValuePair<FGuid, FAesKey>(x.Guid, new FAesKey(k));
});
CUE4Parse.LoadVfs(aes);
AesManager.SetAesKeys();
});
RaisePropertyChanged(nameof(GameDisplayName));
}
public static async Task InitVgmStream()
{
var vgmZipFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "vgmstream-win.zip");
if (File.Exists(vgmZipFilePath)) return;
@ -169,9 +200,17 @@ public class ApplicationViewModel : ViewModel
await ApplicationService.ApiEndpointView.DownloadFileAsync("https://github.com/vgmstream/vgmstream/releases/latest/download/vgmstream-win.zip", vgmZipFilePath);
if (new FileInfo(vgmZipFilePath).Length > 0)
{
var zip = ZipFile.Read(vgmZipFilePath);
var zipDir = vgmZipFilePath.SubstringBeforeLast("\\");
foreach (var e in zip) e.Extract(zipDir, ExtractExistingFileAction.OverwriteSilently);
var zipDir = Path.GetDirectoryName(vgmZipFilePath)!;
await using var zipFs = File.OpenRead(vgmZipFilePath);
using var zip = new ZipArchive(zipFs, ZipArchiveMode.Read);
foreach (var entry in zip.Entries)
{
var entryPath = Path.Combine(zipDir, entry.FullName);
await using var entryFs = File.Create(entryPath);
await using var entryStream = entry.Open();
await entryStream.CopyToAsync(entryFs);
}
}
else
{
@ -179,43 +218,44 @@ public class ApplicationViewModel : ViewModel
}
}
public async Task InitOodle()
public static async Task InitImGuiSettings(bool forceDownload)
{
var dataDir = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data"));
var oodlePath = Path.Combine(dataDir.FullName, OodleCUE4.OODLE_DLL_NAME);
var imgui = "imgui.ini";
var imguiPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", imgui);
if (File.Exists(OodleCUE4.OODLE_DLL_NAME))
{
File.Move(OodleCUE4.OODLE_DLL_NAME, oodlePath, true);
}
else if (!File.Exists(oodlePath))
{
var result = await OodleCUE4.DownloadOodleDll(oodlePath);
if (!result) return;
}
if (File.Exists(imgui)) File.Move(imgui, imguiPath, true);
if (File.Exists(imguiPath) && !forceDownload) return;
if (File.Exists("oo2core_8_win64.dll"))
File.Delete("oo2core_8_win64.dll");
_oodle = new OodleCompressor(oodlePath);
unsafe
{
OodleCUE4.DecompressFunc = (bufferPtr, bufferSize, outputPtr, outputSize, a, b, c, d, e, f, g, h, i, threadModule) =>
_oodle.Decompress(new IntPtr(bufferPtr), bufferSize, new IntPtr(outputPtr), outputSize,
(OodleLZ_FuzzSafe) a, (OodleLZ_CheckCRC) b, (OodleLZ_Verbosity) c, d, e, f, g, h, i, (OodleLZ_Decode_ThreadPhase) threadModule);
}
}
public async Task InitImGuiSettings(bool forceDownload)
{
var imgui = Path.Combine(/*UserSettings.Default.OutputDirectory, ".data", */"imgui.ini");
if (File.Exists(imgui) && !forceDownload) return;
await ApplicationService.ApiEndpointView.DownloadFileAsync("https://cdn.fmodel.app/d/configurations/imgui.ini", imgui);
if (new FileInfo(imgui).Length == 0)
await ApplicationService.ApiEndpointView.DownloadFileAsync($"https://cdn.fmodel.app/d/configurations/{imgui}", imguiPath);
if (new FileInfo(imguiPath).Length == 0)
{
FLogger.Append(ELog.Error, () => FLogger.Text("Could not download ImGui settings", Constants.WHITE, true));
}
}
public static async ValueTask InitOodle()
{
var oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_DLL_NAME);
if (File.Exists(OodleHelper.OODLE_DLL_NAME))
{
File.Move(OodleHelper.OODLE_DLL_NAME, oodlePath, true);
}
else if (!File.Exists(oodlePath))
{
await OodleHelper.DownloadOodleDllAsync(oodlePath);
}
OodleHelper.Initialize(oodlePath);
}
public static async ValueTask InitZlib()
{
var zlibPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", ZlibHelper.DLL_NAME);
if (!File.Exists(zlibPath))
{
await ZlibHelper.DownloadDllAsync(zlibPath);
}
ZlibHelper.Initialize(zlibPath);
}
}

View File

@ -551,14 +551,27 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
case "adpcm":
case "opus":
case "wem":
case "at9":
case "raw":
{
if (TryConvert(out var wavFilePath) && !string.IsNullOrEmpty(wavFilePath))
if (TryConvert(out var wavFilePath))
{
var newAudio = new AudioFile(SelectedAudioFile.Id, new FileInfo(wavFilePath));
Replace(newAudio);
return true;
}
return false;
}
case "binka":
{
if (TryDecode(out var rawFilePath))
{
var newAudio = new AudioFile(SelectedAudioFile.Id, new FileInfo(rawFilePath));
Replace(newAudio);
return true;
}
return false;
}
}
@ -566,7 +579,8 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
return true;
}
private bool TryConvert(out string wavFilePath)
private bool TryConvert(out string wavFilePath) => TryConvert(SelectedAudioFile.FilePath, SelectedAudioFile.Data, out wavFilePath);
private bool TryConvert(string inputFilePath, byte[] inputFileData, out string wavFilePath)
{
wavFilePath = string.Empty;
var vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "test.exe");
@ -576,22 +590,46 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
if (!File.Exists(vgmFilePath)) return false;
}
Directory.CreateDirectory(SelectedAudioFile.FilePath.SubstringBeforeLast("/"));
File.WriteAllBytes(SelectedAudioFile.FilePath, SelectedAudioFile.Data);
Directory.CreateDirectory(inputFilePath.SubstringBeforeLast("/"));
File.WriteAllBytes(inputFilePath, inputFileData);
wavFilePath = Path.ChangeExtension(SelectedAudioFile.FilePath, ".wav");
wavFilePath = Path.ChangeExtension(inputFilePath, ".wav");
var vgmProcess = Process.Start(new ProcessStartInfo
{
FileName = vgmFilePath,
Arguments = $"-o \"{wavFilePath}\" \"{SelectedAudioFile.FilePath}\"",
Arguments = $"-o \"{wavFilePath}\" \"{inputFilePath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
});
vgmProcess?.WaitForExit();
vgmProcess?.WaitForExit(5000);
File.Delete(SelectedAudioFile.FilePath);
File.Delete(inputFilePath);
return vgmProcess?.ExitCode == 0 && File.Exists(wavFilePath);
}
private bool TryDecode(out string rawFilePath)
{
rawFilePath = string.Empty;
var binkadecPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "binkadec.exe");
if (!File.Exists(binkadecPath))
{
return false;
}
Directory.CreateDirectory(SelectedAudioFile.FilePath.SubstringBeforeLast("/"));
File.WriteAllBytes(SelectedAudioFile.FilePath, SelectedAudioFile.Data);
rawFilePath = Path.ChangeExtension(SelectedAudioFile.FilePath, ".wav");
var binkadecProcess = Process.Start(new ProcessStartInfo
{
FileName = binkadecPath,
Arguments = $"-i \"{SelectedAudioFile.FilePath}\" -o \"{rawFilePath}\"",
UseShellExecute = false,
CreateNoWindow = true
});
binkadecProcess?.WaitForExit(5000);
File.Delete(SelectedAudioFile.FilePath);
return binkadecProcess?.ExitCode == 0 && File.Exists(rawFilePath);
}
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.VirtualFileSystem;
using FModel.Framework;
using FModel.Services;
@ -20,6 +21,8 @@ namespace FModel.ViewModels;
public class BackupManagerViewModel : ViewModel
{
public const uint FBKP_MAGIC = 0x504B4246;
private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView;
private ApiEndpointViewModel _apiEndpointView => ApplicationService.ApiEndpointView;
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
@ -64,23 +67,21 @@ public class BackupManagerViewModel : ViewModel
var backupFolder = Path.Combine(UserSettings.Default.OutputDirectory, "Backups");
var fileName = $"{_gameName}_{DateTime.Now:MM'_'dd'_'yyyy}.fbkp";
var fullPath = Path.Combine(backupFolder, fileName);
var func = new Func<GameFile, bool>(x => !x.Path.EndsWith(".uexp") && !x.Path.EndsWith(".ubulk") && !x.Path.EndsWith(".uptnl"));
using var fileStream = new FileStream(fullPath, FileMode.Create);
using var compressedStream = LZ4Stream.Encode(fileStream, LZ4Level.L00_FAST);
using var writer = new BinaryWriter(compressedStream);
writer.Write(FBKP_MAGIC);
writer.Write((byte) EBackupVersion.Latest);
writer.Write(_applicationView.CUE4Parse.Provider.Files.Values.Count(func));
foreach (var asset in _applicationView.CUE4Parse.Provider.Files.Values)
{
if (asset is not VfsEntry entry || entry.Path.EndsWith(".uexp") ||
entry.Path.EndsWith(".ubulk") || entry.Path.EndsWith(".uptnl"))
continue;
writer.Write((long) 0);
writer.Write((long) 0);
writer.Write(entry.Size);
writer.Write(entry.IsEncrypted);
writer.Write(0);
writer.Write($"/{entry.Path.ToLower()}");
writer.Write(0);
if (!func(asset)) continue;
writer.Write(asset.Size);
writer.Write(asset.IsEncrypted);
writer.Write($"/{asset.Path.ToLower()}");
}
SaveCheck(fullPath, fileName, "created", "create");
@ -116,3 +117,12 @@ public class BackupManagerViewModel : ViewModel
}
}
}
public enum EBackupVersion : byte
{
BeforeVersionWasAdded = 0,
Initial,
LatestPlusOne,
Latest = LatestPlusOne - 1
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
using AdonisUI.Controls;
using FModel.Framework;
using FModel.Settings;
using FModel.Views;
namespace FModel.ViewModels.Commands;
@ -31,4 +32,4 @@ public class AddEditDirectoryCommand : ViewModelCommand<CustomDirectoriesViewMod
contextViewModel.Add(customDir);
});
}
}
}

View File

@ -1,4 +1,5 @@
using FModel.Framework;
using FModel.Settings;
namespace FModel.ViewModels.Commands;
@ -17,4 +18,4 @@ public class DeleteDirectoryCommand : ViewModelCommand<CustomDirectoriesViewMode
contextViewModel.Delete(index);
}
}
}

View File

@ -38,7 +38,7 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
public override async void Execute(LoadingModesViewModel contextViewModel, object parameter)
{
if (_applicationView.CUE4Parse.GameDirectory.HasNoFile) return;
if (_applicationView.CUE4Parse.Provider.Files.Count <= 0)
if (_applicationView.CUE4Parse.Provider.Keys.Count == 0 && _applicationView.CUE4Parse.Provider.RequiredKeys.Count > 0)
{
FLogger.Append(ELog.Error, () =>
FLogger.Text("An encrypted archive has been found. In order to decrypt it, please specify a working AES encryption key", Constants.WHITE, true));
@ -61,7 +61,6 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
// filter what to show
switch (UserSettings.Default.LoadingMode)
{
case ELoadingMode.Single:
case ELoadingMode.Multiple:
{
var l = (IList) parameter;
@ -155,7 +154,16 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
FLogger.Append(ELog.Information, () =>
FLogger.Text($"Backup file older than current game is '{openFileDialog.FileName.SubstringAfterLast("\\")}'", Constants.WHITE, true));
using var fileStream = new FileStream(openFileDialog.FileName, FileMode.Open);
var mode = UserSettings.Default.LoadingMode;
var entries = ParseBackup(openFileDialog.FileName, mode, cancellationToken);
_applicationView.Status.UpdateStatusLabel($"{mode.ToString()[6..]} Folders & Packages");
_applicationView.CUE4Parse.AssetsFolder.BulkPopulate(entries);
}
private List<VfsEntry> ParseBackup(string path, ELoadingMode mode, CancellationToken cancellationToken = default)
{
using var fileStream = new FileStream(path, FileMode.Open);
using var memoryStream = new MemoryStream();
if (fileStream.ReadUInt32() == _IS_LZ4)
@ -170,25 +178,41 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
using var archive = new FStreamArchive(fileStream.Name, memoryStream);
var entries = new List<VfsEntry>();
var mode = UserSettings.Default.LoadingMode;
switch (mode)
{
case ELoadingMode.AllButNew:
{
var paths = new Dictionary<string, int>();
while (archive.Position < archive.Length)
var paths = new HashSet<string>();
var magic = archive.Read<uint>();
if (magic != BackupManagerViewModel.FBKP_MAGIC)
{
cancellationToken.ThrowIfCancellationRequested();
archive.Position -= sizeof(uint);
while (archive.Position < archive.Length)
{
cancellationToken.ThrowIfCancellationRequested();
archive.Position += 29;
paths[archive.ReadString().ToLower()[1..]] = 0;
archive.Position += 4;
archive.Position += 29;
paths.Add(archive.ReadString().ToLower()[1..]);
archive.Position += 4;
}
}
else
{
var version = archive.Read<EBackupVersion>();
var count = archive.Read<int>();
for (var i = 0; i < count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
archive.Position += sizeof(long) + sizeof(byte);
paths.Add(archive.ReadString().ToLower()[1..]);
}
}
foreach (var (key, value) in _applicationView.CUE4Parse.Provider.Files)
{
cancellationToken.ThrowIfCancellationRequested();
if (value is not VfsEntry entry || paths.ContainsKey(key) || entry.Path.EndsWith(".uexp") ||
if (value is not VfsEntry entry || paths.Contains(key) || entry.Path.EndsWith(".uexp") ||
entry.Path.EndsWith(".ubulk") || entry.Path.EndsWith(".uptnl")) continue;
entries.Add(entry);
@ -199,31 +223,54 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
}
case ELoadingMode.AllButModified:
{
while (archive.Position < archive.Length)
var magic = archive.Read<uint>();
if (magic != BackupManagerViewModel.FBKP_MAGIC)
{
cancellationToken.ThrowIfCancellationRequested();
archive.Position -= sizeof(uint);
while (archive.Position < archive.Length)
{
cancellationToken.ThrowIfCancellationRequested();
archive.Position += 16;
var uncompressedSize = archive.Read<long>();
var isEncrypted = archive.ReadFlag();
archive.Position += 4;
var fullPath = archive.ReadString().ToLower()[1..];
archive.Position += 4;
archive.Position += 16;
var uncompressedSize = archive.Read<long>();
var isEncrypted = archive.ReadFlag();
archive.Position += 4;
var fullPath = archive.ReadString().ToLower()[1..];
archive.Position += 4;
if (fullPath.EndsWith(".uexp") || fullPath.EndsWith(".ubulk") || fullPath.EndsWith(".uptnl") ||
!_applicationView.CUE4Parse.Provider.Files.TryGetValue(fullPath, out var asset) || asset is not VfsEntry entry ||
entry.Size == uncompressedSize && entry.IsEncrypted == isEncrypted)
continue;
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
AddEntry(fullPath, uncompressedSize, isEncrypted, entries);
}
}
else
{
var version = archive.Read<EBackupVersion>();
var count = archive.Read<int>();
for (var i = 0; i < count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var uncompressedSize = archive.Read<long>();
var isEncrypted = archive.ReadFlag();
var fullPath = archive.ReadString().ToLower()[1..];
AddEntry(fullPath, uncompressedSize, isEncrypted, entries);
}
}
break;
}
}
_applicationView.Status.UpdateStatusLabel($"{mode.ToString()[6..]} Folders & Packages");
_applicationView.CUE4Parse.AssetsFolder.BulkPopulate(entries);
return entries;
}
private void AddEntry(string path, long uncompressedSize, bool isEncrypted, List<VfsEntry> entries)
{
if (path.EndsWith(".uexp") || path.EndsWith(".ubulk") || path.EndsWith(".uptnl") ||
!_applicationView.CUE4Parse.Provider.Files.TryGetValue(path, out var asset) || asset is not VfsEntry entry ||
entry.Size == uncompressedSize && entry.IsEncrypted == isEncrypted)
return;
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
}
}

View File

@ -23,13 +23,13 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
switch (parameter)
{
case "Directory_Selector":
contextViewModel.AvoidEmptyGameDirectoryAndSetEGame(true);
contextViewModel.AvoidEmptyGameDirectory(true);
break;
case "Directory_AES":
Helper.OpenWindow<AdonisWindow>("AES Manager", () => new AesManager().Show());
break;
case "Directory_Backup":
Helper.OpenWindow<AdonisWindow>("Backup Manager", () => new BackupManager(contextViewModel.CUE4Parse.Provider.GameName).Show());
Helper.OpenWindow<AdonisWindow>("Backup Manager", () => new BackupManager(contextViewModel.CUE4Parse.Provider.InternalGameName).Show());
break;
case "Directory_ArchivesInfo":
contextViewModel.CUE4Parse.TabControl.AddTab("Archives Info");
@ -42,28 +42,20 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
case "Views_AudioPlayer":
Helper.OpenWindow<AdonisWindow>("Audio Player", () => new AudioPlayer().Show());
break;
case "Views_MapViewer":
Helper.OpenWindow<AdonisWindow>("Map Viewer", () => new MapViewer().Show());
break;
case "Views_ImageMerger":
Helper.OpenWindow<AdonisWindow>("Image Merger", () => new ImageMerger().Show());
break;
case "Settings":
Helper.OpenWindow<AdonisWindow>("Settings", () => new SettingsView().Show());
break;
case "ModelSettings":
UserSettings.Default.LastOpenedSettingTab = contextViewModel.CUE4Parse.Game == FGame.FortniteGame ? 2 : 1;
Helper.OpenWindow<AdonisWindow>("Settings", () => new SettingsView().Show());
break;
case "Help_About":
Helper.OpenWindow<AdonisWindow>("About", () => new About().Show());
break;
case "Help_Donate":
Process.Start(new ProcessStartInfo { FileName = Constants.DONATE_LINK, UseShellExecute = true });
break;
case "Help_Changelog":
UserSettings.Default.ShowChangelog = true;
ApplicationService.ApiEndpointView.FModelApi.CheckForUpdates(UserSettings.Default.UpdateMode);
case "Help_Releases":
Helper.OpenWindow<AdonisWindow>("Releases", () => new UpdateView().Show());
break;
case "Help_BugsReport":
Process.Start(new ProcessStartInfo { FileName = Constants.ISSUE_LINK, UseShellExecute = true });

View File

@ -0,0 +1,40 @@
using System;
using FModel.Framework;
using FModel.Settings;
namespace FModel.ViewModels.Commands;
public class RemindMeCommand : ViewModelCommand<UpdateViewModel>
{
public RemindMeCommand(UpdateViewModel contextViewModel) : base(contextViewModel)
{
}
public override void Execute(UpdateViewModel contextViewModel, object parameter)
{
switch (parameter)
{
case "Days":
// check for update in 3 days
UserSettings.Default.NextUpdateCheck = DateTime.Now.AddDays(3);
break;
case "Week":
// check for update next week (a week starts on Monday)
var delay = (DayOfWeek.Monday - DateTime.Now.DayOfWeek + 7) % 7;
UserSettings.Default.NextUpdateCheck = DateTime.Now.AddDays(delay == 0 ? 7 : delay);
break;
case "Month":
// check for update next month (if today is 31st, it will be 1st of next month)
UserSettings.Default.NextUpdateCheck = DateTime.Now.AddDays(1 - DateTime.Now.Day).AddMonths(1);
break;
case "Never":
// never check for updates
UserSettings.Default.NextUpdateCheck = DateTime.MaxValue;
break;
default:
// reset
UserSettings.Default.NextUpdateCheck = DateTime.Now;
break;
}
}
}

View File

@ -22,6 +22,7 @@ public class RightClickMenuCommand : ViewModelCommand<ApplicationViewModel>
var assetItems = ((IList) parameters[1]).Cast<AssetItem>().ToArray();
if (!assetItems.Any()) return;
var updateUi = assetItems.Length > 1 ? EBulkType.Auto : EBulkType.None;
await _threadWorkerView.Begin(cancellationToken =>
{
switch (trigger)
@ -47,7 +48,7 @@ public class RightClickMenuCommand : ViewModelCommand<ApplicationViewModel>
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Properties);
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Properties | updateUi);
}
break;
case "Assets_Save_Textures":
@ -55,7 +56,7 @@ public class RightClickMenuCommand : ViewModelCommand<ApplicationViewModel>
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Textures);
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Textures | updateUi);
}
break;
case "Assets_Save_Models":
@ -63,7 +64,7 @@ public class RightClickMenuCommand : ViewModelCommand<ApplicationViewModel>
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Meshes | EBulkType.Auto);
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Meshes | updateUi);
}
break;
case "Assets_Save_Animations":
@ -71,7 +72,7 @@ public class RightClickMenuCommand : ViewModelCommand<ApplicationViewModel>
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Animations | EBulkType.Auto);
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Animations | updateUi);
}
break;
}

View File

@ -11,37 +11,6 @@ using FModel.ViewModels.Commands;
namespace FModel.ViewModels;
public class CustomDirectory : ViewModel
{
private string _header;
public string Header
{
get => _header;
set => SetProperty(ref _header, value);
}
private string _directoryPath;
public string DirectoryPath
{
get => _directoryPath;
set => SetProperty(ref _directoryPath, value);
}
public CustomDirectory()
{
Header = string.Empty;
DirectoryPath = string.Empty;
}
public CustomDirectory(string header, string path)
{
Header = header;
DirectoryPath = path;
}
public override string ToString() => Header;
}
public class CustomDirectoriesViewModel : ViewModel
{
private GoToCommand _goToCommand;
@ -54,13 +23,8 @@ public class CustomDirectoriesViewModel : ViewModel
private readonly ObservableCollection<Control> _directories;
public ReadOnlyObservableCollection<Control> Directories { get; }
private readonly FGame _game;
private readonly string _gameDirectoryAtLaunch;
public CustomDirectoriesViewModel(FGame game, string directory)
public CustomDirectoriesViewModel()
{
_game = game;
_gameDirectoryAtLaunch = directory;
_directories = new ObservableCollection<Control>(EnumerateDirectories());
Directories = new ReadOnlyObservableCollection<Control>(_directories);
}
@ -74,6 +38,7 @@ public class CustomDirectoriesViewModel : ViewModel
public void Add(CustomDirectory dir)
{
_directories.Add(new MenuItem { Header = dir.Header, Tag = dir.DirectoryPath, ItemsSource = EnumerateCommands(dir) });
Save();
}
public void Edit(int index, CustomDirectory newDir)
@ -82,25 +47,25 @@ public class CustomDirectoriesViewModel : ViewModel
dir.Header = newDir.Header;
dir.Tag = newDir.DirectoryPath;
Save();
}
public void Delete(int index)
{
_directories.RemoveAt(index);
Save();
}
public void Save()
{
var cd = new List<CustomDirectory>();
var directories = new List<CustomDirectory>();
for (var i = 2; i < _directories.Count; i++)
{
if (_directories[i] is not MenuItem m) continue;
cd.Add(new CustomDirectory(m.Header.ToString(), m.Tag.ToString()));
directories.Add(new CustomDirectory(m.Header.ToString(), m.Tag.ToString()));
}
if (_game == FGame.Unknown && UserSettings.Default.ManualGames.ContainsKey(_gameDirectoryAtLaunch))
UserSettings.Default.ManualGames[_gameDirectoryAtLaunch].CustomDirectories = cd;
else UserSettings.Default.CustomDirectories[_game] = cd;
UserSettings.Default.CurrentDir.Directories = directories;
}
private IEnumerable<Control> EnumerateDirectories()
@ -115,12 +80,7 @@ public class CustomDirectoriesViewModel : ViewModel
};
yield return new Separator();
IList<CustomDirectory> cd;
if (_game == FGame.Unknown && UserSettings.Default.ManualGames.TryGetValue(_gameDirectoryAtLaunch, out var settings))
cd = settings.CustomDirectories;
else cd = UserSettings.Default.CustomDirectories[_game];
foreach (var setting in cd)
foreach (var setting in UserSettings.Default.CurrentDir.Directories)
{
if (setting.DirectoryPath.EndsWith('/'))
setting.DirectoryPath = setting.DirectoryPath[..^1];
@ -167,4 +127,4 @@ public class CustomDirectoriesViewModel : ViewModel
CommandParameter = dir
};
}
}
}

View File

@ -1,8 +1,11 @@
using FModel.Framework;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Data;
using CUE4Parse.UE4.IO;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.UE4.VirtualFileSystem;
@ -72,6 +75,17 @@ public class FileItem : ViewModel
Length = length;
}
public FileItem(IAesVfsReader reader)
{
Name = reader.Name;
Length = reader.Length;
Guid = reader.EncryptionKeyGuid;
IsEncrypted = reader.IsEncrypted;
IsEnabled = false;
Key = string.Empty;
FileCount = reader is IoStoreReader storeReader ? (int) storeReader.TocResource.Header.TocEntryCount - 1 : 0;
}
public override string ToString()
{
return $"{Name} | {Key}";
@ -84,31 +98,35 @@ public class GameDirectoryViewModel : ViewModel
public readonly ObservableCollection<FileItem> DirectoryFiles;
public ICollectionView DirectoryFilesView { get; }
private readonly Regex _hiddenArchives = new(@"^(?!global|pakchunk.+(optional|ondemand)\-).+(pak|utoc)$", // should be universal
RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
public GameDirectoryViewModel()
{
DirectoryFiles = new ObservableCollection<FileItem>();
DirectoryFilesView = new ListCollectionView(DirectoryFiles) { SortDescriptions = { new SortDescription("Name", ListSortDirection.Ascending) } };
}
public void DeactivateAll()
{
foreach (var file in DirectoryFiles)
{
file.IsEnabled = false;
}
}
public void Add(IAesVfsReader reader)
{
Application.Current.Dispatcher.Invoke(() =>
{
DirectoryFiles.Add(new FileItem(reader.Name, reader.Length)
{
Guid = reader.EncryptionKeyGuid,
IsEncrypted = reader.IsEncrypted,
IsEnabled = false,
Key = string.Empty
});
});
if (!_hiddenArchives.IsMatch(reader.Name)) return;
var fileItem = new FileItem(reader);
Application.Current.Dispatcher.Invoke(() => DirectoryFiles.Add(fileItem));
}
public void Verify(IAesVfsReader reader)
{
if (DirectoryFiles.FirstOrDefault(x => x.Name == reader.Name) is not { } file) return;
file.IsEnabled = true;
file.MountPoint = reader.MountPoint;
file.FileCount = reader.FileCount;
}
public void Disable(IAesVfsReader reader)
{
if (DirectoryFiles.FirstOrDefault(x => x.Name == reader.Name) is not { } file) return;
file.IsEnabled = false;
}
}

View File

@ -22,114 +22,97 @@ public class GameSelectorViewModel : ViewModel
{
public string GameName { get; set; }
public string GameDirectory { get; set; }
public EGame OverridedGame { get; set; }
public bool IsManual { get; set; }
// the followings are only used when game is manually added
public AesResponse AesKeys { get; set; }
public EGame OverridedGame { get; set; }
public List<FCustomVersion> OverridedCustomVersions { get; set; }
public Dictionary<string, bool> OverridedOptions { get; set; }
public Dictionary<string, KeyValuePair<string, string>> OverridedMapStructTypes { get; set; }
public IList<CustomDirectory> CustomDirectories { get; set; }
}
private DetectedGame _selectedDetectedGame;
public DetectedGame SelectedDetectedGame
private DirectorySettings _selectedDirectory;
public DirectorySettings SelectedDirectory
{
get => _selectedDetectedGame;
set => SetProperty(ref _selectedDetectedGame, value);
get => _selectedDirectory;
set => SetProperty(ref _selectedDirectory, value);
}
private readonly ObservableCollection<DetectedGame> _autoDetectedGames;
public ReadOnlyObservableCollection<DetectedGame> AutoDetectedGames { get; }
private readonly ObservableCollection<DirectorySettings> _detectedDirectories;
public ReadOnlyObservableCollection<DirectorySettings> DetectedDirectories { get; }
public ReadOnlyObservableCollection<EGame> UeGames { get; }
public GameSelectorViewModel(string gameDirectory)
{
_autoDetectedGames = new ObservableCollection<DetectedGame>(EnumerateDetectedGames().Where(x => x != null));
foreach (var game in UserSettings.Default.ManualGames.Values)
_detectedDirectories = new ObservableCollection<DirectorySettings>(EnumerateDetectedGames().Where(x => x != null));
foreach (var dir in UserSettings.Default.PerDirectory.Values.Where(x => x.IsManual))
{
_autoDetectedGames.Add(game);
_detectedDirectories.Add((DirectorySettings) dir.Clone());
}
AutoDetectedGames = new ReadOnlyObservableCollection<DetectedGame>(_autoDetectedGames);
DetectedDirectories = new ReadOnlyObservableCollection<DirectorySettings>(_detectedDirectories);
if (AutoDetectedGames.FirstOrDefault(x => x.GameDirectory == gameDirectory) is { } detectedGame)
SelectedDetectedGame = detectedGame;
if (DetectedDirectories.FirstOrDefault(x => x.GameDirectory == gameDirectory) is { } detectedGame)
SelectedDirectory = detectedGame;
else if (!string.IsNullOrEmpty(gameDirectory))
AddUnknownGame(gameDirectory);
AddUndetectedDir(gameDirectory);
else
SelectedDetectedGame = AutoDetectedGames.FirstOrDefault();
SelectedDirectory = DetectedDirectories.FirstOrDefault();
UeGames = new ReadOnlyObservableCollection<EGame>(new ObservableCollection<EGame>(EnumerateUeGames()));
}
/// <summary>
/// dedicated to manual games
/// </summary>
public void AddUnknownGame(string gameName, string gameDirectory)
public void AddUndetectedDir(string gameDirectory) => AddUndetectedDir(gameDirectory.SubstringAfterLast('\\'), gameDirectory);
public void AddUndetectedDir(string gameName, string gameDirectory)
{
var game = new DetectedGame
{
GameName = gameName,
GameDirectory = gameDirectory,
IsManual = true,
AesKeys = null,
OverridedGame = EGame.GAME_UE4_LATEST,
OverridedCustomVersions = null,
OverridedOptions = null,
OverridedMapStructTypes = null,
CustomDirectories = new List<CustomDirectory>()
};
UserSettings.Default.ManualGames[gameDirectory] = game;
_autoDetectedGames.Add(game);
SelectedDetectedGame = AutoDetectedGames.Last();
}
public void AddUnknownGame(string gameDirectory)
{
_autoDetectedGames.Add(new DetectedGame { GameName = gameDirectory.SubstringAfterLast('\\'), GameDirectory = gameDirectory });
SelectedDetectedGame = AutoDetectedGames.Last();
var setting = DirectorySettings.Default(gameName, gameDirectory, true);
UserSettings.Default.PerDirectory[gameDirectory] = setting;
_detectedDirectories.Add(setting);
SelectedDirectory = DetectedDirectories.Last();
}
public void DeleteSelectedGame()
{
UserSettings.Default.ManualGames.Remove(SelectedDetectedGame.GameDirectory); // should not be a problem
_autoDetectedGames.Remove(SelectedDetectedGame);
SelectedDetectedGame = AutoDetectedGames.Last();
UserSettings.Default.PerDirectory.Remove(SelectedDirectory.GameDirectory); // should not be a problem
_detectedDirectories.Remove(SelectedDirectory);
SelectedDirectory = DetectedDirectories.Last();
}
private IEnumerable<DetectedGame> EnumerateDetectedGames()
private IEnumerable<EGame> EnumerateUeGames()
=> Enum.GetValues<EGame>()
.GroupBy(value => (int)value)
.Select(group => group.First())
.OrderBy(value => (int)value == ((int)value & ~0xF));
private IEnumerable<DirectorySettings> EnumerateDetectedGames()
{
yield return GetUnrealEngineGame("Fortnite", "\\FortniteGame\\Content\\Paks");
yield return new DetectedGame { GameName = "Fortnite [LIVE]", GameDirectory = Constants._FN_LIVE_TRIGGER };
yield return GetUnrealEngineGame("Pewee", "\\RogueCompany\\Content\\Paks");
yield return GetUnrealEngineGame("Rosemallow", "\\Indiana\\Content\\Paks");
yield return GetUnrealEngineGame("Catnip", "\\OakGame\\Content\\Paks");
yield return GetUnrealEngineGame("AzaleaAlpha", "\\Prospect\\Content\\Paks");
yield return GetUnrealEngineGame("WorldExplorersLive", "\\WorldExplorers\\Content\\Paks");
yield return GetUnrealEngineGame("Newt", "\\g3\\Content\\Paks");
yield return GetUnrealEngineGame("shoebill", "\\SwGame\\Content\\Paks");
yield return GetUnrealEngineGame("Snoek", "\\StateOfDecay2\\Content\\Paks");
yield return GetUnrealEngineGame("a99769d95d8f400baad1f67ab5dfe508", "\\Core\\Platform\\Content\\Paks");
yield return GetUnrealEngineGame("Nebula", "\\BendGame\\Content");
yield return GetUnrealEngineGame("711c5e95dc094ca58e5f16bd48e751d6", "\\MultiVersus\\Content\\Paks");
yield return GetUnrealEngineGame("9361c8c6d2f34b42b5f2f61093eedf48", "\\TslGame\\Content\\Paks");
yield return GetRiotGame("VALORANT", "ShooterGame\\Content\\Paks");
yield return new DetectedGame { GameName = "Valorant [LIVE]", GameDirectory = Constants._VAL_LIVE_TRIGGER };
yield return GetMojangGame("MinecraftDungeons", "\\dungeons\\dungeons\\Dungeons\\Content\\Paks");
yield return GetSteamGame(381210, "\\DeadByDaylight\\Content\\Paks"); // Dead By Daylight
yield return GetSteamGame(578080, "\\TslGame\\Content\\Paks"); // PUBG
yield return GetSteamGame(1172380, "\\SwGame\\Content\\Paks"); // STAR WARS Jedi: Fallen Order™
yield return GetSteamGame(677620, "\\PortalWars\\Content\\Paks"); // Splitgate
yield return GetSteamGame(1172620, "\\Athena\\Content\\Paks"); // Sea of Thieves
yield return GetSteamGame(1665460, "\\pak"); // eFootball 2023
yield return GetRockstarGamesGame("GTA III - Definitive Edition", "\\Gameface\\Content\\Paks");
yield return GetRockstarGamesGame("GTA San Andreas - Definitive Edition", "\\Gameface\\Content\\Paks");
yield return GetRockstarGamesGame("GTA Vice City - Definitive Edition", "\\Gameface\\Content\\Paks");
yield return GetLevelInfiniteGame("tof_launcher", "\\Hotta\\Content\\Paks");
yield return GetUnrealEngineGame("Fortnite", "\\FortniteGame\\Content\\Paks", EGame.GAME_UE5_5);
yield return DirectorySettings.Default("Fortnite [LIVE]", Constants._FN_LIVE_TRIGGER, ue: EGame.GAME_UE5_5);
yield return GetUnrealEngineGame("Pewee", "\\RogueCompany\\Content\\Paks", EGame.GAME_RogueCompany);
yield return GetUnrealEngineGame("Rosemallow", "\\Indiana\\Content\\Paks", EGame.GAME_UE4_21);
yield return GetUnrealEngineGame("Catnip", "\\OakGame\\Content\\Paks", EGame.GAME_Borderlands3);
yield return GetUnrealEngineGame("AzaleaAlpha", "\\Prospect\\Content\\Paks", EGame.GAME_UE4_27);
yield return GetUnrealEngineGame("shoebill", "\\SwGame\\Content\\Paks", EGame.GAME_StarWarsJediFallenOrder);
yield return GetUnrealEngineGame("Snoek", "\\StateOfDecay2\\Content\\Paks", EGame.GAME_StateOfDecay2);
yield return GetUnrealEngineGame("711c5e95dc094ca58e5f16bd48e751d6", "\\MultiVersus\\Content\\Paks", EGame.GAME_UE4_26);
yield return GetUnrealEngineGame("9361c8c6d2f34b42b5f2f61093eedf48", "\\TslGame\\Content\\Paks", EGame.GAME_PlayerUnknownsBattlegrounds);
yield return GetRiotGame("VALORANT", "ShooterGame\\Content\\Paks", EGame.GAME_Valorant);
yield return DirectorySettings.Default("VALORANT [LIVE]", Constants._VAL_LIVE_TRIGGER, ue: EGame.GAME_Valorant);
yield return GetSteamGame(381210, "\\DeadByDaylight\\Content\\Paks", EGame.GAME_UE4_27); // Dead By Daylight
yield return GetSteamGame(578080, "\\TslGame\\Content\\Paks", EGame.GAME_PlayerUnknownsBattlegrounds); // PUBG
yield return GetSteamGame(1172380, "\\SwGame\\Content\\Paks", EGame.GAME_StarWarsJediFallenOrder); // STAR WARS Jedi: Fallen Order™
yield return GetSteamGame(677620, "\\PortalWars\\Content\\Paks", EGame.GAME_Splitgate); // Splitgate
yield return GetSteamGame(1172620, "\\Athena\\Content\\Paks", EGame.GAME_SeaOfThieves); // Sea of Thieves
yield return GetSteamGame(1665460, "\\pak", EGame.GAME_UE4_26); // eFootball 2023
yield return GetRockstarGamesGame("GTA III - Definitive Edition", "\\Gameface\\Content\\Paks", EGame.GAME_GTATheTrilogyDefinitiveEdition);
yield return GetRockstarGamesGame("GTA San Andreas - Definitive Edition", "\\Gameface\\Content\\Paks", EGame.GAME_GTATheTrilogyDefinitiveEdition);
yield return GetRockstarGamesGame("GTA Vice City - Definitive Edition", "\\Gameface\\Content\\Paks", EGame.GAME_GTATheTrilogyDefinitiveEdition);
yield return GetLevelInfiniteGame("tof_launcher", "\\Hotta\\Content\\Paks", EGame.GAME_TowerOfFantasy);
}
private LauncherInstalled _launcherInstalled;
private DetectedGame GetUnrealEngineGame(string gameName, string pakDirectory)
private DirectorySettings GetUnrealEngineGame(string gameName, string pakDirectory, EGame ueVersion)
{
_launcherInstalled ??= GetDriveLauncherInstalls<LauncherInstalled>("ProgramData\\Epic\\UnrealEngineLauncher\\LauncherInstalled.dat");
if (_launcherInstalled?.InstallationList != null)
@ -140,7 +123,7 @@ public class GameSelectorViewModel : ViewModel
if (installationList.AppName.Equals(gameName, StringComparison.OrdinalIgnoreCase) && Directory.Exists(gameDir))
{
Log.Debug("Found {GameName} in LauncherInstalled.dat", gameName);
return new DetectedGame { GameName = installationList.AppName, GameDirectory = gameDir };
return DirectorySettings.Default(installationList.AppName, gameDir, ue: ueVersion);
}
}
}
@ -149,7 +132,7 @@ public class GameSelectorViewModel : ViewModel
}
private RiotClientInstalls _riotClientInstalls;
private DetectedGame GetRiotGame(string gameName, string pakDirectory)
private DirectorySettings GetRiotGame(string gameName, string pakDirectory, EGame ueVersion)
{
_riotClientInstalls ??= GetDriveLauncherInstalls<RiotClientInstalls>("ProgramData\\Riot Games\\RiotClientInstalls.json");
if (_riotClientInstalls is { AssociatedClient: { } })
@ -160,7 +143,7 @@ public class GameSelectorViewModel : ViewModel
if (key.Contains(gameName, StringComparison.OrdinalIgnoreCase) && Directory.Exists(gameDir))
{
Log.Debug("Found {GameName} in RiotClientInstalls.json", gameName);
return new DetectedGame { GameName = gameName, GameDirectory = gameDir };
return DirectorySettings.Default(gameName, gameDir, ue: ueVersion);
}
}
}
@ -168,36 +151,19 @@ public class GameSelectorViewModel : ViewModel
return null;
}
private LauncherSettings _launcherSettings;
private DetectedGame GetMojangGame(string gameName, string pakDirectory)
{
_launcherSettings ??= GetDataLauncherInstalls<LauncherSettings>("\\.minecraft\\launcher_settings.json");
if (_launcherSettings is { ProductLibraryDir: { } })
{
var gameDir = $"{_launcherSettings.ProductLibraryDir}{pakDirectory}";
if (Directory.Exists(gameDir))
{
Log.Debug("Found {GameName} in launcher_settings.json", gameName);
return new DetectedGame { GameName = gameName, GameDirectory = gameDir };
}
}
return null;
}
private DetectedGame GetSteamGame(int id, string pakDirectory)
private DirectorySettings GetSteamGame(int id, string pakDirectory, EGame ueVersion)
{
var steamInfo = SteamDetection.GetSteamGameById(id);
if (steamInfo is not null)
{
Log.Debug("Found {GameName} in steam manifests", steamInfo.Name);
return new DetectedGame { GameName = steamInfo.Name, GameDirectory = $"{steamInfo.GameRoot}{pakDirectory}" };
return DirectorySettings.Default(steamInfo.Name, $"{steamInfo.GameRoot}{pakDirectory}", ue: ueVersion);
}
return null;
}
private DetectedGame GetRockstarGamesGame(string key, string pakDirectory)
private DirectorySettings GetRockstarGamesGame(string key, string pakDirectory, EGame ueVersion)
{
var installLocation = string.Empty;
try
@ -213,13 +179,13 @@ public class GameSelectorViewModel : ViewModel
if (Directory.Exists(gameDir))
{
Log.Debug("Found {GameName} in the registry", key);
return new DetectedGame { GameName = key, GameDirectory = gameDir };
return DirectorySettings.Default(key, gameDir, ue: ueVersion);
}
return null;
}
private DetectedGame GetLevelInfiniteGame(string key, string pakDirectory)
private DirectorySettings GetLevelInfiniteGame(string key, string pakDirectory, EGame ueVersion)
{
var installLocation = string.Empty;
var displayName = string.Empty;
@ -238,7 +204,7 @@ public class GameSelectorViewModel : ViewModel
if (Directory.Exists(gameDir))
{
Log.Debug("Found {GameName} in the registry", key);
return new DetectedGame { GameName = displayName, GameDirectory = gameDir };
return DirectorySettings.Default(displayName, gameDir, ue: ueVersion);
}
return null;
@ -258,19 +224,6 @@ public class GameSelectorViewModel : ViewModel
return default;
}
private T GetDataLauncherInstalls<T>(string jsonFile)
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var launcher = $"{appData}{jsonFile}";
if (File.Exists(launcher))
{
Log.Debug("\"{Launcher}\" found in \"{AppData}\"", launcher, appData);
return JsonConvert.DeserializeObject<T>(File.ReadAllText(launcher));
}
return default;
}
#pragma warning disable 649
private class LauncherInstalled
{

View File

@ -1,876 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Core.i18N;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.GameplayTags;
using CUE4Parse.UE4.Objects.UObject;
using FModel.Creator;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace FModel.ViewModels;
public class MapLayer
{
public SKBitmap Layer;
public bool IsEnabled;
}
public enum EWaypointType
{
Parkour,
TimeTrials
}
public class MapViewerViewModel : ViewModel
{
private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView;
private DiscordHandler _discordHandler => DiscordService.DiscordHandler;
#region BINDINGS
private bool _brPois;
public bool BrPois
{
get => _brPois;
set => SetProperty(ref _brPois, value, "ApolloGameplay_MapPois");
}
private bool _brLandmarks;
public bool BrLandmarks
{
get => _brLandmarks;
set => SetProperty(ref _brLandmarks, value, "ApolloGameplay_MapLandmarks");
}
private bool _brTagsLocation;
public bool BrTagsLocation
{
get => _brTagsLocation;
set => SetProperty(ref _brTagsLocation, value, "ApolloGameplay_TagsLocation");
}
private bool _brPatrolsPath;
public bool BrPatrolsPath
{
get => _brPatrolsPath;
set => SetProperty(ref _brPatrolsPath, value, "ApolloGameplay_PatrolsPath");
}
private bool _brUpgradeBenches;
public bool BrUpgradeBenches
{
get => _brUpgradeBenches;
set => SetProperty(ref _brUpgradeBenches, value, "ApolloGameplay_UpgradeBenches");
}
private bool _brPhonebooths;
public bool BrPhonebooths
{
get => _brPhonebooths;
set => SetProperty(ref _brPhonebooths, value, "ApolloGameplay_Phonebooths");
}
private bool _brVendingMachines;
public bool BrVendingMachines
{
get => _brVendingMachines;
set => SetProperty(ref _brVendingMachines, value, "ApolloGameplay_VendingMachines");
}
private bool _brBountyBoards;
public bool BrBountyBoards
{
get => _brBountyBoards;
set => SetProperty(ref _brBountyBoards, value, "ApolloGameplay_BountyBoards");
}
private bool _prLandmarks;
public bool PrLandmarks
{
get => _prLandmarks;
set => SetProperty(ref _prLandmarks, value, "PapayaGameplay_MapLandmarks");
}
private bool _prCannonball;
public bool PrCannonball
{
get => _prCannonball;
set => SetProperty(ref _prCannonball, value, "PapayaGameplay_CannonballGame");
}
private bool _prSkydive;
public bool PrSkydive
{
get => _prSkydive;
set => SetProperty(ref _prSkydive, value, "PapayaGameplay_SkydiveGame");
}
private bool _prShootingTargets;
public bool PrShootingTargets
{
get => _prShootingTargets;
set => SetProperty(ref _prShootingTargets, value, "PapayaGameplay_ShootingTargets");
}
private bool _prParkour;
public bool PrParkour
{
get => _prParkour;
set => SetProperty(ref _prParkour, value, "PapayaGameplay_ParkourGame");
}
private bool _prTimeTrials;
public bool PrTimeTrials
{
get => _prTimeTrials;
set => SetProperty(ref _prTimeTrials, value, "PapayaGameplay_TimeTrials");
}
private bool _prVendingMachines;
public bool PrVendingMachines
{
get => _prVendingMachines;
set => SetProperty(ref _prVendingMachines, value, "PapayaGameplay_VendingMachines");
}
private bool _prMusicBlocks;
public bool PrMusicBlocks
{
get => _prMusicBlocks;
set => SetProperty(ref _prMusicBlocks, value, "PapayaGameplay_MusicBlocks");
}
#endregion
#region BITMAP IMAGES
private BitmapImage _brMiniMapImage;
private BitmapImage _prMiniMapImage;
private BitmapImage _mapImage;
public BitmapImage MapImage
{
get => _mapImage;
set => SetProperty(ref _mapImage, value);
}
private BitmapImage _brLayerImage;
private BitmapImage _prLayerImage;
private BitmapImage _layerImage;
public BitmapImage LayerImage
{
get => _layerImage;
set => SetProperty(ref _layerImage, value);
}
private const int _widthHeight = 2048;
private const int _brRadius = 141000;
private const int _prRadius = 51000;
private int _mapIndex;
public int MapIndex // 0 is BR, 1 is PR
{
get => _mapIndex;
set
{
SetProperty(ref _mapIndex, value);
TriggerChange();
}
}
#endregion
private const string _FIRST_BITMAP = "MapCheck";
private readonly Dictionary<string, MapLayer>[] _bitmaps; // first bitmap is the displayed map, others are overlays of the map
private readonly CUE4ParseViewModel _cue4Parse;
public MapViewerViewModel(CUE4ParseViewModel cue4Parse)
{
_bitmaps = new[]
{
new Dictionary<string, MapLayer>(),
new Dictionary<string, MapLayer>()
};
_cue4Parse = cue4Parse;
}
public async void Initialize()
{
Utils.Typefaces ??= new Typefaces(_cue4Parse);
_textPaint.Typeface = _fillPaint.Typeface = Utils.Typefaces.Bottom ?? Utils.Typefaces.DisplayName;
await LoadBrMiniMap();
await LoadPrMiniMap();
TriggerChange();
}
public BitmapImage GetImageToSave() => GetImageSource(GetLayerBitmap(true));
private SKBitmap GetLayerBitmap(bool withMap)
{
var ret = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(ret);
foreach (var (key, value) in _bitmaps[MapIndex])
{
if (!value.IsEnabled || !withMap && key == _FIRST_BITMAP)
continue;
c.DrawBitmap(value.Layer, new SKRect(0, 0, _widthHeight, _widthHeight));
}
return ret;
}
protected override bool SetProperty<T>(ref T storage, T value, string propertyName = null) // don't delete, else nothing will update for some reason
{
var ret = base.SetProperty(ref storage, value, propertyName);
if (bool.TryParse(value.ToString(), out var b)) GenericToggle(propertyName, b);
return ret;
}
private async void GenericToggle(string key, bool enabled)
{
if (_bitmaps[MapIndex].TryGetValue(key, out var layer) && layer.Layer != null)
{
layer.IsEnabled = enabled;
}
else if (enabled) // load layer
{
switch (key)
{
case "ApolloGameplay_MapPois":
case "ApolloGameplay_MapLandmarks":
case "PapayaGameplay_MapLandmarks":
await LoadQuestIndicatorData();
break;
case "ApolloGameplay_TagsLocation":
await LoadTagsLocation();
break;
case "ApolloGameplay_PatrolsPath":
await LoadPatrolsPath();
break;
case "ApolloGameplay_UpgradeBenches":
await LoadUpgradeBenches();
break;
case "ApolloGameplay_Phonebooths":
await LoadPhonebooths();
break;
case "ApolloGameplay_VendingMachines":
await LoadBrVendingMachines();
break;
case "ApolloGameplay_BountyBoards":
await LoadBountyBoards();
break;
case "PapayaGameplay_CannonballGame":
await LoadCannonballGame();
break;
case "PapayaGameplay_SkydiveGame":
await LoadSkydiveGame();
break;
case "PapayaGameplay_ShootingTargets":
await LoadShootingTargets();
break;
case "PapayaGameplay_ParkourGame":
await LoadWaypoint(EWaypointType.Parkour);
break;
case "PapayaGameplay_TimeTrials":
await LoadWaypoint(EWaypointType.TimeTrials);
break;
case "PapayaGameplay_VendingMachines":
await LoadPrVendingMachines();
break;
case "PapayaGameplay_MusicBlocks":
await LoadMusicBlocks();
break;
}
_bitmaps[MapIndex][key].IsEnabled = true;
}
switch (MapIndex)
{
case 0:
_brLayerImage = GetImageSource(GetLayerBitmap(false));
break;
case 1:
_prLayerImage = GetImageSource(GetLayerBitmap(false));
break;
}
TriggerChange();
}
private BitmapImage GetImageSource(SKBitmap bitmap)
{
if (bitmap == null) return null;
using var data = bitmap.Encode(SKEncodedImageFormat.Png, 100);
using var stream = data.AsStream();
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream;
image.EndInit();
image.Freeze();
return image;
}
private void TriggerChange()
{
var layerCount = _bitmaps[_mapIndex].Count(x => x.Value.IsEnabled);
var layerString = $"{layerCount} Layer{(layerCount > 1 ? "s" : "")}";
switch (_mapIndex)
{
case 0:
_discordHandler.UpdateButDontSavePresence(null, $"Map Viewer: Battle Royale ({layerString})");
_mapImage = _brMiniMapImage;
_layerImage = _brLayerImage;
break;
case 1:
_discordHandler.UpdateButDontSavePresence(null, $"Map Viewer: Party Royale ({layerString})");
_mapImage = _prMiniMapImage;
_layerImage = _prLayerImage;
break;
}
RaisePropertyChanged(nameof(MapImage));
RaisePropertyChanged(nameof(LayerImage));
}
private readonly SKPaint _textPaint = new()
{
IsAntialias = true, FilterQuality = SKFilterQuality.High,
Color = SKColors.White, TextAlign = SKTextAlign.Center, TextSize = 26
};
private readonly SKPaint _fillPaint = new()
{
IsAntialias = true, FilterQuality = SKFilterQuality.High,
IsStroke = true, Color = SKColors.Black, TextSize = 26,
TextAlign = SKTextAlign.Center
};
private readonly SKPaint _pathPaint = new()
{
IsAntialias = true, FilterQuality = SKFilterQuality.High, IsStroke = true,
Style = SKPaintStyle.Stroke, StrokeWidth = 5, Color = SKColors.Red,
ImageFilter = SKImageFilter.CreateDropShadow(4, 4, 8, 8, SKColors.Black)
};
private FVector2D GetMapPosition(FVector vector, int mapRadius)
{
const int wh = 2048 + 128 + 32;
var nx = (vector.Y + mapRadius) / (mapRadius * 2) * wh;
var ny = (1 - (vector.X + mapRadius) / (mapRadius * 2)) * wh;
return new FVector2D(nx, ny);
}
private async Task LoadBrMiniMap()
{
if (_bitmaps[0].TryGetValue(_FIRST_BITMAP, out var brMap) && brMap.Layer != null)
return; // if map already loaded
await _threadWorkerView.Begin(_ =>
{
if (!Utils.TryLoadObject("FortniteGame/Content/UI/IngameMap/UIMapManagerBR.Default__UIMapManagerBR_C", out UObject mapManager) ||
!mapManager.TryGetValue(out UMaterial mapMaterial, "MapMaterial")) return;
var midTex = mapMaterial.GetFirstTexture();
if ((midTex?.Name ?? string.Empty).Contains("Mask"))
midTex = mapMaterial.GetTextureAtIndex(1);
if (midTex is not UTexture2D tex) return;
_bitmaps[0][_FIRST_BITMAP] = new MapLayer { Layer = Utils.GetBitmap(tex), IsEnabled = true };
_brMiniMapImage = GetImageSource(_bitmaps[0][_FIRST_BITMAP].Layer);
});
}
private async Task LoadPrMiniMap()
{
if (_bitmaps[1].TryGetValue(_FIRST_BITMAP, out var prMap) && prMap.Layer != null)
return; // if map already loaded
await _threadWorkerView.Begin(_ =>
{
if (!Utils.TryLoadObject("FortniteGame/Content/UI/IngameMap/UIMapManagerPapaya.Default__UIMapManagerPapaya_C", out UObject mapManager) ||
!mapManager.TryGetValue(out UMaterial mapMaterial, "MapMaterial") || mapMaterial.GetFirstTexture() is not UTexture2D tex) return;
_bitmaps[1][_FIRST_BITMAP] = new MapLayer { Layer = Utils.GetBitmap(tex), IsEnabled = true };
_prMiniMapImage = GetImageSource(_bitmaps[1][_FIRST_BITMAP].Layer);
});
}
private async Task LoadQuestIndicatorData()
{
await _threadWorkerView.Begin(_ =>
{
var poisBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
var brLandmarksBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
var prLandmarksBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var pois = new SKCanvas(poisBitmap);
using var brLandmarks = new SKCanvas(brLandmarksBitmap);
using var prLandmarks = new SKCanvas(prLandmarksBitmap);
if (Utils.TryLoadObject("FortniteGame/Content/Quests/QuestIndicatorData", out UObject indicatorData) &&
indicatorData.TryGetValue(out FStructFallback[] challengeMapPoiData, "ChallengeMapPoiData"))
{
foreach (var poiData in challengeMapPoiData)
{
if (!poiData.TryGetValue(out FSoftObjectPath discoveryQuest, "DiscoveryQuest") ||
!poiData.TryGetValue(out FText text, "Text") || string.IsNullOrEmpty(text.Text) ||
!poiData.TryGetValue(out FVector worldLocation, "WorldLocation") ||
!poiData.TryGetValue(out FName discoverBackend, "DiscoverObjectiveBackendName")) continue;
var shaper = new CustomSKShaper(_textPaint.Typeface);
if (discoverBackend.Text.Contains("papaya", StringComparison.OrdinalIgnoreCase))
{
_fillPaint.StrokeWidth = 5;
var vector = GetMapPosition(worldLocation, _prRadius);
prLandmarks.DrawPoint(vector.X, vector.Y, _pathPaint);
prLandmarks.DrawShapedText(shaper, text.Text, vector.X, vector.Y - 12.5F, _fillPaint);
prLandmarks.DrawShapedText(shaper, text.Text, vector.X, vector.Y - 12.5F, _textPaint);
}
else if (discoveryQuest.AssetPathName.Text.Contains("landmarks", StringComparison.OrdinalIgnoreCase))
{
_fillPaint.StrokeWidth = 5;
var vector = GetMapPosition(worldLocation, _brRadius);
brLandmarks.DrawPoint(vector.X, vector.Y, _pathPaint);
brLandmarks.DrawShapedText(shaper, text.Text, vector.X, vector.Y - 12.5F, _fillPaint);
brLandmarks.DrawShapedText(shaper, text.Text, vector.X, vector.Y - 12.5F, _textPaint);
}
else
{
_fillPaint.StrokeWidth = 10;
var vector = GetMapPosition(worldLocation, _brRadius);
pois.DrawShapedText(shaper, text.Text.ToUpperInvariant(), vector.X, vector.Y, _fillPaint);
pois.DrawShapedText(shaper, text.Text.ToUpperInvariant(), vector.X, vector.Y, _textPaint);
}
}
}
_bitmaps[0]["ApolloGameplay_MapPois"] = new MapLayer { Layer = poisBitmap, IsEnabled = false };
_bitmaps[0]["ApolloGameplay_MapLandmarks"] = new MapLayer { Layer = brLandmarksBitmap, IsEnabled = false };
_bitmaps[1]["PapayaGameplay_MapLandmarks"] = new MapLayer { Layer = prLandmarksBitmap, IsEnabled = false };
});
}
private async Task LoadPatrolsPath()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var patrolsPathBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(patrolsPathBitmap);
var exports = Utils.LoadExports("/NPCLibrary/LevelOverlays/Artemis_Overlay_S22_NPCLibrary");
foreach (var export in exports)
{
if (!export.ExportType.Equals("FortAthenaPatrolPath", StringComparison.OrdinalIgnoreCase) ||
!export.TryGetValue(out FPackageIndex[] patrolPoints, "PatrolPoints")) continue;
var displayName = export.Name["FortAthenaPatrolPath_".Length..];
if (export.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags") && gameplayTags.GameplayTags.Length > 0)
displayName = gameplayTags.GameplayTags[0].Text["Athena.AI.SpawnLocation.Tandem.".Length..];
if (!Utils.TryGetPackageIndexExport(patrolPoints[0], out UObject uObject) ||
!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var path = new SKPath();
var vector = GetMapPosition(relativeLocation, _brRadius);
path.MoveTo(vector.X, vector.Y);
for (var i = 1; i < patrolPoints.Length; i++)
{
if (!Utils.TryGetPackageIndexExport(patrolPoints[i], out uObject) ||
!uObject.TryGetValue(out rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out uObject) ||
!uObject.TryGetValue(out relativeLocation, "RelativeLocation")) continue;
vector = GetMapPosition(relativeLocation, _brRadius);
path.LineTo(vector.X, vector.Y);
}
c.DrawPath(path, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[0]["ApolloGameplay_PatrolsPath"] = new MapLayer { Layer = patrolsPathBitmap, IsEnabled = false };
});
}
private async Task LoadCannonballGame()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var cannonballBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(cannonballBitmap);
var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_CannonballGame");
foreach (var export in exports)
{
if (!export.ExportType.Equals("BP_CannonballGame_Target_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("CannonballGame_VehicleSpawner_C", StringComparison.OrdinalIgnoreCase)) continue;
if (!export.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out UObject uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var displayName = Utils.GetLocalizedResource("", "D998BEF44F051E0885C6C58565934BEA", "Cannonball");
var vector = GetMapPosition(relativeLocation, _prRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[1]["PapayaGameplay_CannonballGame"] = new MapLayer { Layer = cannonballBitmap, IsEnabled = false };
});
}
private async Task LoadSkydiveGame()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var skydiveBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(skydiveBitmap);
var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_SkydiveGame");
foreach (var export in exports)
{
if (!export.ExportType.Equals("BP_Waypoint_Papaya_Skydive_Start_C", StringComparison.OrdinalIgnoreCase)) continue;
if (!export.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!export.TryGetValue(out FText minigameActivityName, "MinigameActivityName") ||
!Utils.TryGetPackageIndexExport(rootComponent, out UObject uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _prRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(minigameActivityName.Text, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(minigameActivityName.Text, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[1]["PapayaGameplay_SkydiveGame"] = new MapLayer { Layer = skydiveBitmap, IsEnabled = false };
});
}
private async Task LoadShootingTargets()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var shootingTargetsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(shootingTargetsBitmap);
var bDone = false;
var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_ShootingTargets");
foreach (var export in exports)
{
if (!export.ExportType.Equals("PapayaShootingTarget_C", StringComparison.OrdinalIgnoreCase)) continue;
if (!export.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out UObject uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _prRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
if (bDone) continue;
bDone = true;
c.DrawText("Shooting Target", vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText("Shooting Target", vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[1]["PapayaGameplay_ShootingTargets"] = new MapLayer { Layer = shootingTargetsBitmap, IsEnabled = false };
});
}
private async Task LoadWaypoint(EWaypointType type)
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var waypointBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(waypointBitmap);
string file;
string name;
switch (type)
{
case EWaypointType.Parkour:
file = "PapayaGameplay_ParkourGame";
name = "Parkour";
break;
case EWaypointType.TimeTrials:
file = "PapayaGameplay_TimeTrials";
name = "Time Trials";
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
var path = new SKPath();
var exports = Utils.LoadExports($"/PapayaGameplay/LevelOverlays/{file}");
foreach (var export in exports)
{
if (!export.ExportType.Equals("BP_Waypoint_Parent_Papaya_C", StringComparison.OrdinalIgnoreCase)) continue;
if (!export.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out UObject root) ||
!root.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _prRadius);
if (path.IsEmpty || export.TryGetValue(out bool startsTrial, "StartsTrial") && startsTrial)
{
path.MoveTo(vector.X, vector.Y);
c.DrawText(name, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(name, vector.X, vector.Y - 12.5F, _textPaint);
}
else if (export.TryGetValue(out bool endsTrial, "EndsTrial") && endsTrial)
{
path.LineTo(vector.X, vector.Y);
c.DrawPath(path, _pathPaint);
path = new SKPath();
}
else path.LineTo(vector.X, vector.Y);
}
_bitmaps[1][file] = new MapLayer { Layer = waypointBitmap, IsEnabled = false };
});
}
private async Task LoadPrVendingMachines()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var set = new HashSet<string>();
var timeTrialsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(timeTrialsBitmap);
var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_VendingMachines");
foreach (var export in exports)
{
if (!export.ExportType.Equals("B_Papaya_VendingMachine_Boat_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_BoogieBomb_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_Burger_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_CrashPad_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_FishingPole_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_Grappler_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_Jetpack_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_PaintGrenade_Blue_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_PaintGrenade_Red_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_PaintLauncher_Blue_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_PaintLauncher_Red_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_PlungerBow_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_Quad_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Papaya_VendingMachine_Tomato_C", StringComparison.OrdinalIgnoreCase)) continue;
if (!export.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out UObject root) ||
!root.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var name = export.ExportType.SubstringAfter("B_Papaya_VendingMachine_").SubstringBeforeLast("_C");
var vector = GetMapPosition(relativeLocation, _prRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
if (!set.Add(name)) continue;
c.DrawText(name, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(name, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[1]["PapayaGameplay_VendingMachines"] = new MapLayer { Layer = timeTrialsBitmap, IsEnabled = false };
});
}
private async Task LoadMusicBlocks()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var shootingTargetsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(shootingTargetsBitmap);
var bDone = false;
var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_MusicBlocks");
foreach (var export in exports)
{
if (!export.ExportType.Equals("MusicBlock_Piano3_Papaya_C", StringComparison.OrdinalIgnoreCase)) continue;
if (!export.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out UObject uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _prRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
if (bDone) continue;
bDone = true;
c.DrawText("Music Blocks", vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText("Music Blocks", vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[1]["PapayaGameplay_MusicBlocks"] = new MapLayer { Layer = shootingTargetsBitmap, IsEnabled = false };
});
}
private async Task LoadUpgradeBenches()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var upgradeBenchesBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(upgradeBenchesBitmap);
var exports = Utils.LoadExports("/NPCLibrary/LevelOverlays/Artemis_Overlay_S19_UpgradeBenches");
foreach (var export in exports)
{
if (!export.ExportType.Equals("B_Athena_Spawner_UpgradeStation_C", StringComparison.OrdinalIgnoreCase)) continue;
var displayName = export.Name["B_Athena_Spawner_".Length..];
if (!export.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out UObject uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _brRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[0]["ApolloGameplay_UpgradeBenches"] = new MapLayer { Layer = upgradeBenchesBitmap, IsEnabled = false };
});
}
private async Task LoadPhonebooths()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var phoneboothsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(phoneboothsBitmap);
var exports = Utils.LoadExports("/NPCLibrary/LevelOverlays/Apollo_Terrain_NPCLibrary_Stations_Phonebooths");
foreach (var export in exports)
{
if (!export.ExportType.Equals("B_Athena_Spawner_Payphone_C", StringComparison.OrdinalIgnoreCase)) continue;
var displayName = export.Name["B_Athena_Spawner_".Length..];
if (!export.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out UObject uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _brRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[0]["ApolloGameplay_Phonebooths"] = new MapLayer { Layer = phoneboothsBitmap, IsEnabled = false };
});
}
private async Task LoadBrVendingMachines()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var vendingMachinesBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(vendingMachinesBitmap);
var exports = Utils.LoadExports("/NPCLibrary/LevelOverlays/Artemis_Overlay_S19_ServiceStations");
foreach (var export in exports)
{
if (!export.ExportType.Equals("B_Athena_Spawner_VendingMachine_MendingOnly_C", StringComparison.OrdinalIgnoreCase) &&
!export.ExportType.Equals("B_Athena_Spawner_VendingMachine_Random_C", StringComparison.OrdinalIgnoreCase)) continue;
var displayName = $"{(export.ExportType.Contains("Mending") ? "MM" : "WOM")}_{export.Name["B_Athena_Spawner_VendingMachine_Random".Length..]}";
if (!export.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out UObject uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _brRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[0]["ApolloGameplay_VendingMachines"] = new MapLayer { Layer = vendingMachinesBitmap, IsEnabled = false };
});
}
private async Task LoadBountyBoards()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
var bountyBoardsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(bountyBoardsBitmap);
var exports = Utils.LoadExports("/Bounties/Maps/BB_Overlay_ServiceStations");
foreach (var export in exports)
{
if (!export.ExportType.Equals("B_Bounties_Spawner_BountyBoard_C", StringComparison.OrdinalIgnoreCase)) continue;
var displayName = $"BountyBoard_{export.Name["BP_BountyBoard_C_".Length..]}";
if (!export.TryGetValue(out FPackageIndex rootComponent, "RootComponent") ||
!Utils.TryGetPackageIndexExport(rootComponent, out UObject uObject) ||
!uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue;
var vector = GetMapPosition(relativeLocation, _brRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[0]["ApolloGameplay_BountyBoards"] = new MapLayer { Layer = bountyBoardsBitmap, IsEnabled = false };
});
}
private async Task LoadTagsLocation()
{
await _threadWorkerView.Begin(_ =>
{
_fillPaint.StrokeWidth = 5;
if (!Utils.TryLoadObject("FortniteGame/Content/Quests/QuestTagToLocationDataRows.QuestTagToLocationDataRows", out UDataTable locationData))
return;
var tagsLocationBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(tagsLocationBitmap);
foreach (var (key, uObject) in locationData.RowMap)
{
if (key.Text.StartsWith("Athena.Location.POI", StringComparison.OrdinalIgnoreCase) ||
key.Text.StartsWith("Athena.Location.Unnamed", StringComparison.OrdinalIgnoreCase) ||
key.Text.Contains(".Tandem.", StringComparison.OrdinalIgnoreCase) ||
!uObject.TryGetValue(out FVector worldLocation, "WorldLocation")) continue;
var parts = key.Text.Split('.');
var displayName = parts[^2];
if (!int.TryParse(parts[^1], out var _))
displayName += " " + parts[^1];
var vector = GetMapPosition(worldLocation, _brRadius);
c.DrawPoint(vector.X, vector.Y, _pathPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint);
c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint);
}
_bitmaps[0]["ApolloGameplay_TagsLocation"] = new MapLayer { Layer = tagsLocationBitmap, IsEnabled = false };
});
}
}

View File

@ -1,26 +1,22 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Linq;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.UE4.Objects.Core.Serialization;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Textures;
using CUE4Parse_Conversion.UEFormat.Enums;
using CUE4Parse.UE4.Assets.Exports.Material;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels.ApiEndpoints.Models;
namespace FModel.ViewModels;
public class SettingsViewModel : ViewModel
{
private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView;
private ApiEndpointViewModel _apiEndpointView => ApplicationService.ApiEndpointView;
private readonly DiscordHandler _discordHandler = DiscordService.DiscordHandler;
private bool _useCustomOutputFolders;
@ -30,24 +26,6 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _useCustomOutputFolders, value);
}
private EUpdateMode _selectedUpdateMode;
public EUpdateMode SelectedUpdateMode
{
get => _selectedUpdateMode;
set => SetProperty(ref _selectedUpdateMode, value);
}
private string _selectedPreset;
public string SelectedPreset
{
get => _selectedPreset;
set
{
SetProperty(ref _selectedPreset, value);
RaisePropertyChanged("EnableElements");
}
}
private ETexturePlatform _selectedUePlatform;
public ETexturePlatform SelectedUePlatform
{
@ -62,36 +40,36 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _selectedUeGame, value);
}
private List<FCustomVersion> _selectedCustomVersions;
public List<FCustomVersion> SelectedCustomVersions
private IList<FCustomVersion> _selectedCustomVersions;
public IList<FCustomVersion> SelectedCustomVersions
{
get => _selectedCustomVersions;
set => SetProperty(ref _selectedCustomVersions, value);
}
private Dictionary<string, bool> _selectedOptions;
public Dictionary<string, bool> SelectedOptions
private IDictionary<string, bool> _selectedOptions;
public IDictionary<string, bool> SelectedOptions
{
get => _selectedOptions;
set => SetProperty(ref _selectedOptions, value);
}
private Dictionary<string, KeyValuePair<string, string>> _selectedMapStructTypes;
public Dictionary<string, KeyValuePair<string, string>> SelectedMapStructTypes
private IDictionary<string, KeyValuePair<string, string>> _selectedMapStructTypes;
public IDictionary<string, KeyValuePair<string, string>> SelectedMapStructTypes
{
get => _selectedMapStructTypes;
set => SetProperty(ref _selectedMapStructTypes, value);
}
private FEndpoint _aesEndpoint;
public FEndpoint AesEndpoint
private EndpointSettings _aesEndpoint;
public EndpointSettings AesEndpoint
{
get => _aesEndpoint;
set => SetProperty(ref _aesEndpoint, value);
}
private FEndpoint _mappingEndpoint;
public FEndpoint MappingEndpoint
private EndpointSettings _mappingEndpoint;
public EndpointSettings MappingEndpoint
{
get => _mappingEndpoint;
set => SetProperty(ref _mappingEndpoint, value);
@ -136,7 +114,12 @@ public class SettingsViewModel : ViewModel
public EMeshFormat SelectedMeshExportFormat
{
get => _selectedMeshExportFormat;
set => SetProperty(ref _selectedMeshExportFormat, value);
set
{
SetProperty(ref _selectedMeshExportFormat, value);
RaisePropertyChanged(nameof(SocketSettingsEnabled));
RaisePropertyChanged(nameof(CompressionSettingsEnabled));
}
}
private ESocketFormat _selectedSocketExportFormat;
@ -146,6 +129,13 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _selectedSocketExportFormat, value);
}
private EFileCompressionFormat _selectedCompressionFormat;
public EFileCompressionFormat SelectedCompressionFormat
{
get => _selectedCompressionFormat;
set => SetProperty(ref _selectedCompressionFormat, value);
}
private ELodFormat _selectedLodExportFormat;
public ELodFormat SelectedLodExportFormat
{
@ -167,8 +157,9 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _selectedTextureExportFormat, value);
}
public ReadOnlyObservableCollection<EUpdateMode> UpdateModes { get; private set; }
public ObservableCollection<string> Presets { get; private set; }
public bool SocketSettingsEnabled => SelectedMeshExportFormat == EMeshFormat.ActorX;
public bool CompressionSettingsEnabled => SelectedMeshExportFormat == EMeshFormat.UEFormat;
public ReadOnlyObservableCollection<EGame> UeGames { get; private set; }
public ReadOnlyObservableCollection<ELanguage> AssetLanguages { get; private set; }
public ReadOnlyObservableCollection<EAesReload> AesReloads { get; private set; }
@ -177,15 +168,12 @@ public class SettingsViewModel : ViewModel
public ReadOnlyObservableCollection<EIconStyle> CosmeticStyles { get; private set; }
public ReadOnlyObservableCollection<EMeshFormat> MeshExportFormats { get; private set; }
public ReadOnlyObservableCollection<ESocketFormat> SocketExportFormats { get; private set; }
public ReadOnlyObservableCollection<EFileCompressionFormat> CompressionFormats { get; private set; }
public ReadOnlyObservableCollection<ELodFormat> LodExportFormats { get; private set; }
public ReadOnlyObservableCollection<EMaterialFormat> MaterialExportFormats { get; private set; }
public ReadOnlyObservableCollection<ETextureFormat> TextureExportFormats { get; private set; }
public ReadOnlyObservableCollection<ETexturePlatform> Platforms { get; private set; }
public bool EnableElements => SelectedPreset == Constants._NO_PRESET_TRIGGER;
private readonly FGame _game;
private Game _gamePreset;
private string _outputSnapshot;
private string _rawDataSnapshot;
private string _propertiesSnapshot;
@ -193,27 +181,26 @@ public class SettingsViewModel : ViewModel
private string _audioSnapshot;
private string _modelSnapshot;
private string _gameSnapshot;
private EUpdateMode _updateModeSnapshot;
private string _presetSnapshot;
private ETexturePlatform _uePlatformSnapshot;
private EGame _ueGameSnapshot;
private List<FCustomVersion> _customVersionsSnapshot;
private Dictionary<string, bool> _optionsSnapshot;
private Dictionary<string, KeyValuePair<string, string>> _mapStructTypesSnapshot;
private IList<FCustomVersion> _customVersionsSnapshot;
private IDictionary<string, bool> _optionsSnapshot;
private IDictionary<string, KeyValuePair<string, string>> _mapStructTypesSnapshot;
private ELanguage _assetLanguageSnapshot;
private ECompressedAudio _compressedAudioSnapshot;
private EIconStyle _cosmeticStyleSnapshot;
private EMeshFormat _meshExportFormatSnapshot;
private ESocketFormat _socketExportFormatSnapshot;
private EFileCompressionFormat _compressionFormatSnapshot;
private ELodFormat _lodExportFormatSnapshot;
private EMaterialFormat _materialExportFormatSnapshot;
private ETextureFormat _textureExportFormatSnapshot;
private bool _mappingsUpdate = false;
public SettingsViewModel(FGame game)
public SettingsViewModel()
{
_game = game;
}
public void Initialize()
@ -225,46 +212,30 @@ public class SettingsViewModel : ViewModel
_audioSnapshot = UserSettings.Default.AudioDirectory;
_modelSnapshot = UserSettings.Default.ModelDirectory;
_gameSnapshot = UserSettings.Default.GameDirectory;
_updateModeSnapshot = UserSettings.Default.UpdateMode;
_presetSnapshot = UserSettings.Default.Presets[_game];
_uePlatformSnapshot = UserSettings.Default.OverridedPlatform;
if (_game == FGame.Unknown && UserSettings.Default.ManualGames.TryGetValue(_gameSnapshot, out var settings))
{
_ueGameSnapshot = settings.OverridedGame;
_customVersionsSnapshot = settings.OverridedCustomVersions;
_optionsSnapshot = settings.OverridedOptions;
_mapStructTypesSnapshot = settings.OverridedMapStructTypes;
}
else
{
_ueGameSnapshot = UserSettings.Default.OverridedGame[_game];
_customVersionsSnapshot = UserSettings.Default.OverridedCustomVersions[_game];
_optionsSnapshot = UserSettings.Default.OverridedOptions[_game];
_mapStructTypesSnapshot = UserSettings.Default.OverridedMapStructTypes[_game];
}
_uePlatformSnapshot = UserSettings.Default.CurrentDir.TexturePlatform;
_ueGameSnapshot = UserSettings.Default.CurrentDir.UeVersion;
_customVersionsSnapshot = UserSettings.Default.CurrentDir.Versioning.CustomVersions;
_optionsSnapshot = UserSettings.Default.CurrentDir.Versioning.Options;
_mapStructTypesSnapshot = UserSettings.Default.CurrentDir.Versioning.MapStructTypes;
if (UserSettings.Default.CustomEndpoints.TryGetValue(_game, out var endpoints))
AesEndpoint = UserSettings.Default.CurrentDir.Endpoints[0];
MappingEndpoint = UserSettings.Default.CurrentDir.Endpoints[1];
MappingEndpoint.PropertyChanged += (_, args) =>
{
AesEndpoint = endpoints[0];
MappingEndpoint = endpoints[1];
MappingEndpoint.PropertyChanged += (_, args) =>
{
if (!_mappingsUpdate)
_mappingsUpdate = args.PropertyName is "Overwrite" or "FilePath";
};
}
if (!_mappingsUpdate)
_mappingsUpdate = args.PropertyName is "Overwrite" or "FilePath";
};
_assetLanguageSnapshot = UserSettings.Default.AssetLanguage;
_compressedAudioSnapshot = UserSettings.Default.CompressedAudioMode;
_cosmeticStyleSnapshot = UserSettings.Default.CosmeticStyle;
_meshExportFormatSnapshot = UserSettings.Default.MeshExportFormat;
_socketExportFormatSnapshot = UserSettings.Default.SocketExportFormat;
_compressionFormatSnapshot = UserSettings.Default.CompressionFormat;
_lodExportFormatSnapshot = UserSettings.Default.LodExportFormat;
_materialExportFormatSnapshot = UserSettings.Default.MaterialExportFormat;
_textureExportFormatSnapshot = UserSettings.Default.TextureExportFormat;
SelectedUpdateMode = _updateModeSnapshot;
SelectedPreset = _presetSnapshot;
SelectedUePlatform = _uePlatformSnapshot;
SelectedUeGame = _ueGameSnapshot;
SelectedCustomVersions = _customVersionsSnapshot;
@ -275,14 +246,13 @@ public class SettingsViewModel : ViewModel
SelectedCosmeticStyle = _cosmeticStyleSnapshot;
SelectedMeshExportFormat = _meshExportFormatSnapshot;
SelectedSocketExportFormat = _socketExportFormatSnapshot;
SelectedCompressionFormat = _selectedCompressionFormat;
SelectedLodExportFormat = _lodExportFormatSnapshot;
SelectedMaterialExportFormat = _materialExportFormatSnapshot;
SelectedTextureExportFormat = _textureExportFormatSnapshot;
SelectedAesReload = UserSettings.Default.AesReload;
SelectedDiscordRpc = UserSettings.Default.DiscordRpc;
UpdateModes = new ReadOnlyObservableCollection<EUpdateMode>(new ObservableCollection<EUpdateMode>(EnumerateUpdateModes()));
Presets = new ObservableCollection<string>(EnumeratePresets());
UeGames = new ReadOnlyObservableCollection<EGame>(new ObservableCollection<EGame>(EnumerateUeGames()));
AssetLanguages = new ReadOnlyObservableCollection<ELanguage>(new ObservableCollection<ELanguage>(EnumerateAssetLanguages()));
AesReloads = new ReadOnlyObservableCollection<EAesReload>(new ObservableCollection<EAesReload>(EnumerateAesReloads()));
@ -291,59 +261,13 @@ public class SettingsViewModel : ViewModel
CosmeticStyles = new ReadOnlyObservableCollection<EIconStyle>(new ObservableCollection<EIconStyle>(EnumerateCosmeticStyles()));
MeshExportFormats = new ReadOnlyObservableCollection<EMeshFormat>(new ObservableCollection<EMeshFormat>(EnumerateMeshExportFormat()));
SocketExportFormats = new ReadOnlyObservableCollection<ESocketFormat>(new ObservableCollection<ESocketFormat>(EnumerateSocketExportFormat()));
CompressionFormats = new ReadOnlyObservableCollection<EFileCompressionFormat>(new ObservableCollection<EFileCompressionFormat>(EnumerateCompressionFormat()));
LodExportFormats = new ReadOnlyObservableCollection<ELodFormat>(new ObservableCollection<ELodFormat>(EnumerateLodExportFormat()));
MaterialExportFormats = new ReadOnlyObservableCollection<EMaterialFormat>(new ObservableCollection<EMaterialFormat>(EnumerateMaterialExportFormat()));
TextureExportFormats = new ReadOnlyObservableCollection<ETextureFormat>(new ObservableCollection<ETextureFormat>(EnumerateTextureExportFormat()));
Platforms = new ReadOnlyObservableCollection<ETexturePlatform>(new ObservableCollection<ETexturePlatform>(EnumerateUePlatforms()));
}
public async Task InitPresets(string gameName)
{
await _threadWorkerView.Begin(cancellationToken =>
{
if (string.IsNullOrEmpty(gameName)) return;
_gamePreset = _apiEndpointView.FModelApi.GetGames(cancellationToken, gameName);
});
if (_gamePreset?.Versions == null) return;
foreach (var version in _gamePreset.Versions.Keys)
{
Presets.Add(version);
}
}
public void SwitchPreset(string key)
{
if (_gamePreset?.Versions == null || !_gamePreset.Versions.TryGetValue(key, out var version)) return;
SelectedUeGame = version.GameEnum.ToEnum(EGame.GAME_UE4_LATEST);
SelectedCustomVersions = new List<FCustomVersion>();
foreach (var (guid, v) in version.CustomVersions)
{
SelectedCustomVersions.Add(new FCustomVersion { Key = new FGuid(guid), Version = v });
}
SelectedOptions = new Dictionary<string, bool>();
foreach (var (k, v) in version.Options)
{
SelectedOptions[k] = v;
}
SelectedMapStructTypes = new Dictionary<string, KeyValuePair<string, string>>();
foreach (var (k, v) in version.MapStructTypes)
{
SelectedMapStructTypes[k] = v;
}
}
public void ResetPreset()
{
SelectedUeGame = _ueGameSnapshot;
SelectedCustomVersions = _customVersionsSnapshot;
SelectedOptions = _optionsSnapshot;
SelectedMapStructTypes = _mapStructTypesSnapshot;
}
public bool Save(out List<SettingsOut> whatShouldIDo)
{
var restart = false;
@ -353,8 +277,6 @@ public class SettingsViewModel : ViewModel
whatShouldIDo.Add(SettingsOut.ReloadLocres);
if (_mappingsUpdate)
whatShouldIDo.Add(SettingsOut.ReloadMappings);
if (_updateModeSnapshot != SelectedUpdateMode)
whatShouldIDo.Add(SettingsOut.CheckForUpdates);
if (_ueGameSnapshot != SelectedUeGame || _customVersionsSnapshot != SelectedCustomVersions ||
_uePlatformSnapshot != SelectedUePlatform || _optionsSnapshot != SelectedOptions || // combobox
@ -368,29 +290,18 @@ public class SettingsViewModel : ViewModel
_gameSnapshot != UserSettings.Default.GameDirectory) // textbox
restart = true;
UserSettings.Default.UpdateMode = SelectedUpdateMode;
UserSettings.Default.Presets[_game] = SelectedPreset;
UserSettings.Default.OverridedPlatform = SelectedUePlatform;
if (_game == FGame.Unknown && UserSettings.Default.ManualGames.ContainsKey(UserSettings.Default.GameDirectory))
{
UserSettings.Default.ManualGames[UserSettings.Default.GameDirectory].OverridedGame = SelectedUeGame;
UserSettings.Default.ManualGames[UserSettings.Default.GameDirectory].OverridedCustomVersions = SelectedCustomVersions;
UserSettings.Default.ManualGames[UserSettings.Default.GameDirectory].OverridedOptions = SelectedOptions;
UserSettings.Default.ManualGames[UserSettings.Default.GameDirectory].OverridedMapStructTypes = SelectedMapStructTypes;
}
else
{
UserSettings.Default.OverridedGame[_game] = SelectedUeGame;
UserSettings.Default.OverridedCustomVersions[_game] = SelectedCustomVersions;
UserSettings.Default.OverridedOptions[_game] = SelectedOptions;
UserSettings.Default.OverridedMapStructTypes[_game] = SelectedMapStructTypes;
}
UserSettings.Default.CurrentDir.UeVersion = SelectedUeGame;
UserSettings.Default.CurrentDir.TexturePlatform = SelectedUePlatform;
UserSettings.Default.CurrentDir.Versioning.CustomVersions = SelectedCustomVersions;
UserSettings.Default.CurrentDir.Versioning.Options = SelectedOptions;
UserSettings.Default.CurrentDir.Versioning.MapStructTypes = SelectedMapStructTypes;
UserSettings.Default.AssetLanguage = SelectedAssetLanguage;
UserSettings.Default.CompressedAudioMode = SelectedCompressedAudio;
UserSettings.Default.CosmeticStyle = SelectedCosmeticStyle;
UserSettings.Default.MeshExportFormat = SelectedMeshExportFormat;
UserSettings.Default.SocketExportFormat = SelectedSocketExportFormat;
UserSettings.Default.CompressionFormat = SelectedCompressionFormat;
UserSettings.Default.LodExportFormat = SelectedLodExportFormat;
UserSettings.Default.MaterialExportFormat = SelectedMaterialExportFormat;
UserSettings.Default.TextureExportFormat = SelectedTextureExportFormat;
@ -403,12 +314,11 @@ public class SettingsViewModel : ViewModel
return restart;
}
private IEnumerable<EUpdateMode> EnumerateUpdateModes() => Enum.GetValues<EUpdateMode>();
private IEnumerable<string> EnumeratePresets()
{
yield return Constants._NO_PRESET_TRIGGER;
}
private IEnumerable<EGame> EnumerateUeGames() => Enum.GetValues<EGame>();
private IEnumerable<EGame> EnumerateUeGames()
=> Enum.GetValues<EGame>()
.GroupBy(value => (int)value)
.Select(group => group.First())
.OrderBy(value => (int)value == ((int)value & ~0xF));
private IEnumerable<ELanguage> EnumerateAssetLanguages() => Enum.GetValues<ELanguage>();
private IEnumerable<EAesReload> EnumerateAesReloads() => Enum.GetValues<EAesReload>();
private IEnumerable<EDiscordRpc> EnumerateDiscordRpcs() => Enum.GetValues<EDiscordRpc>();
@ -416,6 +326,7 @@ public class SettingsViewModel : ViewModel
private IEnumerable<EIconStyle> EnumerateCosmeticStyles() => Enum.GetValues<EIconStyle>();
private IEnumerable<EMeshFormat> EnumerateMeshExportFormat() => Enum.GetValues<EMeshFormat>();
private IEnumerable<ESocketFormat> EnumerateSocketExportFormat() => Enum.GetValues<ESocketFormat>();
private IEnumerable<EFileCompressionFormat> EnumerateCompressionFormat() => Enum.GetValues<EFileCompressionFormat>();
private IEnumerable<ELodFormat> EnumerateLodExportFormat() => Enum.GetValues<ELodFormat>();
private IEnumerable<EMaterialFormat> EnumerateMaterialExportFormat() => Enum.GetValues<EMaterialFormat>();
private IEnumerable<ETextureFormat> EnumerateTextureExportFormat() => Enum.GetValues<ETextureFormat>();

View File

@ -68,9 +68,13 @@ public class TabImage : ViewModel
Image = null;
return;
}
_bmp = bitmap;
using var data = _bmp.Encode(NoAlpha ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 100);
using var data = _bmp.Encode(NoAlpha ? ETextureFormat.Jpeg : UserSettings.Default.TextureExportFormat, 100);
using var stream = new MemoryStream(ImageBuffer = data.ToArray(), false);
if (UserSettings.Default.TextureExportFormat == ETextureFormat.Tga)
return;
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
@ -86,6 +90,8 @@ public class TabImage : ViewModel
public class TabItem : ViewModel
{
public string ParentExportType { get; private set; }
private string _header;
public string Header
{
@ -211,29 +217,58 @@ public class TabItem : ViewModel
private GoToCommand _goToCommand;
public GoToCommand GoToCommand => _goToCommand ??= new GoToCommand(null);
public TabItem(string header, string directory)
public TabItem(string header, string directory, string parentExportType)
{
Header = header;
Directory = directory;
ParentExportType = parentExportType;
_images = new ObservableCollection<TabImage>();
}
public void ClearImages()
public void SoftReset(string header, string directory)
{
Header = header;
Directory = directory;
ParentExportType = string.Empty;
ScrollTrigger = null;
Application.Current.Dispatcher.Invoke(() =>
{
_images.Clear();
SelectedImage = null;
RaisePropertyChanged("HasMultipleImages");
Document ??= new TextDocument();
Document.Text = string.Empty;
});
}
public void AddImage(UTexture texture, bool save, bool updateUi)
=> AddImage(texture.Name, texture.RenderNearestNeighbor, texture.Decode(UserSettings.Default.OverridedPlatform), save, updateUi);
public void AddImage(string name, bool rnn, SKBitmap[] img, bool save, bool updateUi)
{
foreach (var i in img) AddImage(name, rnn, i, save, updateUi);
var appendLayerNumber = false;
var img = new SKBitmap[1];
if (texture is UTexture2DArray textureArray)
{
img = textureArray.DecodeTextureArray(UserSettings.Default.CurrentDir.TexturePlatform);
appendLayerNumber = true;
}
else
{
img[0] = texture.Decode(UserSettings.Default.CurrentDir.TexturePlatform);
if (texture is UTextureCube)
{
img[0] = img[0]?.ToPanorama();
}
}
AddImage(texture.Name, texture.RenderNearestNeighbor, img, save, updateUi, appendLayerNumber);
}
public void AddImage(string name, bool rnn, SKBitmap[] img, bool save, bool updateUi, bool appendLayerNumber = false)
{
for (var i = 0; i < img.Length; i++)
{
AddImage($"{name}{(appendLayerNumber ? $"_{i}" : "")}", rnn, img[i], save, updateUi);
}
}
public void AddImage(string name, bool rnn, SKBitmap img, bool save, bool updateUi)
@ -266,20 +301,20 @@ public class TabItem : ViewModel
});
}
public void ResetDocumentText()
{
Application.Current.Dispatcher.Invoke(() =>
{
Document ??= new TextDocument();
Document.Text = string.Empty;
});
}
public void SaveImage() => SaveImage(SelectedImage, true);
private void SaveImage(TabImage image, bool updateUi)
{
if (image == null) return;
var fileName = $"{image.ExportName}.png";
var ext = UserSettings.Default.TextureExportFormat switch
{
ETextureFormat.Png => ".png",
ETextureFormat.Jpeg => ".jpg",
ETextureFormat.Tga => ".tga",
_ => ".png"
};
var fileName = image.ExportName + ext;
var path = Path.Combine(UserSettings.Default.TextureDirectory,
UserSettings.Default.KeepDirectoryStructure ? Directory : "", fileName!).Replace('\\', '/');
@ -360,12 +395,13 @@ public class TabControlViewModel : ViewModel
SelectedTab = TabsItems.FirstOrDefault();
}
public void AddTab(string header = null, string directory = null)
public void AddTab(string header = null, string directory = null, string parentExportType = null)
{
if (!CanAddTabs) return;
var h = header ?? "New Tab";
var d = directory ?? string.Empty;
var p = parentExportType ?? string.Empty;
if (SelectedTab is { Header : "New Tab" })
{
SelectedTab.Header = h;
@ -375,7 +411,7 @@ public class TabControlViewModel : ViewModel
Application.Current.Dispatcher.Invoke(() =>
{
_tabItems.Add(new TabItem(h, d));
_tabItems.Add(new TabItem(h, d, p));
SelectedTab = _tabItems.Last();
});
}
@ -437,6 +473,6 @@ public class TabControlViewModel : ViewModel
private static IEnumerable<TabItem> EnumerateTabs()
{
yield return new TabItem("New Tab", string.Empty);
yield return new TabItem("New Tab", string.Empty, string.Empty);
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Data;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels.ApiEndpoints.Models;
using FModel.ViewModels.Commands;
using FModel.Views.Resources.Converters;
namespace FModel.ViewModels;
public class UpdateViewModel : ViewModel
{
private ApiEndpointViewModel _apiEndpointView => ApplicationService.ApiEndpointView;
private RemindMeCommand _remindMeCommand;
public RemindMeCommand RemindMeCommand => _remindMeCommand ??= new RemindMeCommand(this);
public RangeObservableCollection<GitHubCommit> Commits { get; }
public ICollectionView CommitsView { get; }
public UpdateViewModel()
{
Commits = new RangeObservableCollection<GitHubCommit>();
CommitsView = new ListCollectionView(Commits)
{
GroupDescriptions = { new PropertyGroupDescription("Commit.Author.Date", new DateTimeToDateConverter()) }
};
if (UserSettings.Default.NextUpdateCheck < DateTime.Now)
RemindMeCommand.Execute(this, null);
}
public async Task Load()
{
Commits.AddRange(await _apiEndpointView.GitHubApi.GetCommitHistoryAsync());
var qa = await _apiEndpointView.GitHubApi.GetReleaseAsync("qa");
qa.Assets.OrderByDescending(x => x.CreatedAt).First().IsLatest = true;
foreach (var asset in qa.Assets)
{
var commitSha = asset.Name.SubstringBeforeLast(".zip");
var commit = Commits.FirstOrDefault(x => x.Sha == commitSha);
if (commit != null)
{
commit.Asset = asset;
}
else
{
Commits.Add(new GitHubCommit
{
Sha = commitSha,
Commit = new Commit
{
Message = $"FModel ({commitSha[..7]})",
Author = new Author { Name = asset.Uploader.Login, Date = asset.CreatedAt }
},
Author = asset.Uploader,
Asset = asset
});
}
}
}
public void DownloadLatest()
{
Commits.FirstOrDefault(x => x.Asset.IsLatest)?.Download();
}
}

View File

@ -4,7 +4,8 @@
xmlns:local="clr-namespace:FModel"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
IconVisibility="Collapsed" Width="500" SizeToContent="Height">
IconVisibility="Collapsed" Width="500" SizeToContent="Height"
Loaded="OnLoaded">
<adonisControls:AdonisWindow.Style>
<Style TargetType="adonisControls:AdonisWindow" BasedOn="{StaticResource {x:Type adonisControls:AdonisWindow}}" >
<Setter Property="Title" Value="About" />
@ -24,31 +25,27 @@
</StackPanel>
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="History" FontSize="15" FontWeight="700" Foreground="#9DA3DD" FontStretch="Expanded" />
<TextBlock Text="Description" FontSize="15" FontWeight="700" Foreground="#9DA3DD" FontStretch="Expanded" />
<TextBlock Text="&#09;&#09;&#09;&#09;" FontSize="25" FontWeight="700" Height="2" Foreground="Transparent" HorizontalAlignment="Center" />
</StackPanel>
<TextBlock FontSize="12" Foreground="#727272" TextWrapping="Wrap" Margin="0 0 0 30"
Text="Since the release in March 2019, we've continuously added new features and improved old ones. It started as a simple UE4 file explorer to parse packages and, as things progressed, became really popular to data-mine games and create items icons. FModel 4 is now the most complete and well-made version so far. It features dozens of settings for you to use to make FModel your own." />
<TextBlock FontSize="12" Foreground="#727272" TextWrapping="Wrap" Margin="0 0 0 30" Text="{Binding DescriptionLabel}" />
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="Contributors" FontSize="15" FontWeight="700" Foreground="#9DA3DD" FontStretch="Expanded" />
<TextBlock Text="&#09;&#09;&#09;&#09;" FontSize="25" FontWeight="700" Height="2" Foreground="Transparent" HorizontalAlignment="Center" />
</StackPanel>
<TextBlock FontSize="12" Foreground="#727272" TextWrapping="Wrap" Margin="0 0 0 30"
Text="With the help of Waddlesworth, Fabian, Maiky, GMatrix, Amr, Officer, Tiger, Mang0e, and a lot more, this project continues to exist. If you're making money off of FModel consider donating to sustain FModel's continued improvements." />
<TextBlock FontSize="12" Foreground="#727272" TextWrapping="Wrap" Margin="0 0 0 30" Text="{Binding ContributorsLabel}" />
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="Donators" FontSize="15" FontWeight="700" Foreground="#9DA3DD" FontStretch="Expanded" />
<TextBlock Text="&#09;&#09;&#09;&#09;" FontSize="25" FontWeight="700" Height="2" Foreground="Transparent" HorizontalAlignment="Center" />
</StackPanel>
<TextBlock FontSize="12" Foreground="#727272" TextWrapping="Wrap" Margin="0 0 0 30"
Text="TheGameVlog &#x2665;, Quentin &#x2665;, Maiky &#x2665;, HYPEX &#x2665;, AnimatedAspect, Evan, VenomLeaks, Fortnite.GG, JayKey, Fevers, Netu, Laggy, s0ll, RazTracker, Mikey, kyle, Yanteh, Shiina, SexyNutella, Alexander, Jinx, koba, Tector, imatrix, LamZykoss, Frenzy Leaks, LlamaLeaks, xplore, XTigerHyperX, FunGames, WeLoveFortnite." />
<TextBlock FontSize="12" Foreground="#727272" TextWrapping="Wrap" Margin="0 0 0 30" Text="{Binding DonatorsLabel}" />
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="Powered by" FontSize="15" FontWeight="700" Foreground="#9DA3DD" FontStretch="Expanded" />
<TextBlock Text="&#09;&#09;&#09;&#09;" FontSize="25" FontWeight="700" Height="2" Foreground="Transparent" HorizontalAlignment="Center" />
</StackPanel>
<TextBlock FontSize="12" Foreground="#727272" TextWrapping="Wrap" Margin="0 0 0 30"
Text="CUE4Parse, BenBot, Fortnite-Api, AdonisUI, AvalonEdit, CSCore, NVorbis, VgmStream, RestSharp, Serilog, Discord, K4os.Compression.LZ4, Ookii.Dialogs, Newtonsoft.Json, ..." />
<TextBlock FontSize="12" Foreground="#727272" TextWrapping="Wrap" Margin="0 0 0 30" Text="{Binding ReferencesLabel}" />
</StackPanel>
</adonisControls:AdonisWindow>

View File

@ -1,9 +1,20 @@
namespace FModel.Views;
using System.Windows;
using FModel.ViewModels;
namespace FModel.Views;
public partial class About
{
private readonly AboutViewModel _viewModel;
public About()
{
DataContext = _viewModel = new AboutViewModel();
InitializeComponent();
}
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
await _viewModel.Initialize();
}
}

View File

@ -80,14 +80,8 @@
<Button Grid.Column="1" MinWidth="78" Margin="0 0 12 0" IsDefault="True" IsCancel="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="OK" Click="OnClick" />
<Button Grid.Column="2" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Refresh" Click="OnRefreshAes">
<Button.Visibility>
<!-- if aes custom endpoint is enabled, make this visible -->
<MultiBinding Converter="{x:Static converters:EndpointToTypeConverter.Instance}">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type local:Views.AesManager}}" Path="DataContext" />
<Binding Source="{x:Static local:EEndpointType.Aes}" />
</MultiBinding>
</Button.Visibility>
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Refresh" Click="OnRefreshAes"
Visibility="{Binding Converter={x:Static converters:EndpointToTypeConverter.Instance}, ConverterParameter={x:Static local:EEndpointType.Aes}}">
</Button>
</Grid>
</Border>

View File

@ -1,5 +1,5 @@
using System.Windows;
using FModel.ViewModels;
using FModel.Settings;
namespace FModel.Views;
@ -9,7 +9,7 @@ public partial class CustomDir
{
DataContext = customDir;
InitializeComponent();
Activate();
WpfSuckMyDick.Focus();
WpfSuckMyDick.SelectAll();
@ -20,4 +20,4 @@ public partial class CustomDir
DialogResult = true;
Close();
}
}
}

View File

@ -1,6 +1,7 @@
<adonisControls:AdonisWindow x:Class="FModel.Views.DirectorySelector"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
@ -36,37 +37,51 @@
<GroupBox Grid.Row="1" adonisExtensions:LayerExtension.Layer="2" Margin="10 10 10 18"
Padding="{adonisUi:Space 0}" Background="Transparent">
<StackPanel>
<Grid Margin="0 5">
<Grid x:Name="Hello" Margin="0 5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Detected Game" VerticalAlignment="Center" />
<ComboBox Grid.Column="2" ItemsSource="{Binding AutoDetectedGames}"
VerticalAlignment="Center" SelectedItem="{Binding SelectedDetectedGame, Mode=TwoWay}">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Detected Game" VerticalAlignment="Center" Margin="0 0 0 5" />
<controls:FilterableComboBox Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="3"
ItemsSource="{Binding DetectedDirectories}" Margin="0 0 0 5"
Style="{StaticResource UComboBox}"
adonisExtensions:WatermarkExtension.Watermark="Search for a game..."
VerticalAlignment="Center" SelectedItem="{Binding SelectedDirectory, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding GameName, Converter={x:Static converters:StringToGameConverter.Instance}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
<Grid x:Name="Hello" Margin="0 0 0 5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
</controls:FilterableComboBox>
<TextBox Grid.Column="0" Text="{Binding SelectedDetectedGame.GameDirectory, Mode=TwoWay}" />
<Button Grid.Column="2" Content="..." HorizontalAlignment="Right" Click="OnBrowseDirectories" />
<Button Grid.Column="3" Style="{DynamicResource {x:Static adonisUi:Styles.AccentButton}}" Padding="0"
Click="OnDeleteDirectory" Width="{Binding ActualWidth, ElementName=OkGuysButWhoFuckingAsked}"
Visibility="{Binding SelectedDetectedGame.IsManual, Converter={StaticResource BoolToVisibilityConverter}}"
ToolTip="Delete Game" Margin="5 0 0 0">
<TextBlock Grid.Row="1" Grid.Column="0" Text="UE Versions" VerticalAlignment="Center" Margin="0 0 0 5" />
<controls:FilterableComboBox Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="3" Margin="0 0 0 5"
ItemsSource="{Binding UeGames}"
Style="{StaticResource UComboBox}"
VerticalAlignment="Center" SelectedItem="{Binding SelectedDirectory.UeVersion, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={x:Static converters:EnumToStringConverter.Instance}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</controls:FilterableComboBox>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Directory" VerticalAlignment="Center" />
<TextBox Grid.Row="2" Grid.Column="2" Text="{Binding SelectedDirectory.GameDirectory, Mode=TwoWay}" />
<Button Grid.Row="2" Grid.Column="4" Content="..." HorizontalAlignment="Right" Click="OnBrowseDirectories" />
<Button Grid.Row="2" Grid.Column="4" Style="{DynamicResource {x:Static adonisUi:Styles.AccentButton}}" Padding="0"
Click="OnDeleteDirectory" Width="{Binding ActualWidth, ElementName=OkGuysButWhoFuckingAsked}" ToolTip="Delete Game"
Visibility="{Binding SelectedDirectory.IsManual, Converter={StaticResource BoolToVisibilityConverter}}">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource RemoveIcon}" />
@ -74,7 +89,9 @@
</Viewbox>
</Button>
</Grid>
<Separator Style="{StaticResource CustomSeparator}" Tag="ADD UNDETECTED GAME" />
<Expander ExpandDirection="Down" IsExpanded="False">
<Grid MaxWidth="{Binding ActualWidth, ElementName=Hello}">
<Grid.RowDefinitions>

View File

@ -1,6 +1,10 @@
using FModel.ViewModels;
using System;
using System.IO;
using System.Linq;
using FModel.ViewModels;
using Ookii.Dialogs.Wpf;
using System.Windows;
using CUE4Parse.Utils;
namespace FModel.Views;
@ -29,7 +33,7 @@ public partial class DirectorySelector
var folderBrowser = new VistaFolderBrowserDialog {ShowNewFolderButton = false};
if (folderBrowser.ShowDialog() == true)
{
gameLauncherViewModel.AddUnknownGame(folderBrowser.SelectedPath);
gameLauncherViewModel.AddUndetectedDir(folderBrowser.SelectedPath);
}
}
@ -39,17 +43,39 @@ public partial class DirectorySelector
if (folderBrowser.ShowDialog() == true)
{
HelloGameMyNameIsDirectory.Text = folderBrowser.SelectedPath;
// install_folder/
// ├─ Engine/
// ├─ GameName/
// │ ├─ Binaries/
// │ ├─ Content/
// │ │ ├─ Paks/
// our goal is to get the GameName folder
var currentFolder = folderBrowser.SelectedPath.SubstringAfterLast('\\');
if (currentFolder.Equals("Paks", StringComparison.InvariantCulture))
{
var dir = new DirectoryInfo(folderBrowser.SelectedPath);
if (dir.Parent is { Parent: not null } &&
dir.Parent.Name.Equals("Content", StringComparison.InvariantCulture) &&
dir.Parent.Parent.GetDirectories().Any(x => x.Name == "Binaries"))
{
HelloMyNameIsGame.Text = dir.Parent.Parent.Name;
return;
}
}
HelloMyNameIsGame.Text = folderBrowser.SelectedPath.SubstringAfterLast('\\');
}
}
private void OnAddDirectory(object sender, RoutedEventArgs e)
{
if (DataContext is not GameSelectorViewModel gameLauncherViewModel||
if (DataContext is not GameSelectorViewModel gameLauncherViewModel ||
string.IsNullOrEmpty(HelloMyNameIsGame.Text) ||
string.IsNullOrEmpty(HelloGameMyNameIsDirectory.Text))
return;
gameLauncherViewModel.AddUnknownGame(HelloMyNameIsGame.Text, HelloGameMyNameIsDirectory.Text);
gameLauncherViewModel.AddUndetectedDir(HelloMyNameIsGame.Text, HelloGameMyNameIsDirectory.Text);
HelloMyNameIsGame.Clear();
HelloGameMyNameIsDirectory.Clear();
}
@ -61,4 +87,4 @@ public partial class DirectorySelector
gameLauncherViewModel.DeleteSelectedGame();
}
}
}

View File

@ -142,7 +142,7 @@ public partial class ImageMerger
var fileBrowser = new OpenFileDialog
{
Title = "Add image(s)",
InitialDirectory = $"{UserSettings.Default.OutputDirectory}\\Exports",
InitialDirectory = Path.Combine(UserSettings.Default.OutputDirectory, "Exports"),
Multiselect = true,
Filter = "Image Files (*.png,*.bmp,*.jpg,*.jpeg,*.jfif,*.jpe,*.tiff,*.tif)|*.png;*.bmp;*.jpg;*.jpeg;*.jfif;*.jpe;*.tiff;*.tif|All Files (*.*)|*.*"
};

View File

@ -1,126 +0,0 @@
<adonisControls:AdonisWindow x:Class="FModel.Views.MapViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FModel"
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" SizeToContent="Width" ResizeMode="CanMinimize" Closing="OnClosing"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.75'}"
MinWidth="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.50'}">
<adonisControls:AdonisWindow.Style>
<Style TargetType="adonisControls:AdonisWindow" BasedOn="{StaticResource {x:Type adonisControls:AdonisWindow}}" >
<Setter Property="Title" Value="Map Viewer" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
<controls:OnTagDataTemplateSelector x:Key="TagTemplateSelector" />
<DataTemplate x:Key="BrTemplate">
<StackPanel VerticalAlignment="Center" Margin="25 0">
<CheckBox Content="Points Of Interest" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrPois}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Landmarks" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrLandmarks}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<!-- <CheckBox Content="Tags Location" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrTagsLocation}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
<!-- <CheckBox Content="Patrols Path" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrPatrolsPath}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
<!-- <CheckBox Content="Upgrade Benches" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrUpgradeBenches}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
<!-- <CheckBox Content="Phonebooths" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrPhonebooths}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
<!-- <CheckBox Content="Weapon-o-matic/Mending Machines" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrVendingMachines}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
<!-- <CheckBox Content="Bounty Boards" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrBountyBoards}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="PrTemplate">
<StackPanel VerticalAlignment="Center" Margin="25 0">
<CheckBox Content="Landmarks" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrLandmarks}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Cannonball" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrCannonball}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Skydive" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrSkydive}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Shooting Targets" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrShootingTargets}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Parkour" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrParkour}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Time Trials" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrTimeTrials}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Vending Machines" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrVendingMachines}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<!-- <CheckBox Content="Music Blocks" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrMusicBlocks}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="300" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TreeView Grid.Row="0" Grid.RowSpan="3" x:Name="MapTree" SelectedItemChanged="OnSelectedItemChanged"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}">
<TreeViewItem Tag="BrTemplate" IsSelected="True">
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center" Margin="-20 4 7.5 4">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource GliderIcon}" />
</Canvas>
</Viewbox>
<TextBlock Text="Battle Royale" HorizontalAlignment="Left" VerticalAlignment="Center" />
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
<TreeViewItem Tag="PrTemplate">
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center" Margin="-20 4 7.5 4">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource AnchorIcon}" />
</Canvas>
</Viewbox>
<TextBlock Text="Party Royale" HorizontalAlignment="Left" VerticalAlignment="Center" />
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
</TreeView>
<Grid Grid.Row="1" HorizontalAlignment="Stretch">
<ContentControl ContentTemplateSelector="{StaticResource TagTemplateSelector}" Content="{Binding SelectedItem.Tag, ElementName=MapTree}" />
</Grid>
<Button Grid.Row="2" Content="Save Image" Margin="5" IsEnabled="{Binding Status.IsReady}" VerticalAlignment="Bottom" Click="OnClick" />
</Grid>
<Grid Grid.Column="1" HorizontalAlignment="Stretch">
<controls:MagnifierManager.Magnifier>
<controls:Magnifier Radius="200" ZoomFactor=".4" BorderBrush="{DynamicResource {x:Static adonisUi:Brushes.AccentBrush}}" BorderThickness="1" />
</controls:MagnifierManager.Magnifier>
<TextBlock Text="Minimap is loading, please wait..." HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Image UseLayoutRounding="True" Source="{Binding MapViewer.MapImage}" HorizontalAlignment="Right" VerticalAlignment="Center"
Visibility="{Binding Status.IsReady, Converter={StaticResource BoolToVisibilityConverter}}" />
<Image UseLayoutRounding="True" Source="{Binding MapViewer.LayerImage}" HorizontalAlignment="Right" VerticalAlignment="Center"
Visibility="{Binding Status.IsReady, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</Grid>
</adonisControls:AdonisWindow>

View File

@ -1,78 +0,0 @@
using System.ComponentModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using FModel.Extensions;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels;
using FModel.Views.Resources.Controls;
using Microsoft.Win32;
using Serilog;
namespace FModel.Views;
public partial class MapViewer
{
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
public MapViewer()
{
DataContext = _applicationView;
_applicationView.MapViewer.Initialize();
InitializeComponent();
}
private void OnClosing(object sender, CancelEventArgs e) => DiscordService.DiscordHandler.UpdateToSavedPresence();
private void OnClick(object sender, RoutedEventArgs e)
{
if (_applicationView.MapViewer.MapImage == null) return;
var path = Path.Combine(UserSettings.Default.TextureDirectory, "MiniMap.png");
var saveFileDialog = new SaveFileDialog
{
Title = "Save MiniMap",
FileName = "MiniMap.png",
InitialDirectory = path.SubstringBeforeLast('\\'),
Filter = "PNG Files (*.png)|*.png|All Files (*.*)|*.*"
};
if (!saveFileDialog.ShowDialog().GetValueOrDefault()) return;
path = saveFileDialog.FileName;
using var fileStream = new FileStream(path, FileMode.Create);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(_applicationView.MapViewer.GetImageToSave()));
encoder.Save(fileStream);
if (File.Exists(path))
{
Log.Information("MiniMap.png successfully saved");
FLogger.Append(ELog.Information, () => FLogger.Text("Successfully saved 'MiniMap.png'", Constants.WHITE, true));
}
else
{
Log.Error("MiniMap.png could not be saved");
FLogger.Append(ELog.Error, () => FLogger.Text("Could not save 'MiniMap.png'", Constants.WHITE, true));
}
}
private void OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var i = 0;
foreach (var item in MapTree.Items)
{
if (item is not TreeViewItem { IsSelected: true })
{
i++;
continue;
}
_applicationView.MapViewer.MapIndex = i;
break;
}
}
}

View File

@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using FModel.Extensions;
using ICSharpCode.AvalonEdit.Rendering;
namespace FModel.Views.Resources.Controls;
@ -29,8 +30,10 @@ public class GamePathElementGenerator : VisualLineElementGenerator
public override VisualLineElement ConstructElement(int offset)
{
var m = FindMatch(offset);
if (!m.Success || m.Index != 0) return null;
if (!m.Success || m.Index != 0 ||
!m.Groups.TryGetValue("target", out var g)) return null;
return m.Groups.TryGetValue("target", out var g) ? new GamePathVisualLineText(g.Value, CurrentContext.VisualLine, g.Length + g.Index + 1) : null;
var parentExportType = CurrentContext.Document.GetParentExportType(offset);
return new GamePathVisualLineText(g.Value, parentExportType, CurrentContext.VisualLine, g.Length + g.Index + 1);
}
}
}

View File

@ -16,14 +16,16 @@ public class GamePathVisualLineText : VisualLineText
private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView;
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
public delegate void GamePathOnClick(string gamePath);
public delegate void GamePathOnClick(string gamePath, string parentExportType);
public event GamePathOnClick OnGamePathClicked;
private readonly string _gamePath;
private readonly string _parentExportType;
public GamePathVisualLineText(string gamePath, VisualLine parentVisualLine, int length) : base(parentVisualLine, length)
public GamePathVisualLineText(string gamePath, string parentExportType, VisualLine parentVisualLine, int length) : base(parentVisualLine, length)
{
_gamePath = gamePath;
_parentExportType = parentExportType;
}
public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
@ -56,14 +58,14 @@ public class GamePathVisualLineText : VisualLineText
if (e.Handled || OnGamePathClicked == null)
return;
OnGamePathClicked(_gamePath);
OnGamePathClicked(_gamePath, _parentExportType);
e.Handled = true;
}
protected override VisualLineText CreateInstance(int length)
{
var a = new GamePathVisualLineText(_gamePath, ParentVisualLine, length);
a.OnGamePathClicked += async gamePath =>
var a = new GamePathVisualLineText(_gamePath, _parentExportType, ParentVisualLine, length);
a.OnGamePathClicked += async (gamePath, parentExportType) =>
{
var obj = gamePath.SubstringAfterLast('.');
var package = gamePath.SubstringBeforeLast('.');
@ -80,17 +82,17 @@ public class GamePathVisualLineText : VisualLineText
}
else
{
lineNumber = a.ParentVisualLine.Document.Text.GetLineNumber(obj);
lineNumber = a.ParentVisualLine.Document.Text.GetNameLineNumber(obj);
line = a.ParentVisualLine.Document.GetLineByNumber(lineNumber);
}
AvalonEditor.YesWeEditor.Select(line.Offset, line.Length);
AvalonEditor.YesWeEditor.ScrollToLine(lineNumber);
}
else
{
await _threadWorkerView.Begin(cancellationToken =>
_applicationView.CUE4Parse.ExtractAndScroll(cancellationToken, fullPath, obj));
_applicationView.CUE4Parse.ExtractAndScroll(cancellationToken, fullPath, obj, parentExportType));
}
};
return a;

View File

@ -126,7 +126,7 @@ public partial class AvalonEditor
if (!tabItem.ShouldScroll) return;
var lineNumber = avalonEditor.Document.Text.GetLineNumber(tabItem.ScrollTrigger);
var lineNumber = avalonEditor.Document.Text.GetNameLineNumber(tabItem.ScrollTrigger);
var line = avalonEditor.Document.GetLineByNumber(lineNumber);
avalonEditor.Select(line.Offset, line.Length);
avalonEditor.ScrollToLine(lineNumber);
@ -223,10 +223,9 @@ public partial class AvalonEditor
private void OnTabClose(object sender, EventArgs eventArgs)
{
if (eventArgs is not TabControlViewModel.TabEventArgs e || e.TabToRemove.Document == null)
if (eventArgs is not TabControlViewModel.TabEventArgs e || e.TabToRemove.Document?.FileName is not { } fileName)
return;
var fileName = e.TabToRemove.Document.FileName;
if (_savedCarets.ContainsKey(fileName))
_savedCarets.Remove(fileName);
}

View File

@ -0,0 +1,131 @@
<UserControl x:Class="FModel.Views.Resources.Controls.CommitControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Border BorderThickness="1" CornerRadius="0.5"
BorderBrush="{DynamicResource {x:Static adonisUi:Brushes.Layer4BackgroundBrush}}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="5" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Commit.Message, Converter={x:Static converters:CommitMessageConverter.Instance}, ConverterParameter=Title}" FontWeight="Bold" TextWrapping="Wrap" />
<TextBlock Grid.Row="1" Text="{Binding Commit.Message, Converter={x:Static converters:CommitMessageConverter.Instance}, ConverterParameter=Description}" TextWrapping="Wrap">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Commit.Message, Converter={x:Static converters:CommitMessageConverter.Instance}, ConverterParameter=Description}" Value="">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding Author.AvatarUrl}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="2" FontSize="11">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} committed {1}">
<Binding Path="Author.Login" />
<Binding Path="Commit.Author.Date" Converter="{x:Static converters:RelativeDateTimeConverter.Instance}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</Grid>
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="15" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
BorderThickness="1"
CornerRadius="2.5"
Padding="5,2"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock FontSize="9" Foreground="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType=Border}}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Asset.IsLatest}" Value="True">
<Setter Property="Text" Value="Latest" />
</DataTrigger>
<DataTrigger Binding="{Binding IsCurrent}" Value="True">
<Setter Property="Text" Value="Current" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Border.Style>
<Style TargetType="Border">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding Asset.IsLatest}" Value="True">
<Setter Property="Visibility" Value="Visible" />
<Setter Property="BorderBrush" Value="#3fb950" />
<Setter Property="Background" Value="#0f3fb950" />
</DataTrigger>
<DataTrigger Binding="{Binding IsCurrent}" Value="True">
<Setter Property="Visibility" Value="Visible" />
<Setter Property="BorderBrush" Value="#3f92b9" />
<Setter Property="Background" Value="#0f3f92b9" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<controls:CommitDownloaderControl Grid.Column="2" Commit="{Binding}">
<controls:CommitDownloaderControl.Style>
<Style TargetType="controls:CommitDownloaderControl">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsDownloadable}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</controls:CommitDownloaderControl.Style>
</controls:CommitDownloaderControl>
</Grid>
</Grid>
</Border>
</UserControl>

View File

@ -0,0 +1,12 @@
using System.Windows.Controls;
namespace FModel.Views.Resources.Controls;
public partial class CommitControl : UserControl
{
public CommitControl()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,55 @@
<UserControl x:Class="FModel.Views.Resources.Controls.CommitDownloaderControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="15" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Viewbox Grid.Column="0" Width="16" Height="16" VerticalAlignment="Center" HorizontalAlignment="Center">
<Canvas Width="16" Height="16">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}"
Data="{StaticResource ArchiveIcon}" />
</Canvas>
</Viewbox>
<StackPanel Grid.Column="2">
<TextBlock Text="Size" FontSize="10" />
<TextBlock FontSize="10" Text="{Binding Asset.Size, Converter={x:Static converters:SizeToStringConverter.Instance}}" />
</StackPanel>
</Grid>
<Button Grid.Column="2" Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarButton}}" ToolTip="Download"
Height="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=Grid}}"
Width="{Binding ActualHeight, RelativeSource={RelativeSource Self}}"
IsEnabled="{Binding IsCurrent, Converter={x:Static converters:InvertBooleanConverter.Instance}}"
Click="OnDownload">
<Viewbox Width="16" Height="16" VerticalAlignment="Center" HorizontalAlignment="Center">
<Canvas Width="16" Height="16">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}"
Data="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z" />
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}"
Data="M11.78 4.72a.749.749 0 1 1-1.06 1.06L8.75 3.811V9.5a.75.75 0 0 1-1.5 0V3.811L5.28 5.78a.749.749 0 1 1-1.06-1.06l3.25-3.25a.749.749 0 0 1 1.06 0l3.25 3.25Z" />
</Canvas>
</Viewbox>
</Button>
</Grid>
</UserControl>

View File

@ -0,0 +1,28 @@
using System.Windows;
using System.Windows.Controls;
using FModel.ViewModels.ApiEndpoints.Models;
namespace FModel.Views.Resources.Controls;
public partial class CommitDownloaderControl : UserControl
{
public CommitDownloaderControl()
{
InitializeComponent();
}
public static readonly DependencyProperty CommitProperty =
DependencyProperty.Register(nameof(Commit), typeof(GitHubCommit), typeof(CommitDownloaderControl), new PropertyMetadata(null));
public GitHubCommit Commit
{
get { return (GitHubCommit)GetValue(CommitProperty); }
set { SetValue(CommitProperty, value); }
}
private void OnDownload(object sender, RoutedEventArgs e)
{
Commit.Download();
}
}

View File

@ -23,7 +23,7 @@
</Grid.RowDefinitions>
<avalonEdit:TextEditor x:Name="MyAvalonEditor" Grid.Row="0" Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
FontFamily="Consolas" FontSize="8pt" IsReadOnly="True" ShowLineNumbers="True" Foreground="#DAE5F2" />
FontFamily="Consolas" FontSize="8pt" ShowLineNumbers="True" Foreground="#DAE5F2" />
<Border Grid.Row="1"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer1BackgroundBrush}}"

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using CUE4Parse.UE4.Objects.Core.Misc;
@ -13,7 +12,6 @@ namespace FModel.Views.Resources.Controls;
public partial class DictionaryEditor
{
private readonly bool _enableElements;
private readonly List<FCustomVersion> _defaultCustomVersions;
private readonly Dictionary<string, bool> _defaultOptions;
private readonly Dictionary<string, KeyValuePair<string, string>> _defaultMapStructTypes;
@ -22,9 +20,8 @@ public partial class DictionaryEditor
public Dictionary<string, bool> Options { get; private set; }
public Dictionary<string, KeyValuePair<string, string>> MapStructTypes { get; private set; }
public DictionaryEditor(string title, bool enableElements)
public DictionaryEditor(string title)
{
_enableElements = enableElements;
_defaultCustomVersions = new List<FCustomVersion> { new() { Key = new FGuid(), Version = 0 } };
_defaultOptions = new Dictionary<string, bool> { { "key1", true }, { "key2", false } };
_defaultMapStructTypes = new Dictionary<string, KeyValuePair<string, string>> { { "MapName", new KeyValuePair<string, string>("KeyType", "ValueType") } };
@ -32,11 +29,10 @@ public partial class DictionaryEditor
InitializeComponent();
Title = title;
MyAvalonEditor.IsReadOnly = !_enableElements;
MyAvalonEditor.SyntaxHighlighting = AvalonExtensions.HighlighterSelector("");
}
public DictionaryEditor(List<FCustomVersion> customVersions, string title, bool enableElements) : this(title, enableElements)
public DictionaryEditor(IList<FCustomVersion> customVersions, string title) : this(title)
{
MyAvalonEditor.Document = new TextDocument
{
@ -44,7 +40,7 @@ public partial class DictionaryEditor
};
}
public DictionaryEditor(Dictionary<string, bool> options, string title, bool enableElements) : this(title, enableElements)
public DictionaryEditor(IDictionary<string, bool> options, string title) : this(title)
{
MyAvalonEditor.Document = new TextDocument
{
@ -52,7 +48,7 @@ public partial class DictionaryEditor
};
}
public DictionaryEditor(Dictionary<string, KeyValuePair<string, string>> options, string title, bool enableElements) : this(title, enableElements)
public DictionaryEditor(IDictionary<string, KeyValuePair<string, string>> options, string title) : this(title)
{
MyAvalonEditor.Document = new TextDocument
{
@ -62,13 +58,6 @@ public partial class DictionaryEditor
private void OnClick(object sender, RoutedEventArgs e)
{
if (!_enableElements)
{
DialogResult = false;
Close();
return;
}
try
{
switch (Title)
@ -85,7 +74,7 @@ public partial class DictionaryEditor
DialogResult = true;
Close();
break;
case "MapStructTypes":
case "Versioning Configuration (MapStructTypes)":
MapStructTypes = JsonConvert.DeserializeObject<Dictionary<string, KeyValuePair<string, string>>>(MyAvalonEditor.Document.Text);
// DialogResult = !Options.SequenceEqual(_defaultOptions);
DialogResult = true;
@ -104,9 +93,6 @@ public partial class DictionaryEditor
private void OnReset(object sender, RoutedEventArgs e)
{
if (!_enableElements)
return;
MyAvalonEditor.Document = Title switch
{
"Versioning Configuration (Custom Versions)" => new TextDocument
@ -117,7 +103,7 @@ public partial class DictionaryEditor
{
Text = JsonConvert.SerializeObject(_defaultOptions, Formatting.Indented)
},
"MapStructTypes" => new TextDocument
"Versioning Configuration (MapStructTypes)" => new TextDocument
{
Text = JsonConvert.SerializeObject(_defaultMapStructTypes, Formatting.Indented)
},

View File

@ -2,12 +2,10 @@
using System.Windows;
using System.Windows.Controls;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using ICSharpCode.AvalonEdit.Document;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RestSharp;
namespace FModel.Views.Resources.Controls;
@ -16,7 +14,7 @@ public partial class EndpointEditor
private readonly EEndpointType _type;
private bool _isTested;
public EndpointEditor(FEndpoint endpoint, string title, EEndpointType type)
public EndpointEditor(EndpointSettings endpoint, string title, EEndpointType type)
{
DataContext = endpoint;
_type = type;
@ -52,13 +50,13 @@ public partial class EndpointEditor
private void OnClick(object sender, RoutedEventArgs e)
{
DialogResult = _isTested && DataContext is FEndpoint { IsValid: true };
DialogResult = _isTested && DataContext is EndpointSettings { IsValid: true };
Close();
}
private async void OnSend(object sender, RoutedEventArgs e)
{
if (DataContext is not FEndpoint endpoint) return;
if (DataContext is not EndpointSettings endpoint) return;
var body = await ApplicationService.ApiEndpointView.DynamicApi.GetRequestBody(default, endpoint.Url).ConfigureAwait(false);
Application.Current.Dispatcher.Invoke(delegate
@ -70,7 +68,7 @@ public partial class EndpointEditor
private void OnTest(object sender, RoutedEventArgs e)
{
if (DataContext is not FEndpoint endpoint) return;
if (DataContext is not EndpointSettings endpoint) return;
endpoint.TryValidate(ApplicationService.ApiEndpointView.DynamicApi, _type, out var response);
_isTested = true;
@ -82,7 +80,7 @@ public partial class EndpointEditor
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (sender is not TextBox { IsLoaded: true } ||
DataContext is not FEndpoint endpoint) return;
DataContext is not EndpointSettings endpoint) return;
endpoint.IsValid = false;
}

View File

@ -0,0 +1,273 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace FModel.Views.Resources.Controls;
/// <summary>
/// https://stackoverflow.com/a/58066259/13389331
/// </summary>
public class FilterableComboBox : ComboBox
{
/// <summary>
/// If true, on lost focus or enter key pressed, checks the text in the combobox. If the text is not present
/// in the list, it leaves it blank.
/// </summary>
public bool OnlyValuesInList {
get => (bool)GetValue(OnlyValuesInListProperty);
set => SetValue(OnlyValuesInListProperty, value);
}
public static readonly DependencyProperty OnlyValuesInListProperty =
DependencyProperty.Register(nameof(OnlyValuesInList), typeof(bool), typeof(FilterableComboBox));
/// <summary>
/// Selected item, changes only on lost focus or enter key pressed
/// </summary>
public object EffectivelySelectedItem {
get => (bool)GetValue(EffectivelySelectedItemProperty);
set => SetValue(EffectivelySelectedItemProperty, value);
}
public static readonly DependencyProperty EffectivelySelectedItemProperty =
DependencyProperty.Register(nameof(EffectivelySelectedItem), typeof(object), typeof(FilterableComboBox));
private string CurrentFilter = string.Empty;
private bool TextBoxFreezed;
protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;
private UserChange<bool> IsDropDownOpenUC;
/// <summary>
/// Triggers on lost focus or enter key pressed, if the selected item changed since the last time focus was lost or enter was pressed.
/// </summary>
public event Action<FilterableComboBox, object> SelectionEffectivelyChanged;
public FilterableComboBox()
{
IsDropDownOpenUC = new UserChange<bool>(v => IsDropDownOpen = v);
DropDownOpened += FilteredComboBox_DropDownOpened;
Focusable = true;
IsEditable = true;
IsTextSearchEnabled = true;
StaysOpenOnEdit = true;
IsReadOnly = false;
Loaded += (s, e) => {
if (EditableTextBox != null)
new TextBoxBaseUserChangeTracker(EditableTextBox).UserTextChanged += FilteredComboBox_UserTextChange;
};
SelectionChanged += (_, __) => shouldTriggerSelectedItemChanged = true;
SelectionEffectivelyChanged += (_, o) => EffectivelySelectedItem = o;
}
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
base.OnPreviewKeyDown(e);
if (e.Key == Key.Down && !IsDropDownOpen) {
IsDropDownOpen = true;
e.Handled = true;
}
else if (e.Key == Key.Escape) {
ClearFilter();
Text = "";
IsDropDownOpen = true;
}
else if (e.Key == Key.Enter || e.Key == Key.Tab) {
CheckSelectedItem();
TriggerSelectedItemChanged();
}
}
protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnPreviewLostKeyboardFocus(e);
CheckSelectedItem();
if ((e.OldFocus == this || e.OldFocus == EditableTextBox) && e.NewFocus != this && e.NewFocus != EditableTextBox)
TriggerSelectedItemChanged();
}
private void CheckSelectedItem()
{
if (OnlyValuesInList)
Text = SelectedItem?.ToString() ?? "";
}
private bool shouldTriggerSelectedItemChanged = false;
private void TriggerSelectedItemChanged()
{
if (shouldTriggerSelectedItemChanged) {
SelectionEffectivelyChanged?.Invoke(this, SelectedItem);
shouldTriggerSelectedItemChanged = false;
}
}
public void ClearFilter()
{
if (string.IsNullOrEmpty(CurrentFilter)) return;
CurrentFilter = "";
CollectionViewSource.GetDefaultView(ItemsSource).Refresh();
}
private void FilteredComboBox_DropDownOpened(object sender, EventArgs e)
{
if (IsDropDownOpenUC.IsUserChange)
ClearFilter();
}
private void FilteredComboBox_UserTextChange(object sender, EventArgs e)
{
if (TextBoxFreezed) return;
var tb = EditableTextBox;
if (tb.SelectionStart + tb.SelectionLength == tb.Text.Length)
CurrentFilter = tb.Text.Substring(0, tb.SelectionStart).ToLower();
else
CurrentFilter = tb.Text.ToLower();
RefreshFilter();
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null) {
var view = CollectionViewSource.GetDefaultView(newValue);
view.Filter += FilterItem;
}
if (oldValue != null) {
var view = CollectionViewSource.GetDefaultView(oldValue);
if (view != null) view.Filter -= FilterItem;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
private void RefreshFilter()
{
if (ItemsSource == null) return;
var view = CollectionViewSource.GetDefaultView(ItemsSource);
FreezTextBoxState(() => {
var isDropDownOpen = IsDropDownOpen;
//always hide because showing it enables the user to pick with up and down keys, otherwise it's not working because of the glitch in view.Refresh()
IsDropDownOpenUC.Set(false);
view.Refresh();
if (!string.IsNullOrEmpty(CurrentFilter) || isDropDownOpen)
IsDropDownOpenUC.Set(true);
if (SelectedItem == null) {
foreach (var itm in ItemsSource)
if (itm.ToString() == Text) {
SelectedItem = itm;
break;
}
}
});
}
private void FreezTextBoxState(Action action)
{
TextBoxFreezed = true;
var tb = EditableTextBox;
var text = Text;
var selStart = tb.SelectionStart;
var selLen = tb.SelectionLength;
action();
Text = text;
tb.SelectionStart = selStart;
tb.SelectionLength = selLen;
TextBoxFreezed = false;
}
private bool FilterItem(object value)
{
if (value == null) return false;
if (CurrentFilter.Length == 0) return true;
return value.ToString().ToLower().Contains(CurrentFilter);
}
private class TextBoxBaseUserChangeTracker
{
private bool IsTextInput { get; set; }
public TextBox TextBoxBase { get; set; }
private List<Key> PressedKeys = new List<Key>();
public event EventHandler UserTextChanged;
private string LastText;
public TextBoxBaseUserChangeTracker(TextBox textBoxBase)
{
TextBoxBase = textBoxBase;
LastText = TextBoxBase.ToString();
textBoxBase.PreviewTextInput += (s, e) => {
IsTextInput = true;
};
textBoxBase.TextChanged += (s, e) => {
var isUserChange = PressedKeys.Count > 0 || IsTextInput || LastText == TextBoxBase.ToString();
IsTextInput = false;
LastText = TextBoxBase.ToString();
if (isUserChange)
UserTextChanged?.Invoke(this, e);
};
textBoxBase.PreviewKeyDown += (s, e) => {
switch (e.Key) {
case Key.Back:
case Key.Space:
if (!PressedKeys.Contains(e.Key))
PressedKeys.Add(e.Key);
break;
}
if (e.Key == Key.Back) {
var textBox = textBoxBase as TextBox;
if (textBox.SelectionStart > 0 && textBox.SelectionLength > 0 && (textBox.SelectionStart + textBox.SelectionLength) == textBox.Text.Length) {
textBox.SelectionStart--;
textBox.SelectionLength++;
e.Handled = true;
UserTextChanged?.Invoke(this, e);
}
}
};
textBoxBase.PreviewKeyUp += (s, e) => {
if (PressedKeys.Contains(e.Key))
PressedKeys.Remove(e.Key);
};
textBoxBase.LostFocus += (s, e) => {
PressedKeys.Clear();
IsTextInput = false;
};
}
}
private class UserChange<T>
{
private Action<T> action;
public bool IsUserChange { get; private set; } = true;
public UserChange(Action<T> action)
{
this.action = action;
}
public void Set(T val)
{
try {
IsUserChange = false;
action(val);
}
finally {
IsUserChange = true;
}
}
}
}

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