Compare commits

..

212 Commits

Author SHA1 Message Date
Masusder
8b95b403bb
New Explorer System (#619)
Some checks failed
FModel QA Builder / build (push) Has been cancelled
+ filter by types, find by references (UE5+), and a lot of other improvements

Co-authored-by: Asval <asval.contactme@gmail.com>
Co-authored-by: LongerWarrior <LongerWarrior@gmail.com>
2025-12-19 18:34:33 +01:00
Marlon
5776444020
cue4p update
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-12-12 20:49:46 +01:00
Marlon
5b4494fda2
lib download error logging 2025-12-12 20:48:36 +01:00
Marlon
3098273a33
ci/cd build flag 2025-12-12 20:48:05 +01:00
Marlon
c9a97c73a1
csproj cleanup 2025-12-12 20:47:37 +01:00
Marlon
a68b469556
oodle dl fix 2025-12-11 14:47:43 +01:00
Marlon
4e0efe779b
action updates 2025-12-10 21:59:59 +01:00
Marlon
5d34bfaf4e
oodle init fix 2025-12-10 21:51:20 +01:00
Marlon
f082d39bf9
nuget updates 2025-12-10 21:51:04 +01:00
Marlon
1a2766a7e6
added slnx 2025-12-10 21:50:57 +01:00
LongerWarrior
6b4503abc3 Grounded 2 update
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-12-07 21:53:51 +02:00
LongerWarrior
819fbc42ec The First Descendant textures fix
Some checks failed
FModel QA Builder / build (push) Has been cancelled
Blood Bowl 3 support
2025-12-05 00:40:52 +02:00
LongerWarrior
012f780c08 Wuthering Waves 2.8 and Dead by Daylight 9.3.0 fixes
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-11-27 19:44:06 +02:00
simon
139c61d9db
Add Decompile Blueprint to Search View (#615) 2025-11-27 17:10:26 +02:00
GhostScissors
f51ad82e1f Show CompressionMethods in Archives Info
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-11-25 01:33:21 +05:30
LongerWarrior
1846b82802 Aion2 dat files and HTML5 Emscripten data files support
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2025-11-24 00:40:29 +02:00
LongerWarrior
5d3ab103ee Aion 2 and Octopath Traveler 0 support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
Delta Force relative location fix
2025-11-19 21:46:08 +02:00
LongerWarrior
d58acea554
CRIWARE Support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-11-12 13:01:00 +02:00
LongerWarrior
5b13bb0b46 Skip emissive 2025-11-12 12:57:14 +02:00
Masusder
3b82f14290 Added ADX decoder 2025-11-11 13:24:34 +01:00
Masusder
c525243c50 AWB json converter, fix paths 2025-11-07 22:47:21 +01:00
Masusder
2888c9261c Criware decrypt key setting / HCA decoder 2025-11-07 16:42:41 +01:00
Masusder
879b5a2ce8 CriWare 2025-11-04 21:26:19 +01:00
LongerWarrior
604e59be33 FN 38.00 fixes and Infinity Nikki update
Some checks failed
FModel QA Builder / build (push) Has been cancelled
fixed missing usings
2025-11-04 00:41:08 +02:00
LongerWarrior
1f10656d91 FN 38.00 fixes and Infinity Nikki update 2025-11-04 00:10:26 +02:00
Valentin
69d83d5257
Merge pull request #602 from GMMan/add-audio-export
Some checks are pending
FModel QA Builder / build (push) Waiting to run
Add save audio option to context menus
2025-11-02 20:23:18 +01:00
Marlon
6baf1fcfee
Merge pull request #606 from JustAndr3h/pullreq
Some checks are pending
FModel QA Builder / build (push) Waiting to run
add sidekick and sidekick reactions
2025-11-02 19:40:38 +01:00
Marlon
b8147b3c0c
valorant live fix (wip) 2025-11-02 19:39:26 +01:00
Marlon
e05baeb580
nuget updates 2025-11-02 19:31:45 +01:00
andre
e14cae0637 Update CreatorPackage.cs 2025-11-02 17:41:28 +01:00
LongerWarrior
6fa336c3ee Outer Worlds 2, Raven 2, Duet Night Abyss support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-10-30 20:46:11 +02:00
LongerWarrior
ae52058d26 Psychonauts2 support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-10-23 00:53:26 +03:00
Asval
de34f9c5ce removed auto open sounds 2025-10-21 17:09:49 +02:00
LongerWarrior
35e05d947f Grey Zone Warfare, Reanimal, VEIN, SpongeBob SquarePants: Titans of the Tide support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
FMod Audio Fixes
2025-10-19 01:13:55 +03:00
Yukai Li
570bdd6180 Add save audio option to context menus 2025-10-17 16:38:38 -06:00
Masusder
b51d539c24
FMOD Audio support (#597)
Some checks failed
FModel QA Builder / build (push) Has been cancelled
Co-authored-by: LongerWarrior <LongerWarrior@gmail.com>
2025-10-14 19:45:27 +03:00
GMatrixGames
f366ad1189
Use ZStandard, closer to oodle. 2025-10-11 16:41:19 -04:00
Asval
a4e92b6c59 migrated api endpoints over to UEDB 2025-10-10 14:03:38 +02:00
LongerWarrior
407be3d1a7 ABI, PUBG and FragPunk fixes
Some checks failed
FModel QA Builder / build (push) Has been cancelled
Correct materials preview in Silent Hill f
2025-09-28 21:58:56 +03:00
LongerWarrior
001ddc1057 ABI, PUBG and FragPunk fixes
Correct materials preview in Silent Hill f
2025-09-28 21:46:57 +03:00
LongerWarrior
8eeae5d59b Added Borderlands4 and Wwise bulk packaging support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
Fixed inlined shader maps and paks assets on UE 5.6+
2025-09-25 01:36:47 +03:00
LongerWarrior
378c911083 World of jade Dynasty, Squad, Conan Exiles support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
Dune Awakening, WuWa, ABI, Days Gone, Blue Protocol fixes
2025-09-10 22:53:35 +03:00
LongerWarrior
d3f93021c6 Arena Breakout Infinite, Lost Soul Aside and Ghost of Tabor support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-09-01 01:21:19 +03:00
LongerWarrior
17d3586032 Concord and uncharted Waters Origin support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-08-28 14:56:00 +03:00
LongerWarrior
2c0c5d8694 MGSDelta, Hell Let Loose support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
Splitgate2 Aes fix
2025-08-27 01:51:41 +03:00
Asval
528603e147 Snowbreak fix
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-08-21 19:37:55 +02:00
Valentin
0cc8da95e1
Throne and Liberty, Farlight84 and Mafia fixes
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-08-17 22:16:10 +02:00
Asval
a0586e8acc Merge branch 'dev' into blueprint-decompile
# Conflicts:
#	CUE4Parse
2025-08-17 22:13:31 +02:00
Asval
af6ff0bf9f better 2025-08-17 22:02:11 +02:00
Asval
344b40361f pulled c4p 2025-08-17 21:55:20 +02:00
Krowe Moh
1449f78665 forgot to push 2025-08-11 07:46:52 +10:00
LongerWarrior
a1ddc72b79 Titan Quset 2 support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
Hogwarts Legacy and Ace Combat 7 fixes
2025-08-05 11:59:52 +03:00
Krowe Moh
623656c702 bump 2025-08-05 15:02:49 +10:00
Krowe Moh
52979f2a67 Pull changes 2025-08-05 09:18:56 +10:00
Asval
b71c25606a C4P pull + other random stuff 2025-08-04 21:27:07 +02:00
Krowe Moh
af4435e444 oops 2025-08-04 15:34:01 +10:00
Krowe Moh
fe7106df3a fixed 2025-08-04 15:32:20 +10:00
Krowe Moh
934b4995d5 m 2025-08-02 09:25:57 +10:00
Krowe Moh
ed129819de Demangle 2025-08-01 11:42:43 +10:00
Krowe Moh
bca41b0a5c these settings do not require restart
tested all the settings and it worked fine
2025-07-31 13:01:46 +10:00
LongerWarrior
0760e11058 Valorant and Grounded2 update
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-07-31 00:34:00 +03:00
Krowe Moh
eabda151d4 Merge branch 'dev' of https://github.com/Krowe-moh/FModel into dev 2025-07-30 13:31:01 +10:00
Krowe Moh
6ff3c987d1
Merge branch '4sval:dev' into dev 2025-07-30 12:55:31 +10:00
Krowe Moh
b51983a36a Finished? 2025-07-30 12:55:13 +10:00
Krowe Moh
a49dddaec3 fixed issues 2025-07-27 14:16:54 +10:00
LongerWarrior
4300eb58a3 FateTrigger and BlackStigma support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-07-26 23:11:32 +03:00
Krowe Moh
1e3e4df206
Merge branch 'dev' into dev 2025-07-24 14:39:12 +10:00
Krowe Moh
51541e5ef9 Use Refactored blueprint decompilation (unfinished code)
ignore the json var and GetLoadPackageResult, will be removed once the method is found

Removed the KismetExtensions.cs file and refactored blueprint decompilation logic in CUE4ParseViewModel.cs to use a new DecompileBlueprintToPseudo method. Updated CUE4ParseExtensions.cs with GetLoadPackageResult helper. Improved C++ syntax highlighting in Cpp.xshd by adding 'default' keyword and enhanced comment rule. Minor formatting and code organization improvements across affected files.
2025-07-24 14:36:38 +10:00
Valentin
4e76469169
Merge pull request #582 from Ka1serM/dev
Some checks failed
FModel QA Builder / build (push) Has been cancelled
make HDR export optional
2025-07-21 22:34:26 +02:00
Asval
75bdfc1797 automated GetInternalSID using Athena_SeasonTitles 2025-07-21 18:11:12 +02:00
Marcel K.
66bdddd686 make HDR export optional 2025-07-17 16:15:36 +02:00
LongerWarrior
4bc93c05e3 AshEchoes, Ashen, Dauntless support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-07-16 17:34:46 +03:00
LongerWarrior
1f57279206 AshEchoes, Ashen, Dauntless support 2025-07-16 17:25:27 +03:00
Valentin
4a9a44139d
Merge pull request #564 from Masusder/dev
Some checks are pending
FModel QA Builder / build (push) Waiting to run
Support for audio events from Wwise soundbanks
2025-07-15 23:01:16 +02:00
Asval
e6ef05092e added back transforms manual inputs 2025-07-15 22:58:54 +02:00
Asval
364df7f402 fixed opengl being triggered when not needed 2025-07-15 22:30:46 +02:00
Asval
cfbee11e58 added user setting 2025-07-10 19:24:51 +02:00
Asval
7598ae0a74 Merge branch 'dev' into wwise
# Conflicts:
#	CUE4Parse
#	FModel/ViewModels/CUE4ParseViewModel.cs
2025-07-10 18:29:55 +02:00
LongerWarrior
1f1d2ae3c2 Tony Hawk's™ Pro Skater™ 3 + 4 support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-07-08 16:59:09 +03:00
Krowe Moh
046677b875 Improvements 2025-07-08 20:42:58 +10:00
Krowe Moh
0f3d80bafb extensions 2025-07-08 11:37:31 +10:00
Chompster86
62b103f958
Fixed Burst Weapon's Fire Rates (#575)
Some checks failed
FModel QA Builder / build (push) Has been cancelled
* Update BaseIconStats.cs

Fixed Burst Fire Rates

* Hides fire rate if it = 0

Made it so if the equation = 0, it hides fire rate

* Fixed if firing rate = 1

Made it so if firing rate = 1, it hides the burst firing rate equation and just shows the firing rate.

* Neverness to Everness and Terminull Brigade support
Add new loading mode that skips patched assets

* BinaryConfig.ini support

* Update CUE4Parse

---------

Co-authored-by: GhostScissors <79089473+GhostScissors@users.noreply.github.com>
2025-07-07 08:40:14 +05:30
Krowe Moh
a8a5207dec Improvements 2025-07-07 07:52:34 +10:00
LongerWarrior
233b7ab03b Need For Speed Mobile and Game For Peace fixes
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2025-07-07 00:17:56 +03:00
Asval
89c8cbaccc fixed goto 2025-07-06 22:48:55 +02:00
Asval
df3fdbbd2b refactored ChildProperties 2025-07-06 19:17:57 +02:00
Asval
00bcb7ca16 refactored parts of this hell 2025-07-06 18:52:16 +02:00
Krowe Moh
e78c7a7be9 Cpp xshd by Masus
added projectstore as it said unknown type
2025-07-06 12:21:10 +10:00
Krowe Moh
84030f5e20 Merge branch 'dev' of https://github.com/Krowe-moh/FModel into dev 2025-07-05 13:13:52 +10:00
Krowe Moh
b22c996a58 fixed duplicated "po" extension
fixed again because github pulled latest files for some reason
2025-07-05 13:12:30 +10:00
Krowe Moh
656d3b59ca
mb 2025-07-05 13:09:23 +10:00
Krowe Moh
b0625bbd6f gitignore and md files are now able to be read 2025-07-05 09:06:21 +10:00
Krowe Moh
f64a333847 Blueprint "Decompile" option
turns blueprint logic into readable c++ code, option shows only when setting is on
2025-07-05 09:01:29 +10:00
LongerWarrior
394bcf356f Neverness to Everness and Terminull Brigade support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
Add new loading mode that skips patched assets
2025-07-03 17:27:32 +03:00
LongerWarrior
03a4f79c3a DNAExporter Fix
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-06-30 23:14:29 +03:00
LongerWarrior
ba0aedba68 Extract DNA and fixes for Crystal of Atlan and Naruto to Boruto
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2025-06-30 13:11:25 +03:00
LongerWarrior
2dd98d4d64 full Nanite support for Static Meshes
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-06-26 23:56:59 +03:00
Masusder
ff58050fe9 Bump 2025-06-22 16:08:17 +02:00
LongerWarrior
aa75a8bbf2 Nanite Static Meshes support in UE 5.0-5.3
Some checks failed
FModel QA Builder / build (push) Has been cancelled
MindsEye and Wuthering Waves 2.4 support
2025-06-15 16:18:28 +03:00
LongerWarrior
6f19cacaeb EtheriaRestart and GameForPeace fix 2025-06-08 22:45:43 +03:00
Masusder
61da5a9ae0 Lazy loaded provider 2025-06-03 18:47:11 +02:00
LongerWarrior
9f4597f542 bump 2025-06-01 23:06:34 +03:00
Marlon
8ba756ade5
fix FN live 2025-05-29 13:03:03 +02:00
Masusder
06416cb7f8 Bump CUE4Parse 2025-05-25 21:16:44 +02:00
LongerWarrior
72930bcd14 Splitgate2 and Spectre Divide support 2025-05-25 19:30:55 +03:00
Valentin
c63721b644
Merge pull request #561 from AllyJaxx/fortnite-creator-update
Updates to "Creator" features
2025-05-21 14:50:56 +02:00
Asval
99bcfb2c29 random stuff 2025-05-21 14:49:15 +02:00
Masusder
b75413306f Moved logic to CUE4Parse 2025-05-21 11:55:33 +02:00
Masusder
47d003737b Merge remote-tracking branch 'upstream/dev' into dev 2025-05-18 15:48:11 +02:00
Masusder
746107875c Last bump 2025-05-18 15:36:35 +02:00
Masusder
c10ff9dfc2 Debug helper 2025-05-18 13:34:29 +02:00
Masusder
cf1f19f615 Semi-support for cross-soundbanks audio events
Unfortunately in case some game splits audio events across multiple soundbanks and given game has thousands of them, custom implementation for that game would be required
2025-05-14 22:23:45 +02:00
LongerWarrior
c0a03ac600 Dune Awakening
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-05-14 01:21:04 +03:00
GhostScissors
7d4b9e7094 Merge branch 'fortnite-creator-update' of https://github.com/AllyJaxx/FModel into dev 2025-05-13 23:39:11 +05:30
AllyJaxx
3b24be37d2 support for Cataba 2025-05-13 09:55:51 -04:00
AllyJaxx
7785a33b5e missed an edge case #fuck 2025-05-11 15:46:49 -04:00
AllyJaxx
7d425f6176 holy stats 2025-05-11 15:15:13 -04:00
AllyJaxx
c8de28130c switch 2025-05-11 13:51:11 -04:00
LongerWarrior
134e32757f Infinity Nikki and Wildgate support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-05-11 19:58:06 +03:00
AllyJaxx
4e43a07f8e actual max stat values from Athena.Inventory.Stats.Max(stat) 2025-05-11 09:46:23 -04:00
AllyJaxx
a4ffb29841 note edge case season names correctly (X, OG, Remix, MS1) 2025-05-11 09:41:30 -04:00
AllyJaxx
4d3d43c704 slight rewrite and fixes item stats 2025-05-11 09:40:18 -04:00
AllyJaxx
22168b37c9 AthenaHatItemDefinition (tested on a custom asset) 2025-05-11 09:39:50 -04:00
Masusder
60292cb24f Music switch/rnd 2025-05-09 11:46:26 +02:00
LongerWarrior
cc2ff71759 Fixing 16bit bone weights and FMaterialInput converter
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-05-08 00:55:28 +03:00
Asval
037058da52 fixed fortnite fonts + updated quest icons
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-05-05 16:29:03 +02:00
LongerWarrior
a6c508c743 MotoGP25 and Days Gone support
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2025-05-04 22:24:03 +03:00
Asval
4a1a758896 bump c4p
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-05-04 01:08:55 +02:00
Masusder
31382e3dbe In case project has "Game" in its name 2025-05-02 21:15:04 +02:00
Masusder
c5e78e7ba7 Music Track/Segment 2025-05-02 01:01:09 +02:00
Masusder
adc19388c9 Generic search for wwise directory (v2)
and cute icon
2025-05-01 19:06:52 +02:00
Masusder
9318838ea7 Generic search for wwise directory 2025-04-28 17:28:04 +02:00
Masusder
48d172446e Update CUE4Parse 2025-04-28 13:22:46 +02:00
Masusder
7581310ef3 Too long audio path fix 2025-04-28 13:18:48 +02:00
Masusder
0d9a2a34e9 Bnk audio events support 2025-04-28 11:55:22 +02:00
Masusder
2bd0f5163e
Merge branch '4sval:dev' into dev 2025-04-28 11:12:02 +02:00
Marcel K.
556ae6e036
Texture Changes & HDR Preview/Export (#559)
Some checks failed
FModel QA Builder / build (push) Has been cancelled
* texture refactor
* fix bgra in opengl

Co-authored-by: Asval <asval.contactme@gmail.com>
2025-04-26 23:01:01 +02:00
Masusder
e943ea314a Custom directories for dbd
AES key wasn't changed since they moved to UE5, so I see no reason not to include it
2025-04-20 14:34:59 +02:00
Masusder
39f8ea5702 Generalized wwiseaudio dir lookup 2025-04-16 18:39:57 +02:00
Asval
edebff0925 Wwise audio events now trigger the audio player
Some checks failed
FModel QA Builder / build (push) Has been cancelled
and rename .wem files if the event references any ofc (no .bnk support yet)
2025-04-11 01:06:20 +02:00
Asval
273b124840 fixed assertion errors on modals open
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-04-02 17:46:24 +02:00
Asval
cecd3b7fe6 small fix
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-03-28 19:23:51 +01:00
Asval
e8b061f018 added a guizmo for model transformation 2025-03-28 19:19:50 +01:00
LongerWarrior
0e4d0431a3 Promise Mascot Agency support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-03-18 12:12:12 +02:00
LongerWarrior
819c046f3f Crystal of Atlan support 2025-03-17 12:13:08 +02:00
Asval
fe20eb1483 added CookedIndex support for non iostore archives
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-03-16 17:11:30 +01:00
Asval
239f130e66 fixed shader compilation error for some gpus
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2025-03-16 12:44:27 +01:00
Asval
e3764ef2d0 bump c4p + spline mesh preview in umaps
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2025-03-16 02:01:19 +01:00
LongerWarrior
20a5c63baa Split Fiction and Brickadia support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-03-09 00:15:45 +02:00
Asval
814f34d534 bump c4p
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2025-03-08 00:13:48 +01:00
Marlon
2f7d20fc73
Merge pull request #532 from Chompster86/patch-2
Some checks failed
FModel QA Builder / build (push) Has been cancelled
More missing item defs
2025-03-06 23:34:15 +01:00
Asval
1707f41197 preview anims without pre-loading a mesh
Some checks failed
FModel QA Builder / build (push) Has been cancelled
the anim's default skeleton will act as the loaded mesh if none was provided
added links to mesh importers in the settings
2025-02-27 17:38:23 +01:00
LongerWarrior
e7148d870c FF7Rebirth Static Meshes
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-02-26 01:53:55 +02:00
Asval
91edf0352e per extension audio decoder
Some checks failed
FModel QA Builder / build (push) Has been cancelled
as long as c4p knows the format
and the decoder is named *dec.exe with -i -o args in .data/
2025-02-24 21:37:19 +01:00
Asval
ae0e638854 bump c4p
Some checks failed
FModel QA Builder / build (push) Has been cancelled
support for MK1 morph targets + various wwise audio formats including the good old AkAudioEvent
2025-02-18 20:28:16 +01:00
Valentin
34116d62b2
Merge pull request #550 from 4sval/on-hold
Some checks failed
FModel QA Builder / build (push) Has been cancelled
On hold
2025-02-16 00:23:44 +01:00
Asval
4c790af232 fixed use of obsolete methods 2025-02-16 00:20:28 +01:00
Asval
7bfadc4e20 Merge remote-tracking branch 'origin/dev' into on-hold 2025-02-15 23:59:18 +01:00
Asval
c92103857e fixed backups being case sensitive 2025-02-15 23:47:33 +01:00
LongerWarrior
75e4028bc7 FF7R SKM and Avowed support
Some checks are pending
FModel QA Builder / build (push) Waiting to run
2025-02-15 01:39:47 +02:00
Asval
b5c1fdccc6 SoftObjectPath notify animation support
Marvel Rivals emotes will display notified models
2025-02-14 19:23:10 +01:00
Valentin
a97d773c64
Merge pull request #549 from 4sval/pagination
Some checks are pending
FModel QA Builder / build (push) Waiting to run
Pagination
2025-02-14 17:42:48 +01:00
Asval
e1b9529d6f pagination qa 2025-02-14 17:31:29 +01:00
Asval
37025228a2 bump c4p 2025-02-13 18:50:51 +01:00
Asval
6dfbd11ad4 focus export check on UWorld for maps 2025-02-11 19:47:39 +01:00
Asval
71468dca23 poc pagination for large packages 2025-02-09 00:41:47 +01:00
Asval
93fa0b2bb6 added tab extra title 2025-02-09 00:40:11 +01:00
Asval
dbf618e417 ui now deals with GameFile instead of a bad replica
Some checks failed
FModel QA Builder / build (push) Has been cancelled
that means loose files are now supported, or should be
Export Raw Data shows the correct extension
2025-02-06 18:22:35 +01:00
Asval
3d0bdf05e1 full patched archive support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-02-04 23:12:12 +01:00
LongerWarrior
a704612544 SerilogEnricher
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-01-30 22:40:11 +02:00
Asval
32334a079f fixed delta force, strinova, stalker2, gta, guilty gear + read shader maps option
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-01-27 09:57:39 +01:00
Valentin
a3cbf3555f
Merge pull request #544 from Zi5han/vertical-camera-flipping-patch
Some checks are pending
FModel QA Builder / build (push) Waiting to run
Fix for vertical camera flipping in 3D viewer
2025-01-26 03:54:10 +01:00
Zi5han
298f459dc7 Fixed vertical camera flipping in 3D viewer 2025-01-25 23:05:18 +01:00
Asval
ff2eac3d8e properly handling patched archives
Some checks failed
FModel QA Builder / build (push) Has been cancelled
fixed MR SKMs (not backward compatible)
2025-01-23 19:19:22 +01:00
Asval
73fffc5545 cost less export check 2025-01-23 19:17:16 +01:00
Chompster86
f8c7996a61
Update CreatorPackage.cs
Added another item def
2025-01-16 22:10:20 -05:00
LongerWarrior
7431ef5592 update for Wuthering Waves and Fernbus
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2025-01-13 01:41:08 +02:00
Marlon
e313bb27e9
cue4p sync + nuget updates
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-12-29 16:36:21 +01:00
Chompster86
988b4a5847
More missing item defs
Added 3 more missing item defs
2024-12-17 02:05:47 -05:00
Valentin
09da05218c
Merge pull request #531 from djlorenzouasset/dev
Some checks failed
FModel QA Builder / build (push) Has been cancelled
authorization token change + check if a release is downloadable
2024-12-14 18:54:00 +01:00
Valentin
9a7d7effa5
Update UpdateViewModel.cs 2024-12-14 18:51:51 +01:00
ᴅᴊʟᴏʀ3xᴢᴏ
db988f68c9 changes 2024-12-14 18:46:53 +01:00
ᴅᴊʟᴏʀ3xᴢᴏ
4385255426 commits limit increased 2024-12-14 18:46:35 +01:00
ᴅᴊʟᴏʀ3xᴢᴏ
d48aea4744 check if latest is downloadable 2024-12-14 18:46:15 +01:00
ᴅᴊʟᴏʀ3xᴢᴏ
7dfeb1730d IOS token has been deprecated, use PC token instead 2024-12-14 18:45:22 +01:00
LongerWarrior
0882062a96 UTexture2DArray support
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-12-08 23:01:33 +02:00
GMatrixGames
c07dc02714
forgot to remove this.
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-12-06 22:27:27 -05:00
GMatrixGames
6807e3ada8
parse 2024-12-06 22:23:35 -05:00
GMatrixGames
4ff41502fb
Update for MR S0 2024-12-06 20:39:29 -05:00
Marlon
0f6c806d39
fix manifest 403's 2024-12-06 20:44:00 +01:00
Marlon
8bc9a5535e
refactor
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-12-04 02:21:24 +01:00
Marlon
7402284c80
nuget updates 2024-12-04 02:21:13 +01:00
Valentin
fa0f6447dc
Merge pull request #526 from GhostScissors/dev
Some checks failed
FModel QA Builder / build (push) Has been cancelled
Make downloading of chunks parallel + fix for icon generation and static meshes
2024-12-02 18:15:08 +01:00
GhostScissors
f341bb53ad Pull CUE4Parse 2024-12-02 22:40:35 +05:30
GhostScissors
b6bba28b6d Made CheckGameplayTags in BaseIcon virtual and protected 2024-12-02 20:44:35 +05:30
GhostScissors
8a41247acb Make downloading of chunks parallel + fixes for icons 2024-12-02 12:41:07 +05:30
Asval
062d54e366 NTE support + better directory permission detection
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-11-29 11:47:13 +01:00
Marlon
3edcd31450
credits to @Krowe-moh :)
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-11-23 21:51:02 +01:00
Marlon
21e727e7b9
Merge pull request #521 from Krowe-moh/junoandweapons 2024-11-23 21:42:57 +01:00
Marlon
6bb325c10c
Merge pull request #518 from Krowe-moh/junodescriptions 2024-11-23 21:41:08 +01:00
Marlon
20adb800f3
Merge pull request #519 from Krowe-moh/optimization 2024-11-23 21:39:37 +01:00
Krowe Moh
f1a33b68d0
ig 2024-11-24 06:54:47 +11:00
Marlon
6d3f6f79e7
EpicManifestParser changes 2024-11-23 19:22:11 +01:00
Marlon
4ddf887ba3
RandomAccessStream changes 2024-11-23 19:22:01 +01:00
Marlon
6285804cc9
nuget updates 2024-11-23 19:21:53 +01:00
Marlon
fcb30ddaea
cue4parse sync 2024-11-23 19:21:29 +01:00
Krowe Moh
0395562353
Incorrect locations and if statement 2024-11-16 08:08:54 +11:00
Asval
b846878e54 shoes icon + fixed gameplay tags + show metadata
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-11-14 17:34:59 +01:00
Krowe Moh
55b15ab2b5
better handling 2024-11-14 08:39:44 +11:00
Krowe Moh
7c15ddffa1
fixed juno descriptions 2024-11-14 05:25:40 +11:00
Asval
ac95ca7ebe updated fn fonts path
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-10-30 17:11:47 +01:00
LongerWarrior
090e7445f6 KartRiderDrift, RacingMaster, WildAssault, Gotham Knights
Some checks failed
FModel QA Builder / build (push) Has been cancelled
2024-10-27 19:44:59 +02:00
148 changed files with 7477 additions and 2800 deletions

View File

@ -14,7 +14,7 @@ jobs:
steps:
- name: GIT Checkout
uses: actions/checkout@v2
uses: actions/checkout@v6
with:
submodules: 'true'
@ -22,7 +22,7 @@ jobs:
run: git submodule update --init --recursive
- name: .NET 8 Setup
uses: actions/setup-dotnet@v2
uses: actions/setup-dotnet@v5
with:
dotnet-version: '8.0.x'
@ -33,7 +33,7 @@ jobs:
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
uses: papeloto/action-zip@v1.2
with:
files: ./FModel/bin/Publish/FModel.exe
dest: FModel.zip # will end up in working directory not the Publish folder

View File

@ -10,17 +10,17 @@ jobs:
steps:
- name: GIT Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: 'recursive'
- name: .NET 8 Setup
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: '8.0.x'
- name: .NET Restore
run: dotnet restore FModel
run: dotnet restore "./FModel/FModel.slnx"
- 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
@ -33,7 +33,7 @@ jobs:
path: ./FModel/bin/Publish/FModel.exe
- name: Edit QA Artifact
uses: ncipollo/release-action@v1.14.0
uses: ncipollo/release-action@v1.20.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: 'FModel QA Testing'
@ -51,13 +51,13 @@ jobs:
- name: FModel Auth
id: fmodel_auth
uses: fjogeleit/http-request-action@v1.15.5
uses: fjogeleit/http-request-action@v1.16.6
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
uses: fjogeleit/http-request-action@v1.16.6
with:
url: "https://api.fmodel.app/v1/infos/${{ secrets.QA_ID }}"
method: "PATCH"

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

View File

@ -1,4 +1,4 @@
<Application x:Class="FModel.App"
<Application x:Class="FModel.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
@ -9,6 +9,14 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/AdonisUI;component/ColorSchemes/Dark.xaml"/>
<ResourceDictionary Source="pack://application:,,,/AdonisUI.ClassicTheme;component/Resources.xaml"/>
<ResourceDictionary Source="Views/Resources/Colors.xaml" />
<ResourceDictionary Source="Views/Resources/Icons.xaml" />
<ResourceDictionary Source="Views/Resources/Controls/ContextMenus/FileContextMenu.xaml" />
<ResourceDictionary Source="Views/Resources/Controls/ContextMenus/FolderContextMenu.xaml" />
<ResourceDictionary Source="Views/Resources/Resources.xaml" />
<ResourceDictionary Source="Views/Resources/Controls/TiledExplorer/Resources.xaml" />
</ResourceDictionary.MergedDictionaries>
<Color x:Key="{x:Static adonisUi:Colors.AccentColor}">#206BD4</Color>

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Threading;
using CUE4Parse;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
@ -51,13 +52,20 @@ public partial class App
if (!Directory.Exists(UserSettings.Default.OutputDirectory))
{
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.");
try
{
var outputDir = Directory.CreateDirectory(Path.Combine(currentDir, "Output"));
using (File.Create(Path.Combine(outputDir.FullName, Path.GetRandomFileName()), 1, FileOptions.DeleteOnClose))
{
UserSettings.Default.OutputDirectory = Path.Combine(currentDir, "Output");
}
UserSettings.Default.OutputDirectory = outputDir.FullName;
}
catch (UnauthorizedAccessException exception)
{
throw new Exception("FModel cannot create the output directory where it is currently located. Please move FModel.exe to a different location.", exception);
}
}
if (!Directory.Exists(UserSettings.Default.RawDataDirectory))
@ -96,15 +104,20 @@ public partial class App
Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, "Logs"));
Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data"));
const string template = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Enriched}: {Message:lj}{NewLine}{Exception}";
Log.Logger = new LoggerConfiguration()
#if DEBUG
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();
.Enrich.With<SourceEnricher>()
.MinimumLevel.Verbose()
.WriteTo.Console(outputTemplate: template, theme: AnsiConsoleTheme.Literate)
.WriteTo.File(outputTemplate: template,
path: Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Debug-Log-{DateTime.Now:yyyy-MM-dd}.log"))
#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();
.Enrich.With<CallerEnricher>()
.WriteTo.File(outputTemplate: template,
path: Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Log-{DateTime.Now:yyyy-MM-dd}.log"))
#endif
.CreateLogger();
Log.Information("Version {Version} ({CommitId})", Constants.APP_VERSION, Constants.APP_COMMIT_ID);
Log.Information("{OS}", GetOperatingSystemProductName());
@ -126,15 +139,15 @@ public partial class App
var messageBox = new MessageBoxModel
{
Text = $"An unhandled exception occurred: {e.Exception.Message}",
Text = $"An unhandled {e.Exception.GetBaseException().GetType()} occurred: {e.Exception.Message}",
Caption = "Fatal Error",
Icon = MessageBoxImage.Error,
Buttons = new[]
{
Buttons =
[
MessageBoxButtons.Custom("Reset Settings", EErrorKind.ResetSettings),
MessageBoxButtons.Custom("Restart", EErrorKind.Restart),
MessageBoxButtons.Custom("OK", EErrorKind.Ignore)
},
],
IsSoundEnabled = false
};

View File

@ -1,10 +1,9 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Numerics;
using System.Reflection;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Extensions;
using CUE4Parse.Utils;
namespace FModel;
@ -12,8 +11,9 @@ public static class Constants
{
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_COMMIT_ID = FileVersionInfo.GetVersionInfo(APP_PATH).ProductVersion?.SubstringAfter('+');
public static readonly string APP_SHORT_COMMIT_ID = APP_COMMIT_ID[..7];
public static readonly DateTime APP_BUILD_DATE = File.GetLastWriteTime(APP_PATH);
public const string ZERO_64_CHAR = "0000000000000000000000000000000000000000000000000000000000000000";
public static readonly FGuid ZERO_GUID = new(0U);

View File

@ -3,7 +3,7 @@ using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Core.i18N;
using CUE4Parse.UE4.Objects.UObject;
using FModel.Extensions;
using CUE4Parse.Utils;
using FModel.Framework;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
@ -13,12 +13,11 @@ namespace FModel.Creator.Bases.FN;
public class BaseBundle : UCreator
{
private IList<BaseQuest> _quests;
private const int _headerHeight = 100;
public BaseBundle(UObject uObject, EIconStyle style) : base(uObject, style)
{
Width = 1024;
Height = _headerHeight;
Height = 0;
Margin = 0;
}
@ -56,84 +55,31 @@ public class BaseBundle : UCreator
if (!completionReward.TryGetValue(out int completionCount, "CompletionCount") ||
!completionReward.TryGetValue(out FStructFallback[] rewards, "Rewards")) continue;
var quest = new BaseQuest(completionCount, Style);
foreach (var reward in rewards)
{
if (!reward.TryGetValue(out int quantity, "Quantity") ||
!reward.TryGetValue(out string templateId, "TemplateId") ||
!reward.TryGetValue(out FSoftObjectPath itemDefinition, "ItemDefinition")) continue;
if (!itemDefinition.AssetPathName.IsNone &&
!itemDefinition.AssetPathName.Text.Contains("/Items/Tokens/") &&
!itemDefinition.AssetPathName.Text.Contains("/Items/Quests"))
{
_quests.Add(new BaseQuest(completionCount, itemDefinition, Style));
}
else if (!string.IsNullOrWhiteSpace(templateId))
{
_quests.Add(new BaseQuest(completionCount, quantity, templateId, Style));
}
if (!reward.TryGetValue(out FSoftObjectPath itemDefinition, "ItemDefinition")) continue;
quest.AddCompletionReward(itemDefinition);
}
_quests.Add(quest);
}
}
Height += 256 * _quests.Count;
Height += 200 * _quests.Count;
}
public override SKBitmap[] Draw()
{
var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Opaque);
var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Unpremul);
using var c = new SKCanvas(ret);
DrawHeader(c);
DrawDisplayName(c);
DrawQuests(c);
return new[] { ret };
}
private readonly SKPaint _headerPaint = new()
{
IsAntialias = true, FilterQuality = SKFilterQuality.High,
Typeface = Utils.Typefaces.Bundle, TextSize = 50,
TextAlign = SKTextAlign.Center, Color = SKColor.Parse("#262630")
};
private void DrawHeader(SKCanvas c)
{
c.DrawRect(new SKRect(0, 0, Width, _headerHeight), _headerPaint);
var background = _quests.Count > 0 ? _quests[0].Background : Background;
_headerPaint.Shader = SKShader.CreateRadialGradient(new SKPoint(Width / 2, _headerHeight / 2), Width / 5 * 4,
new[] { background[0].WithAlpha(50), background[1].WithAlpha(50) }, SKShaderTileMode.Clamp);
c.DrawRect(new SKRect(0, 0, Width, _headerHeight), _headerPaint);
_headerPaint.Shader = SKShader.CreateLinearGradient(new SKPoint(Width / 2, _headerHeight), new SKPoint(Width / 2, 75),
new[] { SKColors.Black.WithAlpha(25), background[1].WithAlpha(0) }, SKShaderTileMode.Clamp);
c.DrawRect(new SKRect(0, 75, Width, _headerHeight), _headerPaint);
}
private new void DrawDisplayName(SKCanvas c)
{
if (string.IsNullOrEmpty(DisplayName)) return;
_headerPaint.Shader = null;
_headerPaint.Color = SKColors.White;
while (_headerPaint.MeasureText(DisplayName) > Width)
{
_headerPaint.TextSize -= 1;
}
var shaper = new CustomSKShaper(_headerPaint.Typeface);
c.DrawShapedText(shaper, DisplayName, Width / 2f, _headerHeight / 2f + _headerPaint.TextSize / 2 - 10, _headerPaint);
}
private void DrawQuests(SKCanvas c)
{
var y = _headerHeight;
var y = 0;
foreach (var quest in _quests)
{
quest.DrawQuest(c, y);
y += quest.Height;
}
return [ret];
}
}

View File

@ -58,6 +58,9 @@ public class BaseCommunity : BaseIcon
if (Object.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags"))
CheckGameplayTags(gameplayTags);
else if (Object.TryGetValue(out FInstancedStruct[] dataList, "DataList"))
CheckGameplayTags(dataList);
if (Object.TryGetValue(out FPackageIndex cosmeticItem, "cosmetic_item"))
CosmeticSource = cosmeticItem.Name.ToUpper();
@ -91,7 +94,7 @@ public class BaseCommunity : BaseIcon
return new[] { ret };
}
private void CheckGameplayTags(FGameplayTagContainer gameplayTags)
protected override void CheckGameplayTags(FGameplayTagContainer gameplayTags)
{
if (_design == null) return;
if (_design.DrawSource)
@ -120,8 +123,9 @@ public class BaseCommunity : BaseIcon
{
if (!bShort) return base.GetCosmeticSeason(seasonNumber);
var s = seasonNumber["Cosmetics.Filter.Season.".Length..];
(int chapterIdx, int seasonIdx) = GetInternalSID(int.Parse(s));
return $"C{chapterIdx} S{seasonIdx}";
(string chapterIdx, string seasonIdx, bool onlySeason) = GetInternalSID(s);
var prefix = int.TryParse(seasonIdx, out _) ? "S" : "";
return onlySeason ? $"{prefix}{seasonIdx}" : $"C{chapterIdx} {prefix}{seasonIdx}";
}
private new void DrawBackground(SKCanvas c)

View File

@ -62,7 +62,7 @@ public class BaseIcon : UCreator
// 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"))
if (Object.TryGetValue(out FText description, "Description", "ItemDescription", "SetDescription", "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));
@ -88,6 +88,8 @@ public class BaseIcon : UCreator
{
ParseForReward(UserSettings.Default.CosmeticDisplayAsset);
if (Object.TryGetValue(out FInstancedStruct[] dataList, "DataList"))
CheckGameplayTags(dataList);
if (Object.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags"))
CheckGameplayTags(gameplayTags);
if (Object.TryGetValue(out FPackageIndex cosmeticItem, "cosmetic_item"))
@ -157,7 +159,7 @@ public class BaseIcon : UCreator
{
if (uObject is UTexture2D texture2D)
{
SeriesBackground = texture2D.Decode();
SeriesBackground = texture2D.Decode().ToSkBitmap();
return;
}
@ -215,46 +217,31 @@ public class BaseIcon : UCreator
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);
var format = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_SetMembership", "\nPart of the <SetName>{0}</> set.");
return Utils.RemoveHtmlTags(string.Format(format, name));
}
protected (int, int) GetInternalSID(int number)
protected (string, string, bool) GetInternalSID(string number)
{
static int GetSeasonsInChapter(int chapter) => chapter switch
{
1 => 10,
2 => 8,
3 => 4,
4 => 5,
_ => 10
};
if (!Utils.TryLoadObject("FortniteGame/Plugins/GameFeatures/BattlePassBase/Content/DataTables/Athena_SeasonTitles.Athena_SeasonTitles", out UDataTable seasonTitles) ||
!seasonTitles.TryGetDataTableRow(number, StringComparison.InvariantCulture, out var row) ||
!row.TryGetValue(out FText chapterText, "DisplayChapterText") ||
!row.TryGetValue(out FText seasonText, "DisplaySeasonText") ||
!row.TryGetValue(out FName displayType, "DisplayType"))
return (string.Empty, string.Empty, true);
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);
var onlySeason = displayType.Text.EndsWith("::OnlySeason") || (chapterText.Text == seasonText.Text && !int.TryParse(seasonText.Text, out _));
return (chapterText.Text, seasonText.Text, onlySeason);
}
protected string GetCosmeticSeason(string seasonNumber)
{
var s = seasonNumber["Cosmetics.Filter.Season.".Length..];
var initial = int.Parse(s);
(int chapterIdx, int seasonIdx) = GetInternalSID(initial);
(string chapterIdx, string seasonIdx, bool onlySeason) = GetInternalSID(s);
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)));
if (onlySeason) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, seasonIdx)));
var chapter = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterTextFormat", "Chapter {0}");
var chapterFormat = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterSeasonTextFormat", "{0}, {1}");
@ -262,7 +249,15 @@ public class BaseIcon : UCreator
return Utils.RemoveHtmlTags(string.Format(introduced, d));
}
private void CheckGameplayTags(FGameplayTagContainer gameplayTags)
protected void CheckGameplayTags(FInstancedStruct[] dataList)
{
if (dataList.FirstOrDefault(d => d.NonConstStruct?.TryGetValue(out FGameplayTagContainer _, "Tags") ?? false) is { NonConstStruct: not null } tags)
{
CheckGameplayTags(tags.NonConstStruct.Get<FGameplayTagContainer>("Tags"));
}
}
protected virtual void CheckGameplayTags(FGameplayTagContainer gameplayTags)
{
if (gameplayTags.TryGetGameplayTag("Cosmetics.Source.", out var source))
CosmeticSource = source.Text["Cosmetics.Source.".Length..];

View File

@ -84,44 +84,80 @@ public class BaseIconStats : BaseIcon
weaponStatHandle.TryGetValue(out UDataTable dataTable, "DataTable") &&
dataTable.TryGetDataTableRow(weaponRowName.Text, StringComparison.OrdinalIgnoreCase, out var weaponRowValue))
{
if (weaponRowValue.TryGetValue(out int bpc, "BulletsPerCartridge"))
weaponRowValue.TryGetValue(out float dmgPb, "DmgPB"); //Damage at point blank
weaponRowValue.TryGetValue(out float mdpc, "MaxDamagePerCartridge"); //Max damage a weapon can do in a single hit, usually used for shotguns
weaponRowValue.TryGetValue(out float dmgCritical, "DamageZone_Critical"); //Headshot multiplier
weaponRowValue.TryGetValue(out int clipSize, "ClipSize"); //Item magazine size
weaponRowValue.TryGetValue(out float firingRate, "FiringRate"); //Item firing rate, value is shots per second
weaponRowValue.TryGetValue(out float swingTime, "SwingTime"); //Item swing rate, value is swing per second
weaponRowValue.TryGetValue(out float armTime, "ArmTime"); //Time it takes for traps to be able to be set off
weaponRowValue.TryGetValue(out float reloadTime, "ReloadTime"); //Time it takes for a weapon to reload
weaponRowValue.TryGetValue(out int bpc, "BulletsPerCartridge"); //Amount of pellets shot by a weapon at once, usually for shotguns
weaponRowValue.TryGetValue(out float heatMax, "OverheatingMaxValue"); //Maximum heat overheating weapons can hold before they need to cool off
weaponRowValue.TryGetValue(out float heatPerShot, "OverheatHeatingValue"); //Heat generated per shot on overheat weapons
weaponRowValue.TryGetValue(out float overheatCooldown, "OverheatedCooldownDelay"); //Cooldown after a weapon reaches its maximum heat capacity
weaponRowValue.TryGetValue(out int cartridgePerFire, "CartridgePerFire"); //Amount of bullets shot after pressing the fire button once
weaponRowValue.TryGetValue(out float burstFiringRate, "BurstFiringRate"); //Item firing rate during a burst, value is shots per second
{
var multiplier = bpc != 0f ? bpc : 1;
if (weaponRowValue.TryGetValue(out float dmgPb, "DmgPB") && dmgPb != 0f)
if (dmgPb != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "35D04D1B45737BEA25B69686D9E085B9", "Damage"), dmgPb * multiplier, 200));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "35D04D1B45737BEA25B69686D9E085B9", "Damage"), dmgPb * multiplier, 160));
}
if (weaponRowValue.TryGetValue(out float mdpc, "MaxDamagePerCartridge") && mdpc >= 0f)
if (mdpc > 0f && dmgPb * dmgCritical * multiplier > mdpc)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "0DEF2455463B008C4499FEA03D149EDF", "Headshot Damage"), mdpc, 200));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "0DEF2455463B008C4499FEA03D149EDF", "Headshot Damage"), mdpc, 160));
}
else if (weaponRowValue.TryGetValue(out float dmgCritical, "DamageZone_Critical"))
else if (dmgCritical != 0f && dmgCritical != 1f && dmgPb != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "0DEF2455463B008C4499FEA03D149EDF", "Headshot Damage"), dmgPb * dmgCritical * multiplier, 200));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "0DEF2455463B008C4499FEA03D149EDF", "Headshot Damage"), dmgPb * dmgCritical * multiplier, 160));
}
}
if (weaponRowValue.TryGetValue(out int clipSize, "ClipSize") && clipSize != 0)
if (clipSize > 999f || clipSize == 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "068239DD4327B36124498C9C5F61C038", "Magazine Size"), clipSize, 50));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "068239DD4327B36124498C9C5F61C038", "Magazine Size"), Utils.GetLocalizedResource("", "0FAE8E5445029F2AA209ADB0FE49B23C", "Infinite"), -1));
}
if (weaponRowValue.TryGetValue(out float firingRate, "FiringRate") && firingRate != 0f)
else if (clipSize != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), firingRate, 15));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "068239DD4327B36124498C9C5F61C038", "Magazine Size"), clipSize, 40));
}
if (weaponRowValue.TryGetValue(out float armTime, "ArmTime") && armTime != 0f)
var burstEquation = cartridgePerFire != 0f && burstFiringRate != 0f ? (cartridgePerFire / (((cartridgePerFire - 1f) / burstFiringRate) + (1f / firingRate))) : 0f;
if (burstEquation != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "3BFEB8BD41A677CC5F45B9A90D6EAD6F", "Arming Delay"), armTime, 125));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), burstEquation, 11));
}
else if (firingRate != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), firingRate, 11));
}
else if (swingTime != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), swingTime, 11));
}
if (weaponRowValue.TryGetValue(out float reloadTime, "ReloadTime") && reloadTime != 0f)
if (armTime != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "6EA26D1A4252034FBD869A90F9A6E49A", "Reload Time"), reloadTime, 15));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "3BFEB8BD41A677CC5F45B9A90D6EAD6F", "Arming Delay"), armTime, 5));
}
if (reloadTime != 0f && clipSize < 999f && clipSize != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "6EA26D1A4252034FBD869A90F9A6E49A", "Reload Time"), reloadTime, 10));
}
if (overheatCooldown != 0f && clipSize > 999f || overheatCooldown != 0f && clipSize == 0f)
{
_statistics.Add(new IconStat("Overheat Cooldown", overheatCooldown, 5));
}
if (heatMax != 0f && heatPerShot != 0f && clipSize > 999f || heatMax != 0f && heatPerShot != 0f && clipSize == 0f)
{
_statistics.Add(new IconStat("Shots to Overheat", Math.Ceiling(heatMax / heatPerShot), 80));
}
if ((Object.ExportType.Equals("FortContextTrapItemDefinition", StringComparison.OrdinalIgnoreCase) ||
Object.ExportType.Equals("FortTrapItemDefinition", StringComparison.OrdinalIgnoreCase)) &&
weaponRowValue.TryGetValue(out UDataTable durabilityTable, "Durability") &&
@ -215,6 +251,7 @@ public class BaseIconStats : BaseIcon
_informationPaint.TextSize = 50;
_informationPaint.Color = SKColors.White;
_informationPaint.Typeface = Utils.Typefaces.Bundle;
_informationPaint.FakeBoldText = true;
while (_informationPaint.MeasureText(DisplayName) > Width - _headerHeight * 2)
{
_informationPaint.TextSize -= 1;
@ -284,6 +321,10 @@ public class IconStat
_statPaint.Color = SKColors.White;
c.DrawText(_value.ToString(), new SKPoint(width - 50, y + 10), _statPaint);
if (_maxValue == -1) //fill bar if max value is set to -1, for things that don't return a number here but should still be represented as the maximum value
{
c.DrawRect(new SKRect(height * 2, y, Math.Min(width - height, sliderRight), y + 5), _statPaint);
}
if (_maxValue < 1 || !float.TryParse(_value.ToString(), out var floatValue)) return;
if (floatValue < 0)
floatValue = 0;

View File

@ -28,7 +28,7 @@ public class BaseJuno : BaseIcon
{
foreach (var data in additionalData)
{
if (data.NonConstStruct?.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "SmallPreviewImage") ?? false)
if (data.NonConstStruct?.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "SmallPreviewImage") == true)
{
_character.Preview = Utils.GetBitmap(largePreview);
break;

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Engine;
@ -7,7 +8,7 @@ using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Core.i18N;
using CUE4Parse.UE4.Objects.UObject;
using FModel.Extensions;
using CUE4Parse.Utils;
using FModel.Framework;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
@ -17,9 +18,8 @@ namespace FModel.Creator.Bases.FN;
public class BaseQuest : BaseIcon
{
private int _count;
private Reward _reward;
private readonly List<Reward> _rewards;
private readonly bool _screenLayer;
private readonly string[] _unauthorizedReward = { "Token", "ChallengeBundle", "GiftBox" };
public string NextQuestName { get; private set; }
@ -27,15 +27,15 @@ public class BaseQuest : BaseIcon
{
Margin = 0;
Width = 1024;
Height = 256;
DefaultPreview = Utils.GetBitmap("FortniteGame/Content/Athena/HUD/Quests/Art/T_NPC_Default.T_NPC_Default");
Height = 200;
_rewards = [];
if (uObject != null)
{
_screenLayer = uObject.ExportType.Equals("FortFeatItemDefinition", StringComparison.OrdinalIgnoreCase);
}
}
private BaseQuest(int completionCount, EIconStyle style) : this(null, style) // completion
public BaseQuest(int completionCount, EIconStyle style) : this(null, style) // completion
{
var description = completionCount < 0 ?
Utils.GetLocalizedResource("AthenaChallengeDetailsEntry", "CompletionRewardFormat_All", "Complete <text color=\"FFF\" case=\"upper\" fontface=\"black\">all {0} challenges</> to earn the reward item") :
@ -44,14 +44,14 @@ public class BaseQuest : BaseIcon
DisplayName = ReformatString(description, completionCount.ToString(), completionCount < 0);
}
public BaseQuest(int completionCount, FSoftObjectPath itemDefinition, EIconStyle style) : this(completionCount, style) // completion
public void AddCompletionReward(FSoftObjectPath itemDefinition)
{
_reward = Utils.TryLoadObject(itemDefinition.AssetPathName.Text, out UObject uObject) ? new Reward(uObject) : new Reward();
_rewards.Add(itemDefinition.TryLoad(out UObject uObject) ? new Reward(uObject) : new Reward());
}
public BaseQuest(int completionCount, int quantity, string reward, EIconStyle style) : this(completionCount, style) // completion
public void AddCompletionReward(int quantity, string reward)
{
_reward = new Reward(quantity, reward);
_rewards.Add(new Reward(quantity, reward));
}
public override void ParseForInfo()
@ -69,12 +69,7 @@ public class BaseQuest : BaseIcon
}
else
{
if (!string.IsNullOrEmpty(ShortDescription))
Description = ShortDescription;
if (string.IsNullOrEmpty(DisplayName) && !string.IsNullOrEmpty(Description))
DisplayName = Description;
if (DisplayName == Description)
Description = string.Empty;
Description = string.Empty;
if ((Object.TryGetValue(out FSoftObjectPath icon, "QuestGiverWidgetIcon", "NotificationIconOverride") &&
Utils.TryLoadObject(icon.AssetPathName.Text, out UObject iconObject)) ||
@ -98,8 +93,8 @@ public class BaseQuest : BaseIcon
if (Object.TryGetValue(out FStructFallback[] objectives, "Objectives") && objectives.Length > 0)
{
// actual description doesn't exist
if (string.IsNullOrEmpty(Description) && objectives[0].TryGetValue(out FText description, "Description"))
Description = description.Text;
if (string.IsNullOrEmpty(DisplayName) && objectives[0].TryGetValue(out FText description, "Description"))
DisplayName = description.Text;
// ObjectiveCompletionCount doesn't exist
if (_count == 0)
@ -111,77 +106,65 @@ public class BaseQuest : BaseIcon
}
}
if (Object.TryGetValue(out FStructFallback[] rewards, "Rewards"))
if (Object.TryGetValue(out FPackageIndex[] questDefinitionComponents, "QuestDefinitionComponents"))
{
foreach (var reward in rewards)
foreach (var questDefinitionComponent in questDefinitionComponents)
{
if (!reward.TryGetValue(out FStructFallback itemPrimaryAssetId, "ItemPrimaryAssetId") ||
!reward.TryGetValue(out int quantity, "Quantity")) continue;
if (!questDefinitionComponent.Name.StartsWith("FortQuestDefinitionComponent_Rewards") ||
!questDefinitionComponent.TryLoad(out var rewardComponent) ||
!rewardComponent.TryGetValue(out FInstancedStruct[] questRewardsArray, "QuestRewardsArray")) continue;
if (!itemPrimaryAssetId.TryGetValue(out FStructFallback primaryAssetType, "PrimaryAssetType") ||
!itemPrimaryAssetId.TryGetValue(out FName primaryAssetName, "PrimaryAssetName") ||
!primaryAssetType.TryGetValue(out FName name, "Name")) continue;
if (name.Text.Equals("Quest", StringComparison.OrdinalIgnoreCase))
foreach (var questReward in questRewardsArray)
{
NextQuestName = primaryAssetName.Text;
if (questReward.NonConstStruct.TryGetValue(out FStructFallback[] resourceDataTableRewards, "ResourceDataTableRewards") &&
resourceDataTableRewards.Length > 0 && resourceDataTableRewards[0].TryGetValue(out FStructFallback tableRowEntry, "TableRowEntry") &&
tableRowEntry.TryGetValue(out UDataTable rewardsTable, "DataTable") &&
tableRowEntry.TryGetValue(out FName rowName, "RowName") &&
rewardsTable.TryGetDataTableRow(rowName.Text, StringComparison.InvariantCulture, out var row) &&
row.TryGetValue(out FSoftObjectPath resourceDefinition, "ResourceDefinition") &&
row.TryGetValue(out int quantity, "Quantity"))
{
_rewards.Add(new Reward(quantity, resourceDefinition));
}
else if (questReward.NonConstStruct.TryGetValue(out FInstancedStruct[] rewards, "CosmeticRewards", "CurrencyRewards", "VariantTokenRewards", "ResourceRewards"))
{
foreach (var reward in rewards)
{
if (reward.NonConstStruct.TryGetValue(out FSoftObjectPath cosmeticDefinition, "CosmeticDefinition", "CurrencyDefinition", "VariantTokenDefinition", "ResourceDefinition"))
{
if (reward.NonConstStruct.TryGetValue(out int count, "CurrencyCount", "ResourceCount"))
{
_rewards.Add(new Reward(count, cosmeticDefinition));
}
else if (cosmeticDefinition.TryLoad(out var cosmetic))
{
_rewards.Add(new Reward(cosmetic));
}
}
}
}
}
else if (!_unauthorizedReward.Contains(name.Text))
{
_reward = new Reward(quantity, $"{name}:{primaryAssetName}");
}
}
}
if (_reward == null)
{
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"))
{
_reward = new Reward(quantity, templateId);
}
}
}
if (_reward == null && Object.TryGetValue(out FStructFallback[] hiddenRewards, "HiddenRewards"))
{
foreach (var hiddenReward in hiddenRewards)
{
if (!hiddenReward.TryGetValue(out FName templateId, "TemplateId") ||
!hiddenReward.TryGetValue(out int quantity, "Quantity")) continue;
_reward = new Reward(quantity, templateId);
break;
}
}
_reward ??= new Reward();
}
public void DrawQuest(SKCanvas c, int y)
{
DrawBackground(c, y);
var x = Preview is null ? 0 : Height + 10;
DrawBackground(c, x, y);
DrawPreview(c, y);
DrawTexts(c, y);
DrawTexts(c, x + 50, y, 50);
}
public override SKBitmap[] Draw()
{
var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Opaque);
var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Unpremul);
using var c = new SKCanvas(ret);
DrawQuest(c, 0);
return new[] { ret };
return [ret];
}
private string ReformatString(string s, string completionCount, bool isAll)
@ -202,77 +185,77 @@ public class BaseQuest : BaseIcon
private readonly SKPaint _informationPaint = new()
{
IsAntialias = true, FilterQuality = SKFilterQuality.High,
Color = SKColor.Parse("#262630")
Color = SKColor.Parse("#183E94"),
};
private void DrawBackground(SKCanvas c, int y)
private void DrawBackground(SKCanvas c, int x, int y)
{
c.DrawRect(new SKRect(Margin, y, Width, y + Height), _informationPaint);
_informationPaint.Shader = SKShader.CreateRadialGradient(new SKPoint(Width / 2, y + Height / 2), Width / 5 * 4,
new[] { Background[0].WithAlpha(50), Background[1].WithAlpha(50) }, SKShaderTileMode.Clamp);
c.DrawRect(new SKRect(Height / 2, y, Width, y + Height), _informationPaint);
_informationPaint.Shader = SKShader.CreateLinearGradient(new SKPoint(Width / 2, y + Height), new SKPoint(Width / 2, 75),
new[] { SKColors.Black.WithAlpha(25), Background[1].WithAlpha(0) }, SKShaderTileMode.Clamp);
c.DrawRect(new SKRect(0, y + 75, Width, y + Height), _informationPaint);
_informationPaint.Shader = SKShader.CreateRadialGradient(new SKPoint(Width / 2, y + Height / 2), Width / 5 * 4, Background, SKShaderTileMode.Clamp);
c.DrawRect(new SKRect(Margin, y, Height / 2, y + Height), _informationPaint);
_informationPaint.Shader = SKShader.CreateLinearGradient(new SKPoint(Height / 2, y + Height / 2), new SKPoint(Height / 2 + 100, y + Height / 2),
new[] { SKColors.Black.WithAlpha(25), Background[1].WithAlpha(0) }, SKShaderTileMode.Clamp);
c.DrawRect(new SKRect(Height / 2, y, Height / 2 + 100, y + Height), _informationPaint);
_informationPaint.Shader = SKShader.CreateRadialGradient(new SKPoint(Width * 0.75f, y + Height * 0.5f), Width * 0.75f,
[SKColor.Parse("#1565D0"), SKColor.Parse("#1B1150")], SKShaderTileMode.Clamp);
c.DrawRoundRect(new SKRect(x, y, Width, y + Height), 25, 25, _informationPaint);
}
private void DrawPreview(SKCanvas c, int y)
{
if (Preview is null) return;
ImagePaint.BlendMode = _screenLayer ? SKBlendMode.Screen : Preview == null ? SKBlendMode.ColorBurn : SKBlendMode.SrcOver;
c.DrawBitmap(Preview ?? DefaultPreview, new SKRect(Margin, y, Height - Margin, y + Height), ImagePaint);
var rect = new SKRect(0, y, Height, y + Height);
c.Save();
using (var roundRectPath = new SKPath())
{
roundRectPath.AddRoundRect(rect, 15, 15);
c.ClipPath(roundRectPath, antialias: true);
}
_informationPaint.Shader = SKShader.CreateLinearGradient(
new SKPoint(rect.Left, rect.Bottom),
new SKPoint(rect.Left, rect.Top),
[
_informationPaint.Color,
_informationPaint.Color.WithAlpha(0)
],
[0, 1],
SKShaderTileMode.Clamp
);
c.DrawRect(rect, _informationPaint);
c.DrawBitmap(Preview, rect, ImagePaint);
c.Restore();
}
private void DrawTexts(SKCanvas c, int y)
private void DrawTexts(SKCanvas c, int x, int y, int padding)
{
_informationPaint.Shader = null;
_informationPaint.Color = SKColors.White;
float maxX = Width - padding;
float steps = Height * 0.5f;
foreach (var reward in _rewards)
{
reward.DrawQuest(c, new SKRect(maxX - steps, y + padding, maxX, y + padding + steps));
maxX -= steps;
}
maxX -= steps * 0.5f;
if (!string.IsNullOrWhiteSpace(DisplayName))
{
_informationPaint.TextSize = 40;
_informationPaint.Color = SKColors.White;
_informationPaint.TextSize = 25;
_informationPaint.Typeface = Utils.Typefaces.Bundle;
while (_informationPaint.MeasureText(DisplayName) > Width - Height - 10)
{
_informationPaint.TextSize -= 1;
}
var shaper = new CustomSKShaper(_informationPaint.Typeface);
c.DrawShapedText(shaper, DisplayName, Height, y + 50, _informationPaint);
Utils.DrawMultilineText(c, DisplayName, Width - padding, 0, SKTextAlign.Left,
new SKRect(x, y + padding, maxX, Height - padding * 1.5f), _informationPaint, out _);
}
var outY = y + 75f;
if (!string.IsNullOrWhiteSpace(Description))
{
_informationPaint.TextSize = 16;
_informationPaint.Color = SKColors.White.WithAlpha(175);
_informationPaint.Typeface = Utils.Typefaces.Description;
Utils.DrawMultilineText(c, Description, Width - Height, y, SKTextAlign.Left,
new SKRect(Height, outY, Width - 10, y + Height), _informationPaint, out outY);
}
_informationPaint.Color = Border[0].WithAlpha(100);
c.DrawRect(new SKRect(Height, outY, Width - 150, outY + 5), _informationPaint);
maxX -= steps * 0.5f;
if (maxX > Width * 0.7f) maxX = Width * 0.7f;
if (_count > 0)
{
_informationPaint.TextSize = 25;
_informationPaint.Color = SKColors.White;
_informationPaint.TextSize = 20;
_informationPaint.Typeface = Utils.Typefaces.BundleNumber;
c.DrawText("0 / ", new SKPoint(Width - 130, outY + 10), _informationPaint);
_informationPaint.Color = Border[0];
c.DrawText(_count.ToString(), new SKPoint(Width - 95, outY + 10), _informationPaint);
c.DrawText($"0/{_count}", new SKPoint(maxX, y + Height - padding), _informationPaint);
}
_reward.DrawQuest(c, new SKRect(Height, outY + 25, Width - 20, y + Height - 25));
_informationPaint.Color = SKColor.Parse("#121A45").WithAlpha(200);
c.DrawRoundRect(new SKRect(x, y + Height - padding - 12.5f, maxX - 12.5f, y + Height - padding), 5, 5, _informationPaint);
}
}

View File

@ -19,6 +19,18 @@ public class Reward
_rewardQuantity = "x0";
}
public Reward(int quantity, FSoftObjectPath softObjectPath) : this()
{
_rewardQuantity = $"{quantity / 1000f}k";
if (softObjectPath.TryLoad(out UObject d))
{
_theReward = new BaseIcon(d, EIconStyle.Default);
_theReward.ParseForReward(false);
_theReward.Border[0] = SKColors.White;
}
}
public Reward(int quantity, FName primaryAssetName) : this(quantity, primaryAssetName.Text)
{
}
@ -33,7 +45,7 @@ public class Reward
if (parts[0].Equals("HomebaseBannerIcon", StringComparison.CurrentCultureIgnoreCase))
{
if (!Utils.TryLoadObject($"FortniteGame/Content/Items/BannerIcons/{parts[1]}.{parts[1]}", out UObject p))
if (!Utils.TryLoadObject($"FortniteGame/Plugins/GameFeatures/BRCosmetics/Content/Items/BannerIcons/{parts[1]}.{parts[1]}", out UObject p))
return;
_theReward = new BaseIcon(p, EIconStyle.Default);
@ -51,7 +63,6 @@ public class Reward
_theReward = new BaseIcon(uObject, EIconStyle.Default);
_theReward.ParseForReward(false);
_theReward.Border[0] = SKColors.White;
_rewardQuantity = _theReward.DisplayName;
}
private readonly SKPaint _rewardPaint = new()
@ -61,28 +72,30 @@ public class Reward
public void DrawQuest(SKCanvas c, SKRect rect)
{
_rewardPaint.TextSize = 50;
_rewardPaint.TextSize = 25;
if (HasReward())
{
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;
_rewardPaint.ImageFilter = SKImageFilter.CreateDropShadow(0, 0, 5, 5, _theReward.Background[0].WithAlpha(150));
while (_rewardPaint.MeasureText(_rewardQuantity) > rect.Width)
var origin = new SKPoint(rect.Left, rect.Top);
if (!string.IsNullOrEmpty(_rewardQuantity))
{
_rewardPaint.TextSize -= 1;
origin.Y -= _rewardPaint.TextSize / 2;
}
var shaper = new CustomSKShaper(_rewardPaint.Typeface);
c.DrawShapedText(shaper, _rewardQuantity, rect.Left + rect.Height + 25, rect.MidY + 20, _rewardPaint);
}
else
{
c.DrawBitmap((_theReward.Preview ?? _theReward.DefaultPreview).Resize((int) rect.Height), origin, _rewardPaint);
if (string.IsNullOrEmpty(_rewardQuantity)) return;
_rewardPaint.TextAlign = SKTextAlign.Center;
_rewardPaint.FakeBoldText = true;
_rewardPaint.Color = SKColors.White;
_rewardPaint.Typeface = Utils.Typefaces.BundleNumber;
c.DrawText("No Reward", new SKPoint(rect.Left, rect.MidY + 20), _rewardPaint);
var shaper = new CustomSKShaper(Utils.Typefaces.BundleNumber);
c.DrawShapedText(shaper, _rewardQuantity, rect.Left + rect.Width * 0.5f, rect.Bottom, _rewardPaint);
}
// else
// {
// _rewardPaint.Color = SKColors.White;
// _rewardPaint.Typeface = Utils.Typefaces.BundleNumber;
// c.DrawText("No Reward", new SKPoint(rect.Left, rect.MidY + 20), _rewardPaint);
// }
}
public void DrawSeasonWin(SKCanvas c, int size)

View File

@ -1,21 +1,24 @@
using System;
using System.Diagnostics.CodeAnalysis;
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 string _pkgName;
private string _exportType;
private Lazy<UObject> _object;
private EIconStyle _style;
public CreatorPackage(UObject uObject, EIconStyle style)
public CreatorPackage(string packageName, string exportType, Lazy<UObject> uObject, EIconStyle style)
{
_pkgName = packageName;
_exportType = exportType;
_object = uObject;
_style = style;
}
@ -28,12 +31,12 @@ public class CreatorPackage : IDisposable
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryConstructCreator(out UCreator creator)
public bool TryConstructCreator([MaybeNullWhen(false)] out UCreator creator)
{
switch (_object.ExportType)
// TODO: convert to a type based system
switch (_exportType)
{
// Fortnite
case "FortCreativeWeaponMeleeItemDefinition":
case "AthenaConsumableEmoteItemDefinition":
case "AthenaSkyDiveContrailItemDefinition":
case "AthenaLoadingScreenItemDefinition":
@ -44,9 +47,13 @@ public class CreatorPackage : IDisposable
case "AthenaCharacterItemDefinition":
case "AthenaMapMarkerItemDefinition":
case "AthenaBackpackItemDefinition":
case "CosmeticShoesItemDefinition":
case "CosmeticCompanionItemDefinition":
case "CosmeticCompanionReactFXItemDefinition":
case "AthenaPickaxeItemDefinition":
case "AthenaGadgetItemDefinition":
case "AthenaGliderItemDefinition":
case "AthenaHatItemDefinition":
case "AthenaSprayItemDefinition":
case "AthenaDanceItemDefinition":
case "AthenaEmojiItemDefinition":
@ -88,6 +95,7 @@ public class CreatorPackage : IDisposable
case "FortBackpackItemDefinition":
case "FortEventQuestMapDataAsset":
case "FortBuildingItemDefinition":
case "FortItemCacheItemDefinition":
case "FortWeaponModItemDefinition":
case "FortCodeTokenItemDefinition":
case "FortSchematicItemDefinition":
@ -100,7 +108,7 @@ public class CreatorPackage : IDisposable
case "FortConsumableItemDefinition":
case "StWFortAccoladeItemDefinition":
case "FortAccountBuffItemDefinition":
case "FortWeaponMeleeItemDefinition":
case "FortFOBCoreDecoItemDefinition":
case "FortPlayerPerksItemDefinition":
case "FortPlaysetPropItemDefinition":
case "FortPrerollDataItemDefinition":
@ -110,6 +118,7 @@ public class CreatorPackage : IDisposable
case "FortPlayerAugmentItemDefinition":
case "FortSmartBuildingItemDefinition":
case "FortGiftBoxUnlockItemDefinition":
case "FortCreativeGadgetItemDefinition":
case "FortWeaponModItemDefinitionOptic":
case "RadioContentSourceItemDefinition":
case "FortPlaysetGrenadeItemDefinition":
@ -134,9 +143,7 @@ public class CreatorPackage : IDisposable
case "FortCampaignHeroLoadoutItemDefinition":
case "FortConditionalResourceItemDefinition":
case "FortChallengeBundleScheduleDefinition":
case "FortWeaponMeleeDualWieldItemDefinition":
case "FortDailyRewardScheduleTokenDefinition":
case "FortCreativeWeaponRangedItemDefinition":
case "FortVehicleCosmeticsItemDefinition_Body":
case "FortVehicleCosmeticsItemDefinition_Skin":
case "FortVehicleCosmeticsItemDefinition_Wheel":
@ -148,43 +155,46 @@ public class CreatorPackage : IDisposable
case "FortVehicleCosmeticsItemDefinition_EngineAudio":
creator = _style switch
{
EIconStyle.Cataba => new BaseCommunity(_object, _style, "Cataba"),
_ => new BaseIcon(_object, _style)
EIconStyle.Cataba => new BaseCommunity(_object.Value, _style, "Cataba"),
_ => new BaseIcon(_object.Value, _style)
};
return true;
case "JunoAthenaCharacterItemOverrideDefinition":
case "JunoAthenaDanceItemOverrideDefinition":
creator = new BaseJuno(_object, _style);
creator = new BaseJuno(_object.Value, _style);
return true;
case "FortTandemCharacterData":
creator = new BaseTandem(_object, _style);
creator = new BaseTandem(_object.Value, _style);
return true;
case "FortTrapItemDefinition":
case "FortSpyTechItemDefinition":
case "FortAccoladeItemDefinition":
case "FortContextTrapItemDefinition":
case "FortWeaponMeleeItemDefinition":
case "FortWeaponRangedItemDefinition":
case "FortCreativeWeaponMeleeItemDefinition":
case "FortWeaponMeleeDualWieldItemDefinition":
case "FortCreativeWeaponRangedItemDefinition":
case "Daybreak_LevelExitVehicle_PartItemDefinition_C":
creator = new BaseIconStats(_object, _style);
creator = new BaseIconStats(_object.Value, _style);
return true;
case "FortItemSeriesDefinition":
creator = new BaseSeries(_object, _style);
creator = new BaseSeries(_object.Value, _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);
when _pkgName.Contains("/MI_OfferImages/", StringComparison.OrdinalIgnoreCase) ||
_pkgName.Contains("/RenderSwitch_Materials/", StringComparison.OrdinalIgnoreCase) ||
_pkgName.Contains("/MI_BPTile/", StringComparison.OrdinalIgnoreCase):
creator = new BaseMaterialInstance(_object.Value, _style);
return true;
case "AthenaItemShopOfferDisplayData":
creator = new BaseOfferDisplayData(_object, _style);
creator = new BaseOfferDisplayData(_object.Value, _style);
return true;
case "FortMtxOfferData":
creator = new BaseMtxOffer(_object, _style);
creator = new BaseMtxOffer(_object.Value, _style);
return true;
case "FortPlaylistAthena":
creator = new BasePlaylist(_object, _style);
creator = new BasePlaylist(_object.Value, _style);
return true;
case "FortFeatItemDefinition":
case "FortQuestItemDefinition":
@ -192,17 +202,18 @@ public class CreatorPackage : IDisposable
case "FortQuestItemDefinition_Campaign":
case "AthenaDailyQuestDefinition":
case "FortUrgentQuestItemDefinition":
creator = new Bases.FN.BaseQuest(_object, _style);
creator = new Bases.FN.BaseQuest(_object.Value, _style);
return true;
case "FortCompendiumItemDefinition":
case "FortCompendiumBundleDefinition":
case "FortChallengeBundleItemDefinition":
creator = new BaseBundle(_object, _style);
creator = new BaseBundle(_object.Value, _style);
return true;
// case "AthenaSeasonItemDefinition":
// creator = new BaseSeason(_object, _style);
// return true;
case "FortItemAccessTokenType":
creator = new BaseItemAccessToken(_object, _style);
creator = new BaseItemAccessToken(_object.Value, _style);
return true;
case "FortCreativeOption":
case "PlaylistUserOptionEnum":
@ -216,14 +227,14 @@ public class CreatorPackage : IDisposable
case "PlaylistUserTintedIconIntEnum":
case "PlaylistUserOptionPrimaryAsset":
case "PlaylistUserOptionCollisionProfileEnum":
creator = new BaseUserControl(_object, _style);
creator = new BaseUserControl(_object.Value, _style);
return true;
// PandaGame
case "CharacterData":
creator = new BaseFighter(_object, _style);
creator = new BaseFighter(_object.Value, _style);
return true;
case "PerkGroup":
creator = new BasePerkGroup(_object, _style);
creator = new BasePerkGroup(_object.Value, _style);
return true;
case "StatTrackingBundleData":
case "HydraSyncedDataAsset":
@ -236,10 +247,10 @@ public class CreatorPackage : IDisposable
case "TauntData":
case "SkinData":
case "PerkData":
creator = new BasePandaIcon(_object, _style);
creator = new BasePandaIcon(_object.Value, _style);
return true;
case "QuestData":
creator = new Bases.MV.BaseQuest(_object, _style);
creator = new Bases.MV.BaseQuest(_object.Value, _style);
return true;
default:
creator = null;
@ -247,7 +258,7 @@ public class CreatorPackage : IDisposable
}
}
public override string ToString() => $"{_object.ExportType} | {_style}";
public override string ToString() => $"{_exportType} | {_style}";
public void Dispose()
{

View File

@ -14,7 +14,7 @@ public class Typefaces
private const string _EXT = ".ufont";
// FortniteGame
private const string _FORTNITE_BASE_PATH = "/Game/UI/Foundation/Fonts/";
private const string _FORTNITE_BASE_PATH = "FortniteGame/Plugins/FortUILibrary/Content/Fonts/";
private const string _ASIA_ERINM = "AsiaERINM"; // korean fortnite
private const string _BURBANK_BIG_CONDENSED_BLACK = "BurbankBigCondensed-Black"; // russian
private const string _BURBANK_BIG_REGULAR_BLACK = "BurbankBigRegular-Black";
@ -67,7 +67,7 @@ public class Typefaces
Default = SKTypeface.FromStream(Application.GetResourceStream(_BURBANK_BIG_CONDENSED_BOLD)?.Stream);
switch (viewModel.Provider.InternalGameName.ToUpperInvariant())
switch (viewModel.Provider.ProjectName.ToUpperInvariant())
{
case "FORTNITEGAME":
{
@ -105,7 +105,7 @@ public class Typefaces
_ => _BURBANK_SMALL_BOLD
} + _EXT, true);
BundleNumber = OnTheFly(_FORTNITE_BASE_PATH + _BURBANK_BIG_CONDENSED_BLACK + _EXT);
BundleNumber = OnTheFly(_FORTNITE_BASE_PATH + _BURBANK_SMALL_BOLD + _EXT);
Bundle = OnTheFly(_FORTNITE_BASE_PATH +
language switch
@ -116,7 +116,7 @@ public class Typefaces
ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK,
ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK,
ELanguage.Chinese => _NOTO_SANS_SC_BLACK,
_ => string.Empty
_ => _BURBANK_SMALL_BOLD
} + _EXT, true) ?? BundleNumber;
TandemDisplayName = OnTheFly(_FORTNITE_BASE_PATH +
@ -193,7 +193,7 @@ public class Typefaces
public SKTypeface OnTheFly(string path, bool fallback = false)
{
if (!_viewModel.Provider.TrySaveAsset(path, out var data)) return fallback ? null : Default;
var m = new MemoryStream(data) { Position = 0 };
var m = new MemoryStream(data) { Position = _viewModel.Provider.Versions.Game >= EGame.GAME_UE5_6 ? 4 : 0 };
return SKTypeface.FromStream(m);
}
}

View File

@ -12,6 +12,7 @@ using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.Utils;
using FModel.Framework;
using FModel.Extensions;
using FModel.Services;
@ -127,9 +128,9 @@ 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(FSoftObjectPath softObjectPath) => GetBitmap(softObjectPath.Load<UTexture2D>());
public static SKBitmap GetBitmap(string fullPath) => TryLoadObject(fullPath, out UTexture2D texture) ? GetBitmap(texture) : null;
public static SKBitmap GetBitmap(UTexture2D texture) => texture.Decode(UserSettings.Default.CurrentDir.TexturePlatform);
public static SKBitmap GetBitmap(UTexture2D texture) => texture.Decode(UserSettings.Default.CurrentDir.TexturePlatform).ToSkBitmap();
public static SKBitmap GetBitmap(byte[] data) => SKBitmap.Decode(data);
public static SKBitmap ResizeWithRatio(this SKBitmap me, double width, double height)
@ -160,12 +161,7 @@ public static class Utils
// fullpath must be either without any extension or with the export objectname
public static bool TryLoadObject<T>(string fullPath, out T export) where T : UObject
{
return _applicationView.CUE4Parse.Provider.TryLoadObject(fullPath, out export);
}
public static IEnumerable<UObject> LoadExports(string packagePath)
{
return _applicationView.CUE4Parse.Provider.LoadAllObjects(packagePath);
return _applicationView.CUE4Parse.Provider.TryLoadPackageObject(fullPath, out export);
}
public static float GetMaxFontSize(double sectorSize, SKTypeface typeface, string text, float degreeOfCertainty = 1f, float maxFont = 100f)
@ -199,11 +195,11 @@ public static class Utils
public static string GetLocalizedResource(string @namespace, string key, string defaultValue)
{
return _applicationView.CUE4Parse.Provider.GetLocalizedString(@namespace, key, defaultValue);
return _applicationView.CUE4Parse.Provider.Internationalization.SafeGet(@namespace, key, defaultValue);
}
public static string GetLocalizedResource<T>(T @enum) where T : Enum
{
var resource = _applicationView.CUE4Parse.Provider.GetLocalizedString("", @enum.GetDescription(), @enum.ToString());
var resource = _applicationView.CUE4Parse.Provider.Internationalization.SafeGet("", @enum.GetDescription(), @enum.ToString());
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(resource.ToLower());
}
@ -417,4 +413,4 @@ public static class Utils
return ret;
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using FModel.Extensions;
namespace FModel;
@ -60,19 +61,11 @@ public enum ELoadingMode
[Description("All (New)")]
AllButNew,
[Description("All (Modified)")]
AllButModified
AllButModified,
[Description("All (Except Patched Assets)")]
AllButPatched,
}
// public enum EUpdateMode
// {
// [Description("Stable")]
// Stable,
// [Description("Beta")]
// Beta,
// [Description("QA Testing")]
// Qa
// }
public enum ECompressedAudio
{
[Description("Play the decompressed data")]
@ -112,5 +105,46 @@ public enum EBulkType
Textures = 1 << 2,
Meshes = 1 << 3,
Skeletons = 1 << 4,
Animations = 1 << 5
Animations = 1 << 5,
Audio = 1 << 6
}
public enum EAssetCategory : uint
{
All = AssetCategoryExtensions.CategoryBase + (0 << 16),
Blueprints = AssetCategoryExtensions.CategoryBase + (1 << 16),
BlueprintGeneratedClass = Blueprints + 1,
WidgetBlueprintGeneratedClass = Blueprints + 2,
AnimBlueprintGeneratedClass = Blueprints + 3,
RigVMBlueprintGeneratedClass = Blueprints + 4,
UserDefinedEnum = Blueprints + 5,
UserDefinedStruct = Blueprints + 6,
//Metadata
Blueprint = Blueprints + 8,
CookedMetaData = Blueprints + 9,
Mesh = AssetCategoryExtensions.CategoryBase + (2 << 16),
StaticMesh = Mesh + 1,
SkeletalMesh = Mesh + 2,
Skeleton = Mesh + 3,
Texture = AssetCategoryExtensions.CategoryBase + (3 << 16),
Materials = AssetCategoryExtensions.CategoryBase + (4 << 16),
Material = Materials + 1,
MaterialEditorData = Materials + 2,
MaterialFunction = Materials + 3,
MaterialParameterCollection = Materials + 4,
Animation = AssetCategoryExtensions.CategoryBase + (5 << 16),
Level = AssetCategoryExtensions.CategoryBase + (6 << 16),
World = Level + 1,
BuildData = Level + 2,
LevelSequence = Level + 3,
Foliage = Level + 4,
Data = AssetCategoryExtensions.CategoryBase + (7 << 16),
ItemDefinitionBase = Data + 1,
CurveBase = Data + 2,
PhysicsAsset = Data + 3,
Media = AssetCategoryExtensions.CategoryBase + (8 << 16),
Audio = Media + 1,
Video = Media + 2,
Font = Media + 3,
Particle = AssetCategoryExtensions.CategoryBase + (9 << 16),
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace FModel.Extensions;
public static class AssetCategoryExtensions
{
public const uint CategoryBase = 0x00010000;
public static EAssetCategory GetBaseCategory(this EAssetCategory category)
{
return (EAssetCategory) ((uint) category & 0xFFFF0000);
}
public static bool IsOfCategory(this EAssetCategory item, EAssetCategory category)
{
return item.GetBaseCategory() == category.GetBaseCategory();
}
public static bool IsBaseCategory(this EAssetCategory category)
{
return category == category.GetBaseCategory();
}
public static IEnumerable<EAssetCategory> GetBaseCategories()
{
return Enum.GetValues<EAssetCategory>().Where(c => c.IsBaseCategory());
}
}

View File

@ -0,0 +1,70 @@
using System;
using CUE4Parse.FileProvider;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.Assets;
using CUE4Parse.UE4.Objects.UObject;
using FModel.Settings;
namespace FModel.Extensions;
public static class CUE4ParseExtensions
{
public class LoadPackageResult
{
// more than 1 export per page currently break the inner package navigation feature
// if you have 1k exports per page, at page 2, you click on export index 932
// it will find the export index 932 in the current page, which would realistically be 1932
// fix would be to use InclusiveStart and ExclusiveEnd to determine the page the export index is in
// giving the document access to this would fix the issue and we could re-use Package instead of reloading it but it's quite a bit of work atm
private const int PaginationThreshold = 5000;
private const int MaxExportPerPage = 1;
public IPackage Package;
public int RequestedIndex;
public bool IsPaginated => Package.ExportMapLength >= PaginationThreshold;
/// <summary>
/// index of the first export on the current page
/// this index is the starting point for additional data preview
///
/// it can be >0 even if <see cref="IsPaginated"/> is false if we want to focus data preview on a specific export
/// in this case, we will display all exports but only the focused one will be checked for data preview
/// </summary>
public int InclusiveStart => Math.Max(0, RequestedIndex - RequestedIndex % MaxExportPerPage);
/// <summary>
/// last exclusive export index of the current page
/// </summary>
public int ExclusiveEnd => IsPaginated
? Math.Min(InclusiveStart + MaxExportPerPage, Package.ExportMapLength)
: Package.ExportMapLength;
public int PageSize => ExclusiveEnd - InclusiveStart;
public string TabTitleExtra => IsPaginated ? $"Export{(PageSize > 1 ? "s" : "")} {InclusiveStart}{(PageSize > 1 ? $"-{ExclusiveEnd - 1}" : "")} of {Package.ExportMapLength - 1}" : null;
/// <summary>
/// display all exports unless paginated
/// </summary>
/// <param name="save">if we save the data we will display all exports even if <see cref="IsPaginated"/> is true</param>
/// <returns></returns>
public object GetDisplayData(bool save = false) => !save && IsPaginated
? Package.GetExports(InclusiveStart, PageSize)
: Package.GetExports();
}
public static LoadPackageResult GetLoadPackageResult(this IFileProvider provider, GameFile file, string objectName = null)
{
var result = new LoadPackageResult { Package = provider.LoadPackage(file) };
if (result.IsPaginated || (result.Package.HasFlags(EPackageFlags.PKG_ContainsMap) && UserSettings.Default.PreviewWorlds)) // focus on UWorld if it's a map we want to preview
{
result.RequestedIndex = result.Package.GetExportIndex(file.NameWithoutExtension);
if (objectName != null)
{
result.RequestedIndex = int.TryParse(objectName, out var index) ? index : result.Package.GetExportIndex(objectName);
}
}
return result;
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using CUE4Parse.UE4.Versions;
namespace FModel.Extensions;
@ -18,7 +19,8 @@ public static class EnumExtensions
var suffix = $"{value:D}";
var current = Convert.ToInt32(suffix);
var target = current & ~0xF;
var mask = value.GetType() == typeof(EGame) ? ~0xFFFF : ~0xF;
var target = current & mask;
if (current != target)
{
var values = Enum.GetValues(value.GetType());

View File

@ -6,14 +6,14 @@ using ICSharpCode.AvalonEdit.Document;
namespace FModel.Extensions;
public static class StringExtensions
public static partial class StringExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetReadableSize(double size)
{
if (size == 0) return "0 B";
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
string[] sizes = ["B", "KB", "MB", "GB", "TB"];
var order = 0;
while (size >= 1024 && order < sizes.Length - 1)
{
@ -24,94 +24,30 @@ public static class StringExtensions
return $"{size:# ###.##} {sizes[order]}".TrimStart();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringBefore(this string s, char delimiter)
{
var index = s.IndexOf(delimiter);
return index == -1 ? s : s[..index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringBefore(this string s, string delimiter, StringComparison comparisonType = StringComparison.Ordinal)
{
var index = s.IndexOf(delimiter, comparisonType);
return index == -1 ? s : s[..index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringAfter(this string s, char delimiter)
{
var index = s.IndexOf(delimiter);
return index == -1 ? s : s.Substring(index + 1, s.Length - index - 1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringAfter(this string s, string delimiter, StringComparison comparisonType = StringComparison.Ordinal)
{
var index = s.IndexOf(delimiter, comparisonType);
return index == -1 ? s : s.Substring(index + delimiter.Length, s.Length - index - delimiter.Length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringBeforeLast(this string s, char delimiter)
{
var index = s.LastIndexOf(delimiter);
return index == -1 ? s : s[..index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringBeforeLast(this string s, string delimiter, StringComparison comparisonType = StringComparison.Ordinal)
{
var index = s.LastIndexOf(delimiter, comparisonType);
return index == -1 ? s : s[..index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringBeforeWithLast(this string s, char delimiter)
{
var index = s.LastIndexOf(delimiter);
return index == -1 ? s : s[..(index + 1)];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringBeforeWithLast(this string s, string delimiter, StringComparison comparisonType = StringComparison.Ordinal)
{
var index = s.LastIndexOf(delimiter, comparisonType);
return index == -1 ? s : s[..(index + 1)];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringAfterLast(this string s, char delimiter)
{
var index = s.LastIndexOf(delimiter);
return index == -1 ? s : s.Substring(index + 1, s.Length - index - 1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringAfterLast(this string s, string delimiter, StringComparison comparisonType = StringComparison.Ordinal)
{
var index = s.LastIndexOf(delimiter, comparisonType);
return index == -1 ? s : s.Substring(index + delimiter.Length, s.Length - index - delimiter.Length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetNameLineNumber(this string s, string lineToFind)
{
if (KismetRegex().IsMatch(lineToFind))
return s.GetKismetLineNumber(lineToFind);
if (int.TryParse(lineToFind, out var index))
return s.GetLineNumber(index);
lineToFind = $" \"Name\": \"{lineToFind}\",";
return s.GetNameLineNumberText($" \"Name\": \"{lineToFind}\",");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetNameLineNumberText(this string s, string lineToFind)
{
using var reader = new StringReader(s);
var lineNum = 0;
string line;
while ((line = reader.ReadLine()) != null)
while (reader.ReadLine() is { } line)
{
lineNum++;
if (line.Equals(lineToFind, StringComparison.OrdinalIgnoreCase))
return lineNum;
}
return 1;
return -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -134,18 +70,17 @@ public static class StringExtensions
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetKismetLineNumber(this string s, string input)
private static int GetKismetLineNumber(this string s, string input)
{
var match = Regex.Match(input, @"^(.+)\[(\d+)\]$");
var match = KismetRegex().Match(input);
var name = match.Groups[1].Value;
int index = int.Parse(match.Groups[2].Value);
var lineToFind = $" \"Name\": \"{name}\",";
var offset = $"\"StatementIndex\": {index}";
using var reader = new StringReader(s);
var lineNum = 0;
string line;
while ((line = reader.ReadLine()) != null)
while (reader.ReadLine() is { } line)
{
lineNum++;
if (line.Equals(lineToFind, StringComparison.OrdinalIgnoreCase))
@ -161,7 +96,7 @@ public static class StringExtensions
}
}
return 1;
return -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -169,8 +104,7 @@ public static class StringExtensions
{
using var reader = new StringReader(s);
var lineNum = 0;
string line;
while ((line = reader.ReadLine()) != null)
while (reader.ReadLine() is { } line)
{
lineNum++;
if (line.Equals(" {"))
@ -180,6 +114,9 @@ public static class StringExtensions
return lineNum + 1;
}
return 1;
return -1;
}
[GeneratedRegex(@"^(.+)\[(\d+)\]$", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex KismetRegex();
}

View File

@ -11,23 +11,25 @@
<IsPackable>false</IsPackable>
<IsPublishable>true</IsPublishable>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishSingleFile Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">true</PublishSingleFile>
<PlatformTarget>x64</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<StartupObject>FModel.App</StartupObject>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<PlatformTarget>x64</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>1701;1702;NU1701</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PlatformTarget>x64</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PublishSingleFile>true</PublishSingleFile>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\android.png" />
<None Remove="Resources\apple.png" />
@ -102,6 +104,7 @@
<None Remove="Resources\npcleftside.png" />
<None Remove="Resources\default.frag" />
<None Remove="Resources\default.vert" />
<None Remove="Resources\spline.vert" />
<None Remove="Resources\grid.frag" />
<None Remove="Resources\grid.vert" />
<None Remove="Resources\skybox.frag" />
@ -122,6 +125,7 @@
<ItemGroup>
<EmbeddedResource Include="Resources\Json.xshd" />
<EmbeddedResource Include="Resources\Ini.xshd" />
<EmbeddedResource Include="Resources\spline.vert" />
<EmbeddedResource Include="Resources\Verse.xshd" />
<EmbeddedResource Include="Resources\Xml.xshd" />
<EmbeddedResource Include="Resources\Cpp.xshd" />
@ -149,22 +153,24 @@
<PackageReference Include="AdonisUI" Version="1.17.1" />
<PackageReference Include="AdonisUI.ClassicTheme" Version="1.17.1" />
<PackageReference Include="Autoupdater.NET.Official" Version="1.9.2" />
<PackageReference Include="AvalonEdit" Version="6.3.0.90" />
<PackageReference Include="AvalonEdit" Version="6.3.1.120" />
<PackageReference Include="CSCore" Version="1.2.1.2" />
<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="DiscordRichPresence" Version="1.6.1.70" />
<PackageReference Include="EpicManifestParser" Version="2.4.1" />
<PackageReference Include="EpicManifestParser.ZlibngDotNetDecompressor" Version="1.0.1" />
<PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.3.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="NVorbis" Version="0.10.5" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
<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" />
<PackageReference Include="OpenTK" Version="4.9.4" />
<PackageReference Include="RestSharp" Version="113.0.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.9" />
<PackageReference Include="Svg.Skia" Version="3.2.1" />
<PackageReference Include="Twizzle.ImGui-Bundle.NET" Version="1.91.5.2" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.3.2" />
</ItemGroup>
<ItemGroup>

5
FModel/FModel.slnx Normal file
View File

@ -0,0 +1,5 @@
<Solution>
<Project Path="../CUE4Parse/CUE4Parse-Conversion/CUE4Parse-Conversion.csproj" />
<Project Path="../CUE4Parse/CUE4Parse/CUE4Parse.csproj" />
<Project Path="FModel.csproj" />
</Solution>

View File

@ -0,0 +1,23 @@
using System;
using CUE4Parse.Compression;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Readers;
namespace FModel.Framework;
public class FakeGameFile(string path) : GameFile(path, 0)
{
public override bool IsEncrypted => false;
public override CompressionMethod CompressionMethod => CompressionMethod.None;
public override byte[] Read(FByteBulkDataHeader? header = null)
{
throw new NotImplementedException();
}
public override FArchive CreateReader(FByteBulkDataHeader? header = null)
{
throw new NotImplementedException();
}
}

View File

@ -3,12 +3,12 @@ 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 ImGuizmoNET;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Windowing.Desktop;
using OpenTK.Windowing.GraphicsLibraryFramework;
@ -54,11 +54,11 @@ public class ImGuiController : IDisposable
int major = GL.GetInteger(GetPName.MajorVersion);
int minor = GL.GetInteger(GetPName.MinorVersion);
KHRDebugAvailable = (major == 4 && minor >= 3) || IsExtensionSupported("KHR_debug");
IntPtr context = ImGui.CreateContext();
ImGui.SetCurrentContext(context);
ImGuizmo.SetImGuiContext(context);
var io = ImGui.GetIO();
unsafe
@ -69,29 +69,24 @@ public class ImGuiController : IDisposable
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);
io.Fonts.AddFontDefault();
io.Fonts.Build(); // Build font atlas
io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset;
io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags.DockingEnable;
io.Fonts.Flags |= ImFontAtlasFlags.NoBakedLines;
io.ConfigDockingWithShift = true;
io.ConfigWindowsMoveFromTitleBarOnly = true;
io.BackendRendererUserData = 0;
CreateDeviceResources();
SetPerFrameImGuiData(1f / 60f);
ImGui.NewFrame();
_frameBegun = true;
}
public void Normal() => PushFont(FontNormal);
public void Bold() => PushFont(FontBold);
public void SemiBold() => PushFont(FontSemiBold);
public void PopFont()
{
ImGui.PopFont();
PushFont(FontNormal);
}
private void PushFont(ImFontPtr ptr) => ImGui.PushFont(ptr);
public void WindowResized(int width, int height)
@ -129,34 +124,42 @@ public class ImGuiController : IDisposable
RecreateFontDeviceTexture();
string VertexSource = @"#version 460 core
string VertexSource = @"#version 330 core
uniform mat4 projection_matrix;
layout(location = 0) in vec2 in_position;
layout(location = 1) in vec2 in_texCoord;
layout(location = 2) in vec4 in_color;
out vec4 color;
out vec2 texCoord;
void main()
{
gl_Position = projection_matrix * vec4(in_position, 0, 1);
color = in_color;
texCoord = in_texCoord;
gl_Position = projection_matrix * vec4(in_position, 0, 1);
color = in_color;
texCoord = in_texCoord;
}";
string FragmentSource = @"#version 460 core
string FragmentSource = @"#version 330 core
uniform sampler2D in_fontTexture;
in vec4 color;
in vec2 texCoord;
out vec4 outputColor;
void main()
{
outputColor = color * texture(in_fontTexture, texCoord);
outputColor = color * texture(in_fontTexture, texCoord);
}";
_shader = CreateProgram("ImGui", VertexSource, FragmentSource);
_shaderProjectionMatrixLocation = GL.GetUniformLocation(_shader, "projection_matrix");
_shaderFontTextureLocation = GL.GetUniformLocation(_shader, "in_fontTexture");
int stride = Unsafe.SizeOf<ImDrawVert>();
int stride = Marshal.SizeOf<ImDrawVert>();
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, 0);
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, 8);
GL.VertexAttribPointer(2, 4, VertexAttribPointerType.UnsignedByte, true, stride, 16);
@ -222,7 +225,6 @@ outputColor = color * texture(in_fontTexture, texCoord);
ImGui.Render();
RenderImDrawData(ImGui.GetDrawData());
}
CheckGLError("End of frame");
}
/// <summary>
@ -240,6 +242,7 @@ outputColor = color * texture(in_fontTexture, texCoord);
_frameBegun = true;
ImGui.NewFrame();
ImGuizmo.BeginFrame();
}
/// <summary>
@ -249,7 +252,6 @@ outputColor = color * texture(in_fontTexture, texCoord);
private void SetPerFrameImGuiData(float deltaSeconds)
{
ImGuiIOPtr io = ImGui.GetIO();
// if (io.WantSaveIniSettings) ImGui.SaveIniSettingsToDisk(_iniPath);
io.DisplaySize = new Vector2(
_windowWidth / _scaleFactor.X,
_windowHeight / _scaleFactor.Y);
@ -262,6 +264,7 @@ outputColor = color * texture(in_fontTexture, texCoord);
private void UpdateImGuiInput(GameWindow wnd)
{
ImGuiIOPtr io = ImGui.GetIO();
var mState = wnd.MouseState;
var kState = wnd.KeyboardState;
@ -273,7 +276,7 @@ outputColor = color * texture(in_fontTexture, texCoord);
io.AddMouseButtonEvent(4, mState[MouseButton.Button2]);
io.AddMouseWheelEvent(mState.ScrollDelta.X, mState.ScrollDelta.Y);
foreach (Keys key in Enum.GetValues(typeof(Keys)))
foreach (Keys key in Enum.GetValues<Keys>())
{
if (key == Keys.Unknown) continue;
io.AddKeyEvent(TranslateKey(key), kState.IsKeyDown(key));
@ -291,7 +294,7 @@ outputColor = color * texture(in_fontTexture, texCoord);
io.KeySuper = kState.IsKeyDown(Keys.LeftSuper) || kState.IsKeyDown(Keys.RightSuper);
}
public void PressChar(char keyChar)
internal void PressChar(char keyChar)
{
PressedChars.Add(keyChar);
}
@ -337,7 +340,7 @@ outputColor = color * texture(in_fontTexture, texCoord);
{
ImDrawListPtr cmd_list = draw_data.CmdLists[i];
int vertexSize = cmd_list.VtxBuffer.Size * Unsafe.SizeOf<ImDrawVert>();
int vertexSize = cmd_list.VtxBuffer.Size * Marshal.SizeOf<ImDrawVert>();
if (vertexSize > _vertexBufferSize)
{
int newSize = (int)Math.Max(_vertexBufferSize * 1.5f, vertexSize);
@ -387,7 +390,7 @@ outputColor = color * texture(in_fontTexture, texCoord);
{
ImDrawListPtr cmd_list = draw_data.CmdLists[n];
GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, cmd_list.VtxBuffer.Size * Unsafe.SizeOf<ImDrawVert>(), cmd_list.VtxBuffer.Data);
GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, cmd_list.VtxBuffer.Size * Marshal.SizeOf<ImDrawVert>(), cmd_list.VtxBuffer.Data);
CheckGLError($"Data Vert {n}");
GL.BufferSubData(BufferTarget.ElementArrayBuffer, IntPtr.Zero, cmd_list.IdxBuffer.Size * sizeof(ushort), cmd_list.IdxBuffer.Data);
@ -541,16 +544,16 @@ outputColor = color * texture(in_fontTexture, texCoord);
public static ImGuiKey TranslateKey(Keys key)
{
if (key is >= Keys.D0 and <= Keys.D9)
if (key >= Keys.D0 && key <= Keys.D9)
return key - Keys.D0 + ImGuiKey._0;
if (key is >= Keys.A and <= Keys.Z)
if (key >= Keys.A && key <= Keys.Z)
return key - Keys.A + ImGuiKey.A;
if (key is >= Keys.KeyPad0 and <= Keys.KeyPad9)
if (key >= Keys.KeyPad0 && key <= Keys.KeyPad9)
return key - Keys.KeyPad0 + ImGuiKey.Keypad0;
if (key is >= Keys.F1 and <= Keys.F24)
if (key >= Keys.F1 && key <= Keys.F24)
return key - Keys.F1 + ImGuiKey.F24;
return key switch
@ -593,7 +596,7 @@ outputColor = color * texture(in_fontTexture, texCoord);
Keys.KeyPadAdd => ImGuiKey.KeypadAdd,
Keys.KeyPadEnter => ImGuiKey.KeypadEnter,
Keys.KeyPadEqual => ImGuiKey.KeypadEqual,
Keys.LeftShift => ImGuiKey.LeftShift,
Keys.LeftShift => ImGuiKey.ModShift,
Keys.LeftControl => ImGuiKey.LeftCtrl,
Keys.LeftAlt => ImGuiKey.LeftAlt,
Keys.LeftSuper => ImGuiKey.LeftSuper,

View File

@ -0,0 +1,63 @@
using System;
using System.Diagnostics;
using System.Reflection;
using Serilog.Core;
using Serilog.Events;
namespace FModel.Framework;
public abstract class SerilogEnricher : ILogEventEnricher
{
public abstract void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory);
protected bool TryGetCaller(out MethodBase method)
{
method = null;
var serilogAssembly = typeof(Serilog.Log).Assembly;
var stack = new StackTrace(3);
foreach (var frame in stack.GetFrames())
{
var m = frame.GetMethod();
if (m?.DeclaringType is null) continue;
if (m.DeclaringType.Assembly != serilogAssembly)
{
method = m;
break;
}
}
return method != null;
}
}
public class SourceEnricher : SerilogEnricher
{
public override void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var source = "N/A";
if (logEvent.Properties.TryGetValue("SourceContext", out var sourceContext))
{
source = sourceContext.ToString()[1..^1];
}
else if (TryGetCaller(out var method))
{
source = method.DeclaringType?.Namespace;
}
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Enriched", source.Split('.')[0]));
}
}
public class CallerEnricher : SerilogEnricher
{
public override void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
if (TryGetCaller(out var method))
{
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Enriched", $"{method.DeclaringType?.FullName}.{method.Name}"));
}
}
}

View File

@ -38,7 +38,7 @@ public static class Helper
else
{
var w = GetOpenedWindow<T>(windowName);
if (windowName == "Search View") w.WindowState = WindowState.Normal;
if (windowName == "Search For Packages") w.WindowState = WindowState.Normal;
w.Focus();
}
}

View File

@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FModel"
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls"
xmlns:inputs="clr-namespace:FModel.Views.Resources.Controls.Inputs"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:settings="clr-namespace:FModel.Settings"
xmlns:services="clr-namespace:FModel.Services"
@ -10,8 +11,18 @@
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" Closing="OnClosing" Loaded="OnLoaded" PreviewKeyDown="OnWindowKeyDown"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.85'}"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.75'}">
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.95'}"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.90'}">
<Window.TaskbarItemInfo>
<TaskbarItemInfo ProgressValue="1.0">
<TaskbarItemInfo.ProgressState>
<MultiBinding Converter="{converters:StatusToTaskbarStateConverter}">
<Binding Path="Status.Kind" />
<Binding Path="IsActive" RelativeSource="{RelativeSource AncestorType=Window}" />
</MultiBinding>
</TaskbarItemInfo.ProgressState>
</TaskbarItemInfo>
</Window.TaskbarItemInfo>
<adonisControls:AdonisWindow.Style>
<Style TargetType="adonisControls:AdonisWindow" BasedOn="{StaticResource {x:Type adonisControls:AdonisWindow}}" >
<Setter Property="Title" Value="{Binding DataContext.InitialWindowTitle, RelativeSource={RelativeSource Self}}" />
@ -30,161 +41,179 @@
</Style.Triggers>
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Views/Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="{adonisUi:Space 1}" />
<RowDefinition Height="8" />
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Menu Grid.Row="0">
<MenuItem Header="Directory">
<MenuItem Header="Selector" Command="{Binding MenuCommand}" CommandParameter="Directory_Selector">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoryIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Menu Grid.Column="0">
<MenuItem Header="Directory">
<MenuItem Header="Selector" Command="{Binding MenuCommand}" CommandParameter="Directory_Selector">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoryIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="AES" Command="{Binding MenuCommand}" CommandParameter="Directory_AES" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource KeyIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Backup" Command="{Binding MenuCommand}" CommandParameter="Directory_Backup" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource BackupIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Archives Info" Command="{Binding MenuCommand}" CommandParameter="Directory_ArchivesInfo" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource InfoIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="AES" Command="{Binding MenuCommand}" CommandParameter="Directory_AES" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource KeyIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
<MenuItem Header="Packages">
<MenuItem Header="Search" IsEnabled="{Binding Status.IsReady}" InputGestureText="Ctrl+Shift+F" Click="OnSearchViewClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="References" IsEnabled="{Binding Status.IsReady}" InputGestureText="Ctrl+Shift+R" Click="OnRefViewClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Favorite Directories" ItemsSource="{Binding CustomDirectories.Directories}" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoriesIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="Backup" Command="{Binding MenuCommand}" CommandParameter="Directory_Backup" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource BackupIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
<MenuItem Header="Views">
<MenuItem Header="3D Viewer" Command="{Binding MenuCommand}" CommandParameter="Views_3dViewer">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource MeshIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Audio Player" Command="{Binding MenuCommand}" CommandParameter="Views_AudioPlayer">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Image Merger" Command="{Binding MenuCommand}" CommandParameter="Views_ImageMerger">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource ImageMergerIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="Archives Info" Command="{Binding MenuCommand}" CommandParameter="Directory_ArchivesInfo" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource InfoIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
<MenuItem Header="Settings" Command="{Binding MenuCommand}" CommandParameter="Settings" />
<MenuItem Header="Help" >
<MenuItem Header="Donate" Command="{Binding MenuCommand}" CommandParameter="Help_Donate">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource GiftIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<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 GitHubIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Bugs Report" Command="{Binding MenuCommand}" CommandParameter="Help_BugsReport">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource BugIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Discord Server" Command="{Binding MenuCommand}" CommandParameter="Help_Discord">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource DiscordIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="About FModel" Command="{Binding MenuCommand}" CommandParameter="Help_About">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource InfoIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</MenuItem>
<MenuItem Header="Packages">
<MenuItem Header="Search" IsEnabled="{Binding Status.IsReady}" InputGestureText="Ctrl+Shift+F" Click="OnSearchViewClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Favorite Directories" ItemsSource="{Binding CustomDirectories.Directories}" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoriesIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Auto Open Sounds" IsCheckable="True" StaysOpenOnClick="True"
IsChecked="{Binding IsAutoOpenSounds, Source={x:Static settings:UserSettings.Default}}" />
</MenuItem>
<MenuItem Header="Views">
<MenuItem Header="3D Viewer" Command="{Binding MenuCommand}" CommandParameter="Views_3dViewer">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource MeshIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Audio Player" Command="{Binding MenuCommand}" CommandParameter="Views_AudioPlayer">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Image Merger" Command="{Binding MenuCommand}" CommandParameter="Views_ImageMerger">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource ImageMergerIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="Settings" Command="{Binding MenuCommand}" CommandParameter="Settings" />
<MenuItem Header="Help" >
<MenuItem Header="Donate" Command="{Binding MenuCommand}" CommandParameter="Help_Donate">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource GiftIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<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 GitHubIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Bugs Report" Command="{Binding MenuCommand}" CommandParameter="Help_BugsReport">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource BugIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Discord Server" Command="{Binding MenuCommand}" CommandParameter="Help_Discord">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource DiscordIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="About FModel" Command="{Binding MenuCommand}" CommandParameter="Help_About">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource InfoIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>
</Menu>
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Preview New Explorer System" VerticalAlignment="Center" />
<CheckBox Grid.Column="1" Margin="5 2 5 0" Unchecked="FeaturePreviewOnUnchecked" KeyboardNavigation.TabNavigation="None" KeyboardNavigation.ControlTabNavigation="None"
IsChecked="{Binding FeaturePreviewNewAssetExplorer, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}"/>
</Grid>
</Grid>
<Grid x:Name="RootGrid" Grid.Row="2">
<Grid.ColumnDefinitions>
@ -194,8 +223,8 @@
</Grid.ColumnDefinitions>
<GroupBox Grid.Column="0" adonisExtensions:LayerExtension.Layer="2"
Padding="{adonisUi:Space 0}" Background="Transparent">
<TabControl x:Name="LeftTabControl" SelectionChanged="OnTabItemChange">
Padding="0" Background="Transparent">
<TabControl x:Name="LeftTabControl" SelectionChanged="OnTabItemChange" SelectedIndex="{Binding SelectedLeftTabIndex, Mode=TwoWay}">
<TabItem Style="{StaticResource TabItemFillSpace}" Header="Archives">
<DockPanel>
<Grid DockPanel.Dock="Top">
@ -260,7 +289,8 @@
</Grid>
</DockPanel>
</TabItem>
<TabItem Style="{StaticResource TabItemFillSpace}" Header="Folders">
<TabItem Style="{StaticResource TabItemFillSpace}"
Header="Folders">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@ -318,85 +348,11 @@
</StackPanel>
</Grid>
<Separator Grid.Row="1" Style="{StaticResource CustomSeparator}" Margin="0" />
<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="Export Folder's Packages Raw Data (.uasset)" Click="OnFolderExportClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ExportIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Properties (.json)" Click="OnFolderSaveClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SaveIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Textures" Click="OnFolderTextureClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource TextureIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Models" Click="OnFolderModelClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ModelIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Animations" Click="OnFolderAnimationClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AnimationIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Favorite Directory" Click="OnFavoriteDirectoryClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoriesAddIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy Directory Path" Click="OnCopyDirectoryPathClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource CopyIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</TreeView.ContextMenu>
<TreeView Grid.Row="2"
x:Name="AssetsFolderName"
Style="{StaticResource AssetsFolderTreeView}"
PreviewKeyDown="OnFoldersPreviewKeyDown"
PreviewMouseDoubleClick="OnAssetsTreeMouseDoubleClick">
</TreeView>
<Separator Grid.Row="3" Style="{StaticResource CustomSeparator}" Tag="INFORMATION" />
<StackPanel Grid.Row="4" Orientation="Vertical" Margin="0 0 0 5">
@ -431,32 +387,11 @@
Header="{Binding SelectedItem.AssetsList.Assets.Count, FallbackValue=0, ElementName=AssetsFolderName}"
HeaderStringFormat="{}{0} Packages">
<DockPanel>
<Grid DockPanel.Dock="Top" ZIndex="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" ZIndex="1" HorizontalAlignment="Left" Margin="5 2 0 0">
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</Grid>
<TextBox Grid.Column="0" Grid.ColumnSpan="2" x:Name="AssetsSearchName" AcceptsTab="False" AcceptsReturn="False"
Padding="25 0 0 0" HorizontalAlignment="Stretch" TextChanged="OnFilterTextChanged"
adonisExtensions:WatermarkExtension.Watermark="Search by name..." />
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button ToolTip="Clear Search Filter" Padding="5" Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarButton}}" Click="OnDeleteSearchClick">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource BackspaceIcon}"/>
</Canvas>
</Viewbox>
</Button>
</StackPanel>
</Grid>
<inputs:SearchTextBox DockPanel.Dock="Top" x:Name="AssetsSearchTextBox"
Text="{Binding SelectedItem.SearchText, ElementName=AssetsFolderName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ClearButtonClick="OnClearFilterClick" />
<Grid DockPanel.Dock="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@ -465,156 +400,15 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<controls:Breadcrumb Grid.Row="0" MaxWidth="{Binding ActualWidth, ElementName=AssetsSearchName}" HorizontalAlignment="Left" Margin="0 5 0 5"
<controls:Breadcrumb Grid.Row="0" HorizontalAlignment="Left" Margin="0 5 0 5"
MaxWidth="{Binding ActualWidth, ElementName=AssetsSearchTextBox}"
DataContext="{Binding SelectedItem.PathAtThisPoint, ElementName=AssetsFolderName, FallbackValue='No/Directory/Detected/In/Folder'}"/>
<ListBox Grid.Row="1" x:Name="AssetsListName" Style="{StaticResource AssetsListBox}" PreviewMouseDoubleClick="OnAssetsListMouseDoubleClick" PreviewKeyDown="OnPreviewKeyDown">
<ListBox.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Extract in New Tab" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Extract_New_Tab" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
<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 Raw Data (.uasset)" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Export_Data" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ExportIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Properties (.json)" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Properties" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SaveIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Texture" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Textures" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource TextureIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Model" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Models" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ModelIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Animation" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Animations" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AnimationIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Copy">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource CopyIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
<MenuItem Header="Package Path" Command="{Binding DataContext.CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Path" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Name" Command="{Binding DataContext.CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Name" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Directory Path" Command="{Binding DataContext.CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Directory_Path" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Path w/o Extension" Command="{Binding DataContext.CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Path_No_Extension" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Name w/o Extension" Command="{Binding DataContext.CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Name_No_Extension" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
<ListBox Grid.Row="1" x:Name="AssetsListName"
Style="{StaticResource AssetsListBox}"
PreviewMouseDoubleClick="OnAssetsListMouseDoubleClick"
PreviewKeyDown="OnPreviewKeyDown" />
<Separator Grid.Row="2" Style="{StaticResource CustomSeparator}" Tag="INFORMATION" />
<StackPanel Grid.Row="3" Orientation="Vertical" Margin="0 0 0 5">
<Grid HorizontalAlignment="Stretch">
@ -630,15 +424,15 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding SelectedItem.Offset, ElementName=AssetsListName, FallbackValue=0, StringFormat='{}0x{0:X}'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding SelectedItem.Asset.Offset, ElementName=AssetsListName, FallbackValue=0, StringFormat='{}0x{0:X}'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="Offset" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding SelectedItem.Size, ElementName=AssetsListName, FallbackValue=0, Converter={x:Static converters:SizeToStringConverter.Instance}}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding SelectedItem.Asset.Size, ElementName=AssetsListName, FallbackValue=0, Converter={x:Static converters:SizeToStringConverter.Instance}}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="Size" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding SelectedItem.Compression, ElementName=AssetsListName, FallbackValue='Unknown'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding SelectedItem.Asset.CompressionMethod, ElementName=AssetsListName, FallbackValue='Unknown'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="2" Grid.Column="1" Text="Compression Method" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="3" Grid.Column="0" Text="{Binding SelectedItem.IsEncrypted, ElementName=AssetsListName, FallbackValue='False'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="3" Grid.Column="0" Text="{Binding SelectedItem.Asset.IsEncrypted, ElementName=AssetsListName, FallbackValue='False'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="3" Grid.Column="1" Text="Is Encrypted" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="4" Grid.Column="0" Text="{Binding SelectedItem.Archive, ElementName=AssetsListName, FallbackValue='None'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="4" Grid.Column="0" Text="{Binding SelectedItem.Asset.Vfs.Name, ElementName=AssetsListName, FallbackValue='None'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="4" Grid.Column="1" Text="Included In Archive" VerticalAlignment="Center" HorizontalAlignment="Right" />
</Grid>
</StackPanel>
@ -653,14 +447,154 @@
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer0BackgroundBrush}}" />
<GroupBox Grid.Column="2" adonisExtensions:LayerExtension.Layer="2"
Padding="{adonisUi:Space 0}" Background="Transparent">
Padding="0" Background="Transparent">
<Grid Margin="0 0 3 0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TabControl Grid.Row="0" x:Name="TabControlName" Style="{StaticResource GameFilesTabControl}" />
<Grid Grid.Row="0">
<Border BorderThickness="1" Padding="10"
BorderBrush="{DynamicResource {x:Static adonisUi:Brushes.Layer3BorderBrush}}"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
Visibility="{Binding IsAssetsExplorerVisible, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" adonisExtensions:LayerExtension.Layer="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" MinWidth="150" />
</Grid.ColumnDefinitions>
<inputs:SearchTextBox x:Name="AssetsExplorerSearch" Grid.Column="0"
Text="{Binding SelectedItem.SearchText, ElementName=AssetsFolderName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ClearButtonClick="OnClearFilterClick" />
<ComboBox x:Name="CategoriesSelector" Grid.Column="2"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedItem.SelectedCategory, ElementName=AssetsFolderName, Mode=TwoWay}" />
</Grid>
<controls:Breadcrumb Grid.Row="1" Margin="0 5 0 0"
HorizontalAlignment="Left"
DataContext="{Binding SelectedItem.PathAtThisPoint, ElementName=AssetsFolderName}" />
<ListBox x:Name="AssetsExplorer" Grid.Row="2"
ItemsSource="{Binding SelectedItem.CombinedEntries, ElementName=AssetsFolderName, IsAsync=True}"
Style="{StaticResource TiledExplorer}"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
PreviewKeyDown="OnPreviewKeyDown" />
</Grid>
</Border>
<TabControl x:Name="TabControlName"
Style="{StaticResource GameFilesTabControl}"
Visibility="{Binding IsAssetsExplorerVisible, Converter={x:Static converters:InvertBoolToVisibilityConverter.Instance}, ConverterParameter=Hidden}" />
</Grid>
<Border Grid.Row="0"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Margin="12,12,20,12"
Opacity="0.5"
Visibility="{Binding FeaturePreviewNewAssetExplorer, Source={x:Static settings:UserSettings.Default}, Converter={StaticResource BoolToVisibilityConverter}}"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer2BackgroundBrush}}"
CornerRadius="8"
Padding="4"
Effect="{DynamicResource ShadowEffect}">
<Border.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="0.5"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<ToggleButton Width="32"
Height="32"
Cursor="Hand"
Margin="0, 0, 2, 0"
Checked="OnPreviewTexturesToggled"
Focusable="False"
IsChecked="{Binding PreviewTexturesAssetExplorer, Source={x:Static settings:UserSettings.Default}, Mode=TwoWay}">
<ToggleButton.Style>
<Style TargetType="ToggleButton" BasedOn="{StaticResource ModernToggleButtonStyle}">
<Setter Property="ToolTip" Value="Preview Textures (OFF)" />
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" Value="MediumPurple" />
<Setter Property="ToolTip" Value="Preview Textures (ON)" />
</Trigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Data="{StaticResource TextureIconAlt}"
Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" />
</Canvas>
</Viewbox>
</ToggleButton>
<ToggleButton Width="32"
Height="32"
Cursor="Hand"
Margin="0, 0, 2, 0"
Checked="OnPreviewTexturesToggled"
ToolTip="Assets Explorer"
Focusable="False"
IsChecked="{Binding IsAssetsExplorerVisible, Mode=TwoWay}"
Style="{StaticResource ModernToggleButtonStyle}">
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Data="{StaticResource FolderIconAlt}"
Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" />
</Canvas>
</Viewbox>
</ToggleButton>
<ToggleButton Width="32"
Height="32"
Cursor="Hand"
ToolTip="File Properties"
Focusable="False"
IsChecked="{Binding IsAssetsExplorerVisible, Converter={x:Static converters:InvertBooleanConverter.Instance}, Mode=TwoWay}"
Style="{StaticResource ModernToggleButtonStyle}">
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Data="{StaticResource AssetIcon}"
Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" />
</Canvas>
</Viewbox>
</ToggleButton>
</StackPanel>
</Border>
<Expander Grid.Row="1" Margin="0 5 0 5" ExpandDirection="Down"
IsExpanded="{Binding IsLoggerExpanded, Source={x:Static settings:UserSettings.Default}}">
<Grid>
@ -673,7 +607,7 @@
<controls:CustomRichTextBox Grid.Column="0" Grid.ColumnSpan="3" x:Name="LogRtbName" Style="{StaticResource CustomRichTextBox}" />
<StackPanel Grid.Column="2" Orientation="Vertical" VerticalAlignment="Bottom" Margin="-5 -5 5 5">
<Button Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarButton}}" ToolTip="Open Output Folder" Padding="0,4,0,4"
Command="{Binding MenuCommand}" CommandParameter="ToolBox_Open_Output_Directory">
Command="{Binding MenuCommand}" CommandParameter="ToolBox_Open_Output_Directory" Focusable="False">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource FolderIcon}" />
@ -681,7 +615,7 @@
</Viewbox>
</Button>
<Button Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarButton}}" ToolTip="Clear Logs" Padding="0,4,0,4"
Command="{Binding MenuCommand}" CommandParameter="ToolBox_Clear_Logs">
Command="{Binding MenuCommand}" CommandParameter="ToolBox_Clear_Logs" Focusable="False">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource TrashIcon}"/>
@ -792,19 +726,6 @@
</Viewbox>
</StatusBarItem>
<StatusBarItem Width="30" HorizontalContentAlignment="Stretch" ToolTip="Auto Open Sounds Enabled">
<StatusBarItem.Style>
<Style TargetType="StatusBarItem">
<Style.Triggers>
<DataTrigger Binding="{Binding IsAutoOpenSounds, Source={x:Static settings:UserSettings.Default}}" Value="False">
<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>

View File

@ -4,9 +4,8 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using AdonisUI.Controls;
using FModel.Extensions;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels;
@ -30,14 +29,54 @@ public partial class MainWindow
{
CommandBindings.Add(new CommandBinding(new RoutedCommand("ReloadMappings", typeof(MainWindow), new InputGestureCollection { new KeyGesture(Key.F12) }), OnMappingsReload));
CommandBindings.Add(new CommandBinding(ApplicationCommands.Find, (_, _) => OnOpenAvalonFinder()));
CommandBindings.Add(new CommandBinding(NavigationCommands.BrowseBack, (_, _) =>
{
if (UserSettings.Default.FeaturePreviewNewAssetExplorer && !_applicationView.IsAssetsExplorerVisible)
{
// back browsing the json view will reopen the assets explorer
_applicationView.IsAssetsExplorerVisible = true;
return;
}
if (LeftTabControl.SelectedIndex == 2)
{
LeftTabControl.SelectedIndex = 1;
}
else if (LeftTabControl.SelectedIndex == 1 && AssetsFolderName.SelectedItem is TreeItem { Parent: TreeItem parent })
{
AssetsFolderName.Focus();
parent.IsSelected = true;
}
}));
DataContext = _applicationView;
InitializeComponent();
AssetsExplorer.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
AssetsListName.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
AssetsExplorer.SelectionChanged += (_, e) => SyncSelection(AssetsListName, e);
AssetsListName.SelectionChanged += (_, e) => SyncSelection(AssetsExplorer, e);
FLogger.Logger = LogRtbName;
YesWeCats = this;
}
// Hack to sync selection between packages tab and explorer
private void SyncSelection(ListBox target, SelectionChangedEventArgs e)
{
foreach (var added in e.AddedItems.OfType<GameFileViewModel>())
{
if (!target.SelectedItems.Contains(added))
target.SelectedItems.Add(added);
}
foreach (var removed in e.RemovedItems.OfType<GameFileViewModel>())
{
if (target.SelectedItems.Contains(removed))
target.SelectedItems.Remove(removed);
}
}
private void OnClosing(object sender, CancelEventArgs e)
{
_discordHandler.Dispose();
@ -61,18 +100,23 @@ public partial class MainWindow
break;
}
await ApplicationViewModel.InitOodle();
await ApplicationViewModel.InitZlib();
await Task.WhenAll(
ApplicationViewModel.InitOodle(),
ApplicationViewModel.InitZlib()
).ConfigureAwait(false);
await _applicationView.CUE4Parse.Initialize();
await _applicationView.AesManager.InitAes();
await _applicationView.UpdateProvider(true);
#if !DEBUG
await _applicationView.CUE4Parse.InitInformation();
#endif
await Task.WhenAll(
_applicationView.CUE4Parse.VerifyConsoleVariables(),
_applicationView.CUE4Parse.VerifyOnDemandArchives(),
_applicationView.CUE4Parse.InitMappings(),
ApplicationViewModel.InitDetex(),
ApplicationViewModel.InitVgmStream(),
ApplicationViewModel.InitImGuiSettings(newOrUpdated),
Task.Run(() =>
@ -85,10 +129,7 @@ public partial class MainWindow
#if DEBUG
// await _threadWorkerView.Begin(cancellationToken =>
// _applicationView.CUE4Parse.Extract(cancellationToken,
// "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"));
// _applicationView.CUE4Parse.Provider["Marvel/Content/Marvel/Wwise/Assets/Events/Music/music_new/event/Entry.uasset"]));
#endif
}
@ -109,10 +150,33 @@ public partial class MainWindow
}
else if (_applicationView.Status.IsReady && e.Key == Key.F && Keyboard.Modifiers.HasFlag(ModifierKeys.Control) && Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
OnSearchViewClick(null, null);
else if (e.Key == Key.Left && _applicationView.CUE4Parse.TabControl.SelectedTab.HasImage)
else if (_applicationView.Status.IsReady && e.Key == Key.R && Keyboard.Modifiers.HasFlag(ModifierKeys.Control) && Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
OnRefViewClick(null, null);
else if (e.Key == Key.F3)
OnOpenAvalonFinder();
else if (e.Key == Key.Left && !_applicationView.IsAssetsExplorerVisible && _applicationView.CUE4Parse.TabControl.SelectedTab is { HasImage: true })
_applicationView.CUE4Parse.TabControl.SelectedTab.GoPreviousImage();
else if (e.Key == Key.Right && _applicationView.CUE4Parse.TabControl.SelectedTab.HasImage)
else if (e.Key == Key.Right && !_applicationView.IsAssetsExplorerVisible && _applicationView.CUE4Parse.TabControl.SelectedTab is { HasImage: true })
_applicationView.CUE4Parse.TabControl.SelectedTab.GoNextImage();
else if (_applicationView.Status.IsReady && _applicationView.IsAssetsExplorerVisible && Keyboard.Modifiers.HasFlag(ModifierKeys.Alt))
{
CategoriesSelector.SelectedIndex = e.SystemKey switch
{
Key.D0 or Key.NumPad0 => 0,
Key.D1 or Key.NumPad1 => 1,
Key.D2 or Key.NumPad2 => 2,
Key.D3 or Key.NumPad3 => 3,
Key.D4 or Key.NumPad4 => 4,
Key.D5 or Key.NumPad5 => 5,
Key.D6 or Key.NumPad6 => 6,
Key.D7 or Key.NumPad7 => 7,
Key.D8 or Key.NumPad8 => 8,
Key.D9 or Key.NumPad9 => 9,
_ => CategoriesSelector.SelectedIndex
};
}
else if (_applicationView.Status.IsReady && UserSettings.Default.FeaturePreviewNewAssetExplorer && UserSettings.Default.SwitchAssetExplorer.IsTriggered(e.Key))
_applicationView.IsAssetsExplorerVisible = !_applicationView.IsAssetsExplorerVisible;
else if (UserSettings.Default.AssetAddTab.IsTriggered(e.Key))
_applicationView.CUE4Parse.TabControl.AddTab();
else if (UserSettings.Default.AssetRemoveTab.IsTriggered(e.Key))
@ -121,15 +185,22 @@ public partial class MainWindow
_applicationView.CUE4Parse.TabControl.GoLeftTab();
else if (UserSettings.Default.AssetRightTab.IsTriggered(e.Key))
_applicationView.CUE4Parse.TabControl.GoRightTab();
else if (UserSettings.Default.DirLeftTab.IsTriggered(e.Key) && LeftTabControl.SelectedIndex > 0)
LeftTabControl.SelectedIndex--;
else if (UserSettings.Default.DirRightTab.IsTriggered(e.Key) && LeftTabControl.SelectedIndex < LeftTabControl.Items.Count - 1)
LeftTabControl.SelectedIndex++;
else if (UserSettings.Default.DirLeftTab.IsTriggered(e.Key) && _applicationView.SelectedLeftTabIndex > 0)
_applicationView.SelectedLeftTabIndex--;
else if (UserSettings.Default.DirRightTab.IsTriggered(e.Key) && _applicationView.SelectedLeftTabIndex < LeftTabControl.Items.Count - 1)
_applicationView.SelectedLeftTabIndex++;
}
private void OnSearchViewClick(object sender, RoutedEventArgs e)
{
Helper.OpenWindow<AdonisWindow>("Search View", () => new SearchView().Show());
var searchView = Helper.GetWindow<SearchView>("Search For Packages", () => new SearchView().Show());
searchView.FocusTab(ESearchViewTab.SearchView);
}
private void OnRefViewClick(object sender, RoutedEventArgs e)
{
var searchView = Helper.GetWindow<SearchView>("Search For Packages", () => new SearchView().Show());
searchView.FocusTab(ESearchViewTab.RefView);
}
private void OnTabItemChange(object sender, SelectionChangedEventArgs e)
@ -137,7 +208,18 @@ public partial class MainWindow
if (e.OriginalSource is not TabControl tabControl)
return;
(tabControl.SelectedItem as System.Windows.Controls.TabItem)?.Focus();
switch (tabControl.SelectedIndex)
{
case 0:
DirectoryFilesListBox.Focus();
break;
case 1:
AssetsFolderName.Focus();
break;
case 2:
AssetsListName.Focus();
break;
}
}
private async void OnMappingsReload(object sender, ExecutedRoutedEventArgs e)
@ -147,119 +229,71 @@ public partial class MainWindow
private void OnOpenAvalonFinder()
{
_applicationView.CUE4Parse.TabControl.SelectedTab.HasSearchOpen = true;
AvalonEditor.YesWeSearch.Focus();
AvalonEditor.YesWeSearch.SelectAll();
if (_applicationView.IsAssetsExplorerVisible)
{
AssetsExplorerSearch.TextBox.Focus();
AssetsExplorerSearch.TextBox.SelectAll();
}
else if (_applicationView.CUE4Parse.TabControl.SelectedTab is { } tab)
{
tab.HasSearchOpen = true;
AvalonEditor.YesWeSearch.Focus();
AvalonEditor.YesWeSearch.SelectAll();
}
}
private void OnAssetsTreeMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is not TreeView { SelectedItem: TreeItem treeItem } || treeItem.Folders.Count > 0) return;
LeftTabControl.SelectedIndex++;
_applicationView.SelectedLeftTabIndex++;
}
private void OnPreviewTexturesToggled(object sender, RoutedEventArgs e) => ItemContainerGenerator_StatusChanged(AssetsExplorer.ItemContainerGenerator, EventArgs.Empty);
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (sender is not ItemContainerGenerator { Status: GeneratorStatus.ContainersGenerated } generator)
return;
var foundVisibleItem = false;
var itemCount = generator.Items.Count;
for (var i = 0; i < itemCount; i++)
{
var container = generator.ContainerFromIndex(i);
if (container == null)
{
if (foundVisibleItem) break; // we're past the visible range already
continue; // keep scrolling to find visible items
}
if (container is FrameworkElement { IsVisible: true } && generator.Items[i] is GameFileViewModel file)
{
foundVisibleItem = true;
file.OnIsVisible();
}
}
}
private async void OnAssetsListMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is not ListBox listBox) return;
var selectedItems = listBox.SelectedItems.Cast<AssetItem>().ToList();
var selectedItems = listBox.SelectedItems.OfType<GameFileViewModel>().Select(gvm => gvm.Asset).ToArray();
if (selectedItems.Length == 0) return;
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.ExtractSelected(cancellationToken, selectedItems); });
}
private async void OnFolderExtractClick(object sender, RoutedEventArgs e)
private void OnClearFilterClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.ExtractFolder(cancellationToken, folder); });
folder.SearchText = string.Empty;
folder.SelectedCategory = EAssetCategory.All;
}
}
private async void OnFolderExportClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.ExportFolder(cancellationToken, folder); });
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully exported ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.RawDataDirectory, true);
});
}
}
private async void OnFolderSaveClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.SaveFolder(cancellationToken, folder); });
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.PropertiesDirectory, true);
});
}
}
private async void OnFolderTextureClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.TextureFolder(cancellationToken, folder); });
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.TextureDirectory, true);
});
}
}
private async void OnFolderModelClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.ModelFolder(cancellationToken, folder); });
}
}
private async void OnFolderAnimationClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.AnimationFolder(cancellationToken, folder); });
}
}
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 favorite directory", Constants.WHITE, true));
}
private void OnCopyDirectoryPathClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is not TreeItem folder) return;
Clipboard.SetText(folder.PathAtThisPoint);
}
private void OnDeleteSearchClick(object sender, RoutedEventArgs e)
{
AssetsSearchName.Text = string.Empty;
AssetsListName.ScrollIntoView(AssetsListName.SelectedItem);
}
private void OnFilterTextChanged(object sender, TextChangedEventArgs e)
{
if (sender is not TextBox textBox || AssetsFolderName.SelectedItem is not TreeItem folder)
return;
var filters = textBox.Text.Trim().Split(' ');
folder.AssetsList.AssetsView.Filter = o => { return o is AssetItem assetItem && filters.All(x => assetItem.FullPath.SubstringAfterLast('/').Contains(x, StringComparison.OrdinalIgnoreCase)); };
}
private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (!_applicationView.Status.IsReady || sender is not ListBox listBox) return;
@ -269,14 +303,67 @@ public partial class MainWindow
private async void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (!_applicationView.Status.IsReady || sender is not ListBox listBox) return;
if (!_applicationView.Status.IsReady || sender is not ListBox listBox)
return;
if (e.Key != Key.Enter)
return;
if (listBox.SelectedItem == null)
return;
switch (e.Key)
switch (listBox.SelectedItem)
{
case Key.Enter:
var selectedItems = listBox.SelectedItems.Cast<AssetItem>().ToList();
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.ExtractSelected(cancellationToken, selectedItems); });
case GameFileViewModel file:
_applicationView.IsAssetsExplorerVisible = false;
ApplicationService.ApplicationView.SelectedLeftTabIndex = 2;
await _threadWorkerView.Begin(cancellationToken => _applicationView.CUE4Parse.ExtractSelected(cancellationToken, [file.Asset]));
break;
case TreeItem folder:
ApplicationService.ApplicationView.SelectedLeftTabIndex = 1;
var parent = folder.Parent;
while (parent != null)
{
parent.IsExpanded = true;
parent = parent.Parent;
}
var childFolder = folder;
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Assets.Count == 0)
{
childFolder.IsExpanded = true;
childFolder = childFolder.Folders[0];
}
childFolder.IsExpanded = true;
childFolder.IsSelected = true;
break;
}
}
private void FeaturePreviewOnUnchecked(object sender, RoutedEventArgs e)
{
_applicationView.IsAssetsExplorerVisible = false;
}
private async void OnFoldersPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter || sender is not TreeView treeView || treeView.SelectedItem is not TreeItem folder)
return;
if ((folder.IsExpanded || folder.Folders.Count == 0) && folder.AssetsList.Assets.Count > 0)
{
_applicationView.SelectedLeftTabIndex++;
return;
}
var childFolder = folder;
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Assets.Count == 0)
{
childFolder.IsExpanded = true;
childFolder = childFolder.Folders[0];
}
childFolder.IsExpanded = true;
childFolder.IsSelected = true;
}
}

View File

@ -1,195 +1,150 @@
<?xml version="1.0"?>
<!-- syntaxdefinition for C/C++ 2001 by Andrea Paatz and Mike Krueger -->
<!-- converted to AvalonEdit format by Siegfried Pammer in 2010 -->
<SyntaxDefinition name="C++" extensions=".c;.h;.cc;.cpp;.hpp" xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color name="Comment" foreground="Green" />
<Color name="Character" foreground="Fuchsia" />
<Color name="String" foreground="Fuchsia" />
<Color name="Preprocessor" foreground="Green" />
<Color name="Punctuation" foreground="DarkGreen" />
<Color name="MethodName" foreground="MidnightBlue" fontWeight="bold" />
<Color name="Digits" foreground="#F78C6C" />
<Color name="CompoundKeywords" foreground="Black" fontWeight="bold" />
<Color name="This" foreground="Black" fontWeight="bold" />
<Color name="Operators" foreground="#FF008B8B" fontWeight="bold" />
<Color name="Namespace" foreground="#FF008000" fontWeight="bold" />
<Color name="Friend" foreground="#FFA52A2A" />
<Color name="Modifiers" foreground="#FF0000FF" fontWeight="bold" />
<Color name="TypeKeywords" foreground="#FFFF0000" />
<Color name="BooleanConstants" foreground="#FF000000" fontWeight="bold" />
<Color name="Keywords" foreground="#FF0000FF" fontWeight="bold" />
<Color name="LoopKeywords" foreground="#FF0000FF" fontWeight="bold" />
<Color name="JumpKeywords" foreground="#FF000080" />
<Color name="ExceptionHandling" foreground="#FF008080" fontWeight="bold" />
<Color name="ControlFlow" foreground="#FF0000FF" fontWeight="bold" />
<RuleSet ignoreCase="false">
<Rule color="Punctuation">
[?,.;()\[\]{}+\-/%*&lt;&gt;^=~!&amp;]+
</Rule>
<Keywords color="CompoundKeywords">
<Word>__abstract</Word>
<Word>__box</Word>
<Word>__delegate</Word>
<Word>__gc</Word>
<Word>__identifier</Word>
<Word>__nogc</Word>
<Word>__pin</Word>
<Word>__property</Word>
<Word>__sealed</Word>
<Word>__try_cast</Word>
<Word>__typeof</Word>
<Word>__value</Word>
<Word>__event</Word>
<Word>__hook</Word>
<Word>__raise</Word>
<Word>__unhook</Word>
<Word>__interface</Word>
<Word>ref class</Word>
<Word>ref struct</Word>
<Word>value class</Word>
<Word>value struct</Word>
<Word>interface class</Word>
<Word>interface struct</Word>
<Word>enum class</Word>
<Word>enum struct</Word>
<Word>delegate</Word>
<Word>event</Word>
<Word>property</Word>
<Word>abstract</Word>
<Word>override</Word>
<Word>sealed</Word>
<Word>generic</Word>
<Word>where</Word>
<Word>finally</Word>
<Word>for each</Word>
<Word>gcnew</Word>
<Word>in</Word>
<Word>initonly</Word>
<Word>literal</Word>
<Word>nullptr</Word>
</Keywords>
<Keywords color="This">
<Word>this</Word>
</Keywords>
<Keywords color="Operators">
<Word>and</Word>
<Word>and_eq</Word>
<Word>bitand</Word>
<Word>bitor</Word>
<Word>new</Word>
<Word>not</Word>
<Word>not_eq</Word>
<Word>or</Word>
<Word>or_eq</Word>
<Word>xor</Word>
<Word>xor_eq</Word>
</Keywords>
<Keywords color="Namespace">
<Word>using</Word>
<Word>namespace</Word>
</Keywords>
<Keywords color="Friend">
<Word>friend</Word>
</Keywords>
<Keywords color="Modifiers">
<Word>private</Word>
<Word>protected</Word>
<Word>public</Word>
<Word>const</Word>
<Word>volatile</Word>
<Word>static</Word>
</Keywords>
<Keywords color="TypeKeywords">
<Word>bool</Word>
<Word>char</Word>
<Word>unsigned</Word>
<Word>union</Word>
<Word>virtual</Word>
<Word>double</Word>
<Word>float</Word>
<Word>short</Word>
<Word>signed</Word>
<Word>void</Word>
<Word>class</Word>
<Word>enum</Word>
<Word>struct</Word>
</Keywords>
<Keywords color="BooleanConstants">
<Word>false</Word>
<Word>true</Word>
</Keywords>
<Keywords color="LoopKeywords">
<Word>do</Word>
<Word>for</Word>
<Word>while</Word>
</Keywords>
<Keywords color="JumpKeywords">
<Word>break</Word>
<Word>continue</Word>
<Word>goto</Word>
<Word>return</Word>
</Keywords>
<Keywords color="ExceptionHandling">
<Word>catch</Word>
<Word>throw</Word>
<Word>try</Word>
</Keywords>
<Keywords color="ControlFlow">
<Word>case</Word>
<Word>else</Word>
<Word>if</Word>
<Word>switch</Word>
<Word>default</Word>
</Keywords>
<Keywords color="Keywords">
<Word>asm</Word>
<Word>auto</Word>
<Word>compl</Word>
<Word>mutable</Word>
<Word>const_cast</Word>
<Word>delete</Word>
<Word>dynamic_cast</Word>
<Word>explicit</Word>
<Word>export</Word>
<Word>extern</Word>
<Word>inline</Word>
<Word>int</Word>
<Word>long</Word>
<Word>operator</Word>
<Word>register</Word>
<Word>reinterpret_cast</Word>
<Word>sizeof</Word>
<Word>static_cast</Word>
<Word>template</Word>
<Word>typedef</Word>
<Word>typeid</Word>
<Word>typename</Word>
</Keywords>
<Span color="Preprocessor">
<Begin>\#</Begin>
</Span>
<Span color="Comment">
<Begin>//</Begin>
</Span>
<Span color="Comment" multiline="true">
<Begin>/\*</Begin>
<End>\*/</End>
</Span>
<Span color="String">
<Begin>"</Begin>
<End>"</End>
<RuleSet>
<Span begin="\\" end="." />
</RuleSet>
</Span>
<Span color="Character">
<Begin>'</Begin>
<End>'</End>
<RuleSet>
<Span begin="\\" end="." />
</RuleSet>
</Span>
<Rule color="MethodName">[\d\w_]+(?=(\s*\())</Rule>
<Rule color="Digits">\b0[xX][0-9a-fA-F]+|(\b\d+(\.[0-9]+)?|\.[0-9]+)([eE][+-]?[0-9]+)?</Rule>
</RuleSet>
</SyntaxDefinition>
<SyntaxDefinition name="C++" extensions=".cpp;.h;.hpp;.c" xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color name="Comment" foreground="#84c36b" />
<Color name="Keyword" foreground="#82AAFF" fontWeight="bold" />
<Color name="Type" foreground="#4cc9b0" />
<Color name="String" foreground="#ECC48D" />
<Color name="Preprocessor" foreground="#82AAFF" fontWeight="bold" />
<Color name="Number" foreground="#F78C6C" />
<Color name="Function" foreground="#C3E88D" />
<Color name="AccessModifier" foreground="#f20f5c" fontWeight="bold" />
<Color name="UEMacro" foreground="#82AAFF" fontWeight="bold" />
<Color name="LabelColor" foreground="#808080" />
<Color name="JumpKeywords" foreground="#dda0dd" />
<Color name="CompoundKeywords" foreground="#FF569CD6" fontWeight="bold" />
<Color name="Pointer" foreground="#ff8888" fontWeight="bold"/>
<Color name="StaticClass" foreground="#c2a8ff" />
<Color name="Brace" foreground="#89DDFF" />
<Color name="This" foreground="#FF569CD6" fontWeight="bold" />
<Color name="BooleanConstants" foreground="#569cd6" fontWeight="bold" />
<RuleSet ignoreCase="false">
<Span color="String" begin="&quot;" end="&quot;" />
<!-- UE Macros -->
<Keywords color="UEMacro">
<Word>UCLASS</Word>
<Word>USTRUCT</Word>
<Word>UPROPERTY</Word>
<Word>UFUNCTION</Word>
<Word>GENERATED_BODY</Word>
<Word>GENERATED_USTRUCT_BODY</Word>
<Word>GENERATED_UCLASS_BODY</Word>
</Keywords>
<Keywords color="JumpKeywords">
<Word>goto</Word>
<Word>return</Word>
<Word>throw</Word>
</Keywords>
<!-- C++ Keywords -->
<Keywords color="Keyword">
<Word>void</Word>
<Word>int</Word>
<Word>Int8</Word>
<Word>Int16</Word>
<Word>Int32</Word>
<Word>Int64</Word>
<Word>uint</Word>
<Word>UInt16</Word>
<Word>UInt32</Word>
<Word>UInt64</Word>
<Word>float</Word>
<Word>double</Word>
<Word>bool</Word>
<Word>return</Word>
<Word>if</Word>
<Word>else</Word>
<Word>for</Word>
<Word>while</Word>
<Word>do</Word>
<Word>switch</Word>
<Word>case</Word>
<Word>break</Word>
<Word>continue</Word>
<Word>namespace</Word>
<Word>using</Word>
<Word>typedef</Word>
<Word>sizeof</Word>
<Word>new</Word>
<Word>delete</Word>
<Word>class</Word>
<Word>struct</Word>
<Word>enum</Word>
<Word>template</Word>
<Word>typename</Word>
<Word>const</Word>
<Word>static</Word>
<Word>mutable</Word>
<Word>volatile</Word>
<Word>override</Word>
<Word>virtual</Word>
<Word>explicit</Word>
<Word>friend</Word>
<Word>inline</Word>
<Word>constexpr</Word>
<Word>default</Word>
</Keywords>
<Keywords color="Pointer">
<Word>nullptr</Word>
</Keywords>
<Keywords color="BooleanConstants">
<Word>true</Word>
<Word>True</Word>
<Word>false</Word>
<Word>False</Word>
<Word>NULL</Word>
</Keywords>
<Keywords color="AccessModifier">
<Word>public</Word>
<Word>protected</Word>
<Word>private</Word>
</Keywords>
<Keywords color="This">
<Word>this</Word>
</Keywords>
<!-- Reference symbols -->
<Rule color="Pointer">(?&lt;=[A-Za-z0-9_&gt;&amp;\]])&amp;(?=\s*[A-Za-z_&lt;])</Rule>
<Rule color="LabelColor">\bLabel_\d+:</Rule>
<!-- Numbers (hex too) -->
<Rule color="Number">\b(0x[0-9a-fA-F]+|[0-9]+(\.[0-9]+)?)\b</Rule>
<Rule color="StaticClass">\bU[A-Z][A-Za-z0-9_]*\b(?=::)</Rule>
<Rule color="Function">[A-Za-z_][A-Za-z0-9_]*\s*(?=\()</Rule>
<Rule color="Brace">[\[\]\{\}]</Rule>
<Rule color="Comment">(\/\/.*|\/\*[\s\S]*?\*\/)</Rule>
<!-- Template Functions -->
<Rule color="Function">\b[A-Za-z_][A-Za-z0-9_]*\b(?=&lt;)</Rule>
<!-- Types -->
<Rule color="Type">\b[A-Z][A-Za-z0-9_]*(?:&lt;[^&gt;]+&gt;)?[*&amp;]?(?=\s+[*&amp;]?[A-Za-z_][A-Za-z0-9_]*\s*(=|;|\)|,))</Rule>
<!-- Types inside <> -->
<Rule color="Type">(?&lt;=&lt;)\s*[A-Z][A-Za-z0-9_]*(?:&lt;[^&gt;]+&gt;)?[*&amp;]?\s*(?=[&gt;,])</Rule>
<!-- Match class name after the 'class' keyword -->
<Rule color="Type">\b(?&lt;=class\s)[A-Za-z_][A-Za-z0-9_]*</Rule>
<!-- Match name after 'public' keyword -->
<Rule color="Type">\b(?&lt;=public\s)[A-Za-z_][A-Za-z0-9_]*</Rule>
<!-- Types in function parameters -->
<Rule color="Type">\b(?:T|F|U|E)[A-Z][A-Za-z0-9_]*(?:&lt;[^&gt;]+&gt;)?[*&amp;]?(?=\s+[*&amp;]?[A-Za-z_][A-Za-z0-9_]*\s*(?:=|,|\)))</Rule>
<Rule color="Type">\b(?&lt;=[,(]\s*const\s)(?:T|F|U)[A-Z][A-Za-z0-9_]*[*&amp;]?(?=\s)</Rule>
<!-- First parameter type in function -->
<Rule color="Type">\b(?&lt;=\()\s*[TUF][A-Z][A-Za-z0-9_]*(?=\s*&lt;)</Rule>
<Rule color="Type">\b(?&lt;=\()\s*[TUF][A-Z][A-Za-z0-9_]*[*&amp;]?(?=\s)</Rule>
<Rule color="Type">\b(?&lt;=\(\s*const\s)[TUF][A-Z][A-Za-z0-9_]*[*&amp;]?(?=\s)</Rule>
</RuleSet>
</SyntaxDefinition>

View File

@ -12,7 +12,7 @@ out vec3 fColor;
void main()
{
gl_PointSize = 7.5f;
gl_PointSize = 7.5;
gl_Position = uProjection * uView * uInstanceMatrix * vec4(vPos, 1.0);
fPos = vec3(uInstanceMatrix * vec4(vPos, 1.0));
fColor = vColor;

View File

@ -13,7 +13,7 @@ out vec3 fColor;
void main()
{
gl_PointSize = 7.5f;
gl_PointSize = 7.5;
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);

View File

@ -81,6 +81,18 @@ void main()
finalNormal = normalize(finalNormal);
finalTangent = normalize(finalTangent);
}
else if (uIsSpline)
{
GpuSplineMeshParams params = uSplineParameters[gl_InstanceID];
float distanceAlong = GetAxisValueRef(params.ForwardAxis, bindPos.xzy);
vec3 computed = ComputeRatioAlongSpline(params, distanceAlong);
mat4 sliceTransform = CalcSliceTransformAtSplineOffset(params, computed);
SetAxisValueRef(params.ForwardAxis, bindPos.xzy, 0.0);
finalPos = (sliceTransform * bindPos.xzyw).xzyw;
finalNormal = bindNormal;
finalTangent = bindTangent;
}
else
{
finalPos = bindPos;

View File

@ -32,5 +32,5 @@ void main()
outVar.view = view;
outVar.nearPoint = UnprojectPoint(vPos.xy, -1.0).xyz;
outVar.farPoint = UnprojectPoint(vPos.xy, 1.0).xyz;
gl_Position = vec4(vPos, 1.0f);
gl_Position = vec4(vPos, 1.0);
}

View File

@ -62,6 +62,17 @@ void main()
}
}
}
else if (uIsSpline)
{
GpuSplineMeshParams params = uSplineParameters[gl_InstanceID];
float distanceAlong = GetAxisValueRef(params.ForwardAxis, bindPos.xzy);
vec3 computed = ComputeRatioAlongSpline(params, distanceAlong);
mat4 sliceTransform = CalcSliceTransformAtSplineOffset(params, computed);
SetAxisValueRef(params.ForwardAxis, bindPos.xzy, 0.0);
finalPos = (sliceTransform * bindPos.xzyw).xzyw;
finalNormal = bindNormal;
}
else
{
finalPos = bindPos;

View File

@ -47,6 +47,16 @@ void main()
}
}
}
else if (uIsSpline)
{
GpuSplineMeshParams params = uSplineParameters[gl_InstanceID];
float distanceAlong = GetAxisValueRef(params.ForwardAxis, bindPos.xzy);
vec3 computed = ComputeRatioAlongSpline(params, distanceAlong);
mat4 sliceTransform = CalcSliceTransformAtSplineOffset(params, computed);
SetAxisValueRef(params.ForwardAxis, bindPos.xzy, 0.0);
finalPos = (sliceTransform * bindPos.xzyw).xzyw;
}
else finalPos = bindPos;
gl_Position = uProjection * uView * vInstanceMatrix * finalPos;

View File

@ -0,0 +1,216 @@
#version 460 core
// yeeted from minshu https://github.com/FabianFG/CUE4Parse/commit/61cef25b8eef4160651ee41e2b1ceefc5135803f
struct GpuSplineMeshParams {
int ForwardAxis;
float SplineBoundaryMin;
float SplineBoundaryMax;
bool bSmoothInterpRollScale;
vec3 MeshOrigin;
vec3 MeshBoxExtent;
vec3 StartPos;
float StartRoll;
vec3 StartTangent;
vec2 StartScale;
vec2 StartOffset;
vec3 EndPos;
float EndRoll;
vec3 EndTangent;
vec2 EndScale;
vec2 EndOffset;
vec3 SplineUpDir;
};
layout(std430, binding = 3) buffer SplineParameters
{
GpuSplineMeshParams uSplineParameters[];
};
uniform bool uIsSpline;
vec3 getSafeNormal(vec3 vector) {
float squareSum = dot(vector, vector);
if (squareSum == 1.0) {
return vector;
}
if (squareSum < 1e-8) {
return vec3(0.0); // Return a zero vector
}
// Calculate the scale factor to normalize the vector
float scale = inversesqrt(squareSum);
return vector * scale;
}
float GetAxisValueRef(int forwardAxis, vec3 pos)
{
switch (forwardAxis)
{
case 0: return pos.x;
case 1: return pos.y;
case 2: return pos.z;
default: return 0;
}
}
void SetAxisValueRef(int forwardAxis, inout vec3 pos, float v)
{
switch (forwardAxis)
{
case 0: pos.x = v; break;
case 1: pos.y = v; break;
case 2: pos.z = v; break;
}
}
vec3 SplineEvalTangent(GpuSplineMeshParams params, float a)
{
vec3 c = (6 * params.StartPos) + (3 * params.StartTangent) + (3 * params.EndTangent) - (6 * params.EndPos);
vec3 d = (-6 * params.StartPos) - (4 * params.StartTangent) - (2 * params.EndTangent) + (6 * params.EndPos);
vec3 e = params.StartTangent;
float a2 = a * a;
return (c * a2) + (d * a) + e;
}
vec3 SplineEvalDir(GpuSplineMeshParams params, float a)
{
return getSafeNormal(SplineEvalTangent(params, a));
}
vec3 SplineEvalPos(GpuSplineMeshParams params, float a)
{
float a2 = a * a;
float a3 = a2 * a;
return (((2 * a3) - (3 * a2) + 1) * params.StartPos) + ((a3 - (2 * a2) + a) * params.StartTangent) + ((a3 - a2) * params.EndTangent) + (((-2 * a3) + (3 * a2)) * params.EndPos);
}
vec3 ComputeRatioAlongSpline(GpuSplineMeshParams params, float distanceAlong)
{
float alpha = 0.0;
float minT = 0.0;
float maxT = 1.0;
const float SmallNumber = 1e-8;
bool bHasCustomBoundary = abs(params.SplineBoundaryMin - params.SplineBoundaryMax) > SmallNumber;
if (bHasCustomBoundary)
{
float splineLength = params.SplineBoundaryMax - params.SplineBoundaryMin;
if (splineLength > 0)
{
alpha = (distanceAlong - params.SplineBoundaryMin) / splineLength;
}
float boundMin = GetAxisValueRef(params.ForwardAxis, params.MeshOrigin - params.MeshBoxExtent);
float boundMax = GetAxisValueRef(params.ForwardAxis, params.MeshOrigin + params.MeshBoxExtent);
float boundMinT = (boundMin - params.SplineBoundaryMin) / (params.SplineBoundaryMax - params.SplineBoundaryMin);
float boundMaxT = (boundMax - params.SplineBoundaryMin) / (params.SplineBoundaryMax - params.SplineBoundaryMin);
const float MaxSplineExtrapolation = 4.0;
minT = max(-MaxSplineExtrapolation, boundMinT);
maxT = min(boundMaxT, MaxSplineExtrapolation);
}
else
{
float meshMinZ = GetAxisValueRef(params.ForwardAxis, params.MeshOrigin) - GetAxisValueRef(params.ForwardAxis, params.MeshBoxExtent);
float meshRangeZ = 2 * GetAxisValueRef(params.ForwardAxis, params.MeshBoxExtent);
if (meshRangeZ > SmallNumber) {
alpha = (distanceAlong - meshMinZ) / meshRangeZ;
}
}
return vec3(alpha, minT, maxT);
}
mat4 CalcSliceTransformAtSplineOffset(GpuSplineMeshParams params, vec3 computed)
{
float alpha = computed.x;
float minT = computed.y;
float maxT = computed.z;
float hermiteAlpha = params.bSmoothInterpRollScale ? smoothstep(0.0, 1.0, alpha) : alpha;
vec3 splinePos;
vec3 splineDir;
if (alpha < minT)
{
vec3 startTangent = SplineEvalTangent(params, minT);
splinePos = SplineEvalPos(params, minT) + (startTangent * (alpha - minT));
splineDir = getSafeNormal(startTangent);
}
else if (alpha > maxT)
{
vec3 endTangent = SplineEvalTangent(params, maxT);
splinePos = SplineEvalPos(params, maxT) + (endTangent * (alpha - maxT));
splineDir = getSafeNormal(endTangent);
}
else
{
splinePos = SplineEvalPos(params, alpha);
splineDir = SplineEvalDir(params, alpha);
}
// base
vec3 baseXVec = getSafeNormal(cross(params.SplineUpDir, splineDir));
vec3 baseYVec = getSafeNormal(cross(splineDir, baseXVec));
// Offset the spline by the desired amount
vec2 sliceOffset = mix(params.StartOffset, params.EndOffset, hermiteAlpha);
splinePos += sliceOffset.x * baseXVec;
splinePos += sliceOffset.y * baseYVec;
// Apply Roll
float useRoll = mix(params.StartRoll, params.EndRoll, hermiteAlpha);
float cosAng = cos(useRoll);
float sinAng = sin(useRoll);
vec3 xVec = (cosAng * baseXVec) - (sinAng * baseYVec);
vec3 yVec = (cosAng * baseYVec) + (sinAng * baseXVec);
// Find Scale
vec2 useScale = mix(params.StartScale, params.EndScale, hermiteAlpha);
// Build overall transform
mat4 sliceTransform = mat4(0);
vec3 scale;
switch (params.ForwardAxis) {
case 0:
sliceTransform[0] = vec4(splineDir, 0.0);
sliceTransform[1] = vec4(xVec, 0.0);
sliceTransform[2] = vec4(yVec, 0.0);
sliceTransform[3] = vec4(splinePos, 1.0);
scale = vec3(1.0, useScale.x, useScale.y);
break;
case 1:
sliceTransform[0] = vec4(yVec, 0.0);
sliceTransform[1] = vec4(splineDir, 0.0);
sliceTransform[2] = vec4(xVec, 0.0);
sliceTransform[3] = vec4(splinePos, 1.0);
scale = vec3(useScale.y, 1.0, useScale.x);
break;
case 2:
sliceTransform[0] = vec4(xVec, 0.0);
sliceTransform[1] = vec4(yVec, 0.0);
sliceTransform[2] = vec4(splineDir, 0.0);
sliceTransform[3] = vec4(splinePos, 1.0);
scale = vec3(useScale.x, useScale.y, 1.0);
break;
}
mat4 scaleMatrix = mat4(
vec4(scale.x, 0.0, 0.0, 0.0),
vec4(0.0, scale.y, 0.0, 0.0),
vec4(0.0, 0.0, scale.z, 0.0),
vec4(0.0, 0.0, 0.0, 1.0)
);
return sliceTransform * scaleMatrix;
}

View File

@ -22,7 +22,7 @@ namespace FModel.Services
private readonly Assets _staticAssets = new()
{
LargeImageKey = "official_logo", SmallImageKey = "verified", SmallImageText = $"v{Constants.APP_VERSION}"
LargeImageKey = "official_logo", SmallImageKey = "verified", SmallImageText = $"v{Constants.APP_VERSION} ({Constants.APP_SHORT_COMMIT_ID})"
};
private readonly Button[] _buttons =
@ -48,7 +48,7 @@ namespace FModel.Services
public void UpdatePresence(CUE4ParseViewModel viewModel) =>
UpdatePresence(
$"{viewModel.Provider.GameDisplayName ?? viewModel.Provider.InternalGameName} - {viewModel.Provider.MountedVfs.Count}/{viewModel.Provider.MountedVfs.Count + viewModel.Provider.UnloadedVfs.Count} Packages",
$"{viewModel.Provider.GameDisplayName ?? viewModel.Provider.ProjectName} - {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

@ -30,6 +30,21 @@ public class CustomDirectory : ViewModel
new("Shop Backgrounds", "ShooterGame/Content/UI/OutOfGame/MainMenu/Store/Shared/Textures/"),
new("Weapon Renders", "ShooterGame/Content/UI/Screens/OutOfGame/MainMenu/Collection/Assets/Large/")
};
case "Dead by Daylight":
return new List<CustomDirectory>
{
new("Characters V1", "DeadByDaylight/Plugins/DBDCharacters/"),
new("Characters V2", "DeadByDaylight/Plugins/Runtime/Bhvr/DBDCharacters/"),
new("Characters (Deprecated)", "DeadbyDaylight/Content/Characters/"),
new("Meshes", "DeadByDaylight/Content/Meshes/"),
new("Textures", "DeadByDaylight/Content/Textures/"),
new("Icons", "DeadByDaylight/Content/UI/UMGAssets/Icons/"),
new("Blueprints", "DeadByDaylight/Content/Blueprints/"),
new("Audio Events", "DeadByDaylight/Content/Audio/Events/"),
new("Audio", "DeadByDaylight/Content/WwiseAudio/Cooked/"),
new("Data Tables", "DeadByDaylight/Content/Data/"),
new("Localization", "DeadByDaylight/Content/Localization/")
};
default:
return new List<CustomDirectory>();
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Versions;
@ -24,7 +24,8 @@ public class DirectorySettings : ViewModel, ICloneable
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)
LastAesReload = old?.LastAesReload ?? DateTime.Today.AddDays(-1),
CriwareDecryptionKey = old?.CriwareDecryptionKey ?? 0
};
}
@ -98,6 +99,13 @@ public class DirectorySettings : ViewModel, ICloneable
set => SetProperty(ref _lastAesReload, value);
}
private ulong _criwareDecryptionKey;
public ulong CriwareDecryptionKey
{
get => _criwareDecryptionKey;
set => SetProperty(ref _criwareDecryptionKey, value);
}
private bool Equals(DirectorySettings other)
{
return GameDirectory == other.GameDirectory && UeVersion == other.UeVersion;

View File

@ -16,8 +16,8 @@ public class EndpointSettings : ViewModel
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)
new("https://uedb.dev/svc/api/v1/fortnite/aes", "$.['mainKey','dynamicKeys']"),
new("https://uedb.dev/svc/api/v1/fortnite/mappings", "$.mappings.ZStandard")
};
default:
return new EndpointSettings[] { new(), new() };

View File

@ -3,13 +3,14 @@ using System.Collections.Generic;
using System.IO;
using System.Windows;
using System.Windows.Input;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Nanite;
using CUE4Parse.UE4.Versions;
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;
using FModel.ViewModels.ApiEndpoints.Models;
@ -60,6 +61,7 @@ namespace FModel.Settings
{
LodFormat = Default.LodExportFormat,
MeshFormat = Default.MeshExportFormat,
NaniteMeshFormat = Default.NaniteMeshExportFormat,
AnimFormat = Default.MeshExportFormat switch
{
EMeshFormat.UEFormat => EAnimFormat.UEFormat,
@ -71,7 +73,8 @@ namespace FModel.Settings
CompressionFormat = Default.CompressionFormat,
Platform = Default.CurrentDir.TexturePlatform,
ExportMorphTargets = Default.SaveMorphTargets,
ExportMaterials = Default.SaveEmbeddedMaterials
ExportMaterials = Default.SaveEmbeddedMaterials,
ExportHdrTexturesAsHdr = Default.SaveHdrTexturesAsHdr
};
private bool _showChangelog = true;
@ -137,13 +140,6 @@ namespace FModel.Settings
set => SetProperty(ref _lastOpenedSettingTab, value);
}
private bool _isAutoOpenSounds = true;
public bool IsAutoOpenSounds
{
get => _isAutoOpenSounds;
set => SetProperty(ref _isAutoOpenSounds, value);
}
private bool _isLoggerExpanded = true;
public bool IsLoggerExpanded
{
@ -200,6 +196,13 @@ namespace FModel.Settings
set => SetProperty(ref _keepDirectoryStructure, value);
}
private bool _showDecompileOption = false;
public bool ShowDecompileOption
{
get => _showDecompileOption;
set => SetProperty(ref _showDecompileOption, value);
}
private ECompressedAudio _compressedAudioMode = ECompressedAudio.PlayDecompressed;
public ECompressedAudio CompressedAudioMode
{
@ -256,6 +259,13 @@ namespace FModel.Settings
set => SetProperty(ref _readScriptData, value);
}
private bool _readShaderMaps;
public bool ReadShaderMaps
{
get => _readShaderMaps;
set => SetProperty(ref _readShaderMaps, value);
}
private IDictionary<string, DirectorySettings> _perDirectory = new Dictionary<string, DirectorySettings>();
public IDictionary<string, DirectorySettings> PerDirectory
{
@ -297,6 +307,13 @@ namespace FModel.Settings
set => SetProperty(ref _dirRightTab, value);
}
private Hotkey _switchAssetExplorer = new(Key.Z);
public Hotkey SwitchAssetExplorer
{
get => _switchAssetExplorer;
set => SetProperty(ref _switchAssetExplorer, value);
}
private Hotkey _assetLeftTab = new(Key.Q);
public Hotkey AssetLeftTab
{
@ -353,13 +370,20 @@ namespace FModel.Settings
set => SetProperty(ref _nextAudio, value);
}
private EMeshFormat _meshExportFormat = EMeshFormat.ActorX;
private EMeshFormat _meshExportFormat = EMeshFormat.UEFormat;
public EMeshFormat MeshExportFormat
{
get => _meshExportFormat;
set => SetProperty(ref _meshExportFormat, value);
}
private ENaniteMeshFormat _naniteMeshExportFormat = ENaniteMeshFormat.OnlyNaniteLOD;
public ENaniteMeshFormat NaniteMeshExportFormat
{
get => _naniteMeshExportFormat;
set => SetProperty(ref _naniteMeshExportFormat, value);
}
private EMaterialFormat _materialExportFormat = EMaterialFormat.FirstLayer;
public EMaterialFormat MaterialExportFormat
{
@ -423,6 +447,13 @@ namespace FModel.Settings
set => SetProperty(ref _cameraMode, value);
}
private int _wwiseMaxBnkPrefetch;
public int WwiseMaxBnkPrefetch
{
get => _wwiseMaxBnkPrefetch;
set => SetProperty(ref _wwiseMaxBnkPrefetch, value);
}
private int _previewMaxTextureSize = 1024;
public int PreviewMaxTextureSize
{
@ -444,6 +475,13 @@ namespace FModel.Settings
set => SetProperty(ref _previewSkeletalMeshes, value);
}
private bool _previewAnimations = true;
public bool PreviewAnimations
{
get => _previewAnimations;
set => SetProperty(ref _previewAnimations, value);
}
private bool _previewMaterials = true;
public bool PreviewMaterials
{
@ -478,5 +516,26 @@ namespace FModel.Settings
get => _saveSkeletonAsMesh;
set => SetProperty(ref _saveSkeletonAsMesh, value);
}
private bool _saveHdrTexturesAsHdr = true;
public bool SaveHdrTexturesAsHdr
{
get => _saveHdrTexturesAsHdr;
set => SetProperty(ref _saveHdrTexturesAsHdr, value);
}
private bool _featurePreviewNewAssetExplorer = true;
public bool FeaturePreviewNewAssetExplorer
{
get => _featurePreviewNewAssetExplorer;
set => SetProperty(ref _featurePreviewNewAssetExplorer, value);
}
private bool _previewTexturesAssetExplorer = true;
public bool PreviewTexturesAssetExplorer
{
get => _previewTexturesAssetExplorer;
set => SetProperty(ref _previewTexturesAssetExplorer, value);
}
}
}

View File

@ -1,13 +1,19 @@
using RestSharp;
using RestSharp.Interceptors;
namespace FModel.ViewModels.ApiEndpoints;
public abstract class AbstractApiProvider
{
protected readonly RestClient _client;
protected readonly Interceptor _interceptor;
protected AbstractApiProvider(RestClient client)
{
_client = client;
_interceptor = new CompatibilityInterceptor
{
OnBeforeDeserialization = resp => { resp.ContentType = "application/json; charset=utf-8"; }
};
}
}

View File

@ -1,7 +1,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FModel.Extensions;
using CUE4Parse.Utils;
using FModel.Framework;
using FModel.ViewModels.ApiEndpoints.Models;
using Newtonsoft.Json.Linq;
@ -66,7 +66,7 @@ public class DynamicApiEndpoint : AbstractApiProvider
{
var request = new FRestRequest(url)
{
OnBeforeDeserialization = resp => { resp.ContentType = "application/json; charset=utf-8"; }
Interceptors = [_interceptor]
};
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);

View File

@ -16,7 +16,7 @@ namespace FModel.ViewModels.ApiEndpoints;
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 _BASIC_TOKEN = "basic ZWM2ODRiOGM2ODdmNDc5ZmFkZWEzY2IyYWQ4M2Y1YzY6ZTFmMzFjMjExZjI4NDEzMTg2MjYyZDM3YTEzZmM4NGQ=";
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";
public EpicApiEndpoint(RestClient client) : base(client) { }

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AutoUpdaterDotNET;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
@ -105,7 +106,11 @@ public class FModelApiEndpoint : AbstractApiProvider
public void CheckForUpdates(bool launch = false)
{
if (DateTime.Now < UserSettings.Default.NextUpdateCheck) return;
if (DateTime.Now < UserSettings.Default.NextUpdateCheck)
{
Log.Warning("Updates have been silenced until {DateTime}", UserSettings.Default.NextUpdateCheck);
return;
}
if (launch)
{
@ -139,7 +144,8 @@ public class FModelApiEndpoint : AbstractApiProvider
{
UserSettings.Default.LastUpdateCheck = DateTime.Now;
if (((CustomMandatory)args.Mandatory).CommitHash == Constants.APP_COMMIT_ID)
var targetHash = ((CustomMandatory) args.Mandatory).CommitHash;
if (targetHash == Constants.APP_COMMIT_ID)
{
if (UserSettings.Default.ShowChangelog)
ShowChangelog(args);
@ -151,6 +157,7 @@ public class FModelApiEndpoint : AbstractApiProvider
UserSettings.Default.ShowChangelog = currentVersion != args.InstalledVersion;
const string message = "A new update is available!";
Log.Warning("{message} Version {CurrentVersion} ({Hash})", message, currentVersion, targetHash);
Helper.OpenWindow<AdonisWindow>(message, () => new UpdateView { Title = message, ResizeMode = ResizeMode.NoResize }.ShowDialog());
}
else

View File

@ -11,19 +11,19 @@ public class FortniteCentralApiEndpoint : AbstractApiProvider
{
public FortniteCentralApiEndpoint(RestClient client) : base(client) { }
public async Task<Dictionary<string, Dictionary<string, string>>> GetHotfixesAsync(CancellationToken token, string language = "en")
public async Task<IDictionary<string, IDictionary<string, string>>> GetHotfixesAsync(CancellationToken token, string language = "en")
{
var request = new FRestRequest("https://fortnitecentral.genxgames.gg/api/v1/hotfixes")
{
OnBeforeDeserialization = resp => { resp.ContentType = "application/json; charset=utf-8"; }
Interceptors = [_interceptor]
};
request.AddParameter("lang", language);
var response = await _client.ExecuteAsync<Dictionary<string, Dictionary<string, string>>>(request, token).ConfigureAwait(false);
var response = await _client.ExecuteAsync<IDictionary<string, IDictionary<string, string>>>(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.Data;
}
public Dictionary<string, Dictionary<string, string>> GetHotfixes(CancellationToken token, string language = "en")
public IDictionary<string, IDictionary<string, string>> GetHotfixes(CancellationToken token, string language = "en")
{
return GetHotfixesAsync(token, language).GetAwaiter().GetResult();
}

View File

@ -1,15 +1,13 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using FModel.Framework;
using FModel.ViewModels.ApiEndpoints.Models;
using RestSharp;
namespace FModel.ViewModels.ApiEndpoints;
public class GitHubApiEndpoint : AbstractApiProvider
public class GitHubApiEndpoint(RestClient client) : AbstractApiProvider(client)
{
public GitHubApiEndpoint(RestClient client) : base(client) { }
public async Task<GitHubCommit[]> GetCommitHistoryAsync(string branch = "dev", int page = 1, int limit = 20)
public async Task<GitHubCommit[]> GetCommitHistoryAsync(string branch = "dev", int page = 1, int limit = 30)
{
var request = new FRestRequest(Constants.GH_COMMITS_HISTORY);
request.AddParameter("sha", branch);
@ -25,4 +23,11 @@ public class GitHubApiEndpoint : AbstractApiProvider
var response = await _client.ExecuteAsync<GitHubRelease>(request).ConfigureAwait(false);
return response.Data;
}
public async Task<Author> GetUserAsync(string username)
{
var request = new FRestRequest($"https://api.github.com/users/{username}");
var response = await _client.ExecuteAsync<Author>(request).ConfigureAwait(false);
return response.Data;
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Windows;
using AdonisUI.Controls;
using AutoUpdaterDotNET;
@ -37,8 +38,7 @@ public class GitHubAsset : ViewModel
public class GitHubCommit : ViewModel
{
private string _sha;
[J("sha")]
public string Sha
[J("sha")] public string Sha
{
get => _sha;
set
@ -52,6 +52,35 @@ public class GitHubCommit : ViewModel
[J("commit")] public Commit Commit { get; set; }
[J("author")] public Author Author { get; set; }
private Author[] _coAuthors = [];
public Author[] CoAuthors
{
get => _coAuthors;
set
{
SetProperty(ref _coAuthors, value);
RaisePropertyChanged(nameof(Authors));
RaisePropertyChanged(nameof(AuthorNames));
}
}
public Author[] Authors => Author != null ? new[] { Author }.Concat(CoAuthors).ToArray() : CoAuthors;
public string AuthorNames
{
get
{
var authors = Authors;
return authors.Length switch
{
0 => string.Empty,
1 => authors[0].Login,
2 => $"{authors[0].Login} and {authors[1].Login}",
_ => string.Join(", ", authors.Take(authors.Length - 1).Select(a => a.Login)) + $", and {authors[^1].Login}"
};
}
}
private GitHubAsset _asset;
public GitHubAsset Asset
{

View File

@ -22,7 +22,7 @@ namespace FModel.ViewModels.ApiEndpoints;
public class ValorantApiEndpoint : AbstractApiProvider
{
private const string _URL = "https://fmodel.fortnite-api.com/valorant/v2/manifest";
private const string _URL = "https://valorant-api.com/v1/fmodel/manifest";
public ValorantApiEndpoint(RestClient client) : base(client) { }
@ -30,6 +30,8 @@ public class ValorantApiEndpoint : AbstractApiProvider
{
var request = new FRestRequest(_URL);
var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false);
if (!response.IsSuccessful)
return null;
return new VManifest(response.RawBytes);
}
@ -116,8 +118,6 @@ public class VManifest
return chunkBytes;
}
public VPakStream GetPakStream(int index) => new VPakStream(this, index);
}
public readonly struct VHeader
@ -168,6 +168,7 @@ public readonly struct VPak
}
public string GetFullName() => $"ValorantLive/ShooterGame/Content/Paks/{Name}";
public VPakStream GetStream(VManifest manifest) => new(manifest, this);
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
@ -176,22 +177,21 @@ public readonly struct VChunk
public readonly ulong Id;
public readonly uint Size;
public string GetUrl() => $"https://fmodel.fortnite-api.com/valorant/v2/chunks/{Id}";
public string GetUrl() => $"https://valorant-api.com/v1/fmodel/chunks/{Id}";
}
public class VPakStream : Stream, IRandomAccessStream, ICloneable
public class VPakStream : RandomAccessStream, ICloneable
{
private readonly VManifest _manifest;
private readonly int _pakIndex;
private readonly VPak _pak;
private readonly VChunk[] _chunks;
public VPakStream(VManifest manifest, int pakIndex, long position = 0L)
public VPakStream(VManifest manifest, in VPak pak, long position = 0L)
{
_manifest = manifest;
_pakIndex = pakIndex;
_pak = pak;
_position = position;
var pak = manifest.Paks[pakIndex];
_chunks = new VChunk[pak.ChunkIndices.Length];
for (var i = 0; i < _chunks.Length; i++)
{
@ -201,12 +201,12 @@ public class VPakStream : Stream, IRandomAccessStream, ICloneable
Length = pak.Size;
}
public object Clone() => new VPakStream(_manifest, _pakIndex, _position);
public object Clone() => new VPakStream(_manifest, _pak, _position);
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) =>
public override 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)
@ -216,7 +216,7 @@ public class VPakStream : Stream, IRandomAccessStream, ICloneable
return bytesRead;
}
public async Task<int> ReadAtAsync(long position, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
public override async Task<int> ReadAtAsync(long position, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default)
{
var (i, startPos) = GetChunkIndex(position);
if (i == -1) return 0;
@ -248,11 +248,6 @@ public class VPakStream : Stream, IRandomAccessStream, ICloneable
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>();
@ -262,7 +257,7 @@ public class VPakStream : Stream, IRandomAccessStream, ICloneable
await s.WaitAsync(cancellationToken).ConfigureAwait(false);
var chunk = _chunks[i++];
tasks.Add(PrefetchChunkAsync(chunk));
tasks.Add(PrefetchChunkAsync(_manifest, chunk, s, cancellationToken));
if (i == _chunks.Length) break;
count -= chunk.Size - startPos;
@ -271,11 +266,12 @@ public class VPakStream : Stream, IRandomAccessStream, ICloneable
await Task.WhenAll(tasks).ConfigureAwait(false);
s.Dispose();
return;
async Task PrefetchChunkAsync(VChunk chunk)
static async Task PrefetchChunkAsync(VManifest manifest, VChunk chunk, SemaphoreSlim semaphore, CancellationToken cancellationToken)
{
await _manifest.PrefetchChunk(chunk, cancellationToken).ConfigureAwait(false);
s.Release(); // This is intended
await manifest.PrefetchChunk(chunk, cancellationToken).ConfigureAwait(false);
semaphore.Release();
}
}

View File

@ -6,10 +6,12 @@ using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using CUE4Parse_Conversion.Textures.BC;
using CUE4Parse.Compression;
using CUE4Parse.Encryption.Aes;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.UE4.VirtualFileSystem;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
@ -42,6 +44,32 @@ public class ApplicationViewModel : ViewModel
private init => SetProperty(ref _status, value);
}
public IEnumerable<EAssetCategory> Categories { get; } = AssetCategoryExtensions.GetBaseCategories();
private bool _isAssetsExplorerVisible;
public bool IsAssetsExplorerVisible
{
get => _isAssetsExplorerVisible;
set
{
if (value && !UserSettings.Default.FeaturePreviewNewAssetExplorer)
return;
SetProperty(ref _isAssetsExplorerVisible, value);
}
}
private int _selectedLeftTabIndex;
public int SelectedLeftTabIndex
{
get => _selectedLeftTabIndex;
set
{
if (value is < 0 or > 2) return;
SetProperty(ref _selectedLeftTabIndex, value);
}
}
public RightClickMenuCommand RightClickMenuCommand => _rightClickMenuCommand ??= new RightClickMenuCommand(this);
private RightClickMenuCommand _rightClickMenuCommand;
public MenuCommand MenuCommand => _menuCommand ??= new MenuCommand(this);
@ -49,7 +77,7 @@ public class ApplicationViewModel : ViewModel
public CopyCommand CopyCommand => _copyCommand ??= new CopyCommand(this);
private CopyCommand _copyCommand;
public string InitialWindowTitle => $"FModel ({Constants.APP_SHORT_COMMIT_ID})";
public string InitialWindowTitle => $"FModel ({Constants.APP_SHORT_COMMIT_ID} - {Constants.APP_BUILD_DATE:MMM d, yyyy})";
public string GameDisplayName => CUE4Parse.Provider.GameDisplayName ?? "Unknown";
public string TitleExtra => $"({UserSettings.Default.CurrentDir.UeVersion}){(Build != EBuildKind.Release ? $" ({Build})" : "")}";
@ -195,32 +223,37 @@ public class ApplicationViewModel : ViewModel
public static async Task InitVgmStream()
{
var vgmZipFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "vgmstream-win.zip");
if (File.Exists(vgmZipFilePath)) return;
var vgmFileInfo = new FileInfo(vgmZipFilePath);
await ApplicationService.ApiEndpointView.DownloadFileAsync("https://github.com/vgmstream/vgmstream/releases/latest/download/vgmstream-win.zip", vgmZipFilePath);
if (new FileInfo(vgmZipFilePath).Length > 0)
if (!vgmFileInfo.Exists || vgmFileInfo.LastWriteTimeUtc < DateTime.UtcNow.AddMonths(-4))
{
var zipDir = Path.GetDirectoryName(vgmZipFilePath)!;
await using var zipFs = File.OpenRead(vgmZipFilePath);
using var zip = new ZipArchive(zipFs, ZipArchiveMode.Read);
await ApplicationService.ApiEndpointView.DownloadFileAsync("https://github.com/vgmstream/vgmstream/releases/latest/download/vgmstream-win.zip", vgmZipFilePath);
vgmFileInfo.Refresh();
foreach (var entry in zip.Entries)
if (vgmFileInfo.Length > 0)
{
var entryPath = Path.Combine(zipDir, entry.FullName);
await using var entryFs = File.Create(entryPath);
await using var entryStream = entry.Open();
await entryStream.CopyToAsync(entryFs);
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
{
FLogger.Append(ELog.Error, () => FLogger.Text("Could not download VgmStream", Constants.WHITE, true));
}
}
else
{
FLogger.Append(ELog.Error, () => FLogger.Text("Could not download VgmStream", Constants.WHITE, true));
}
}
public static async Task InitImGuiSettings(bool forceDownload)
{
var imgui = "imgui.ini";
const string imgui = "imgui.ini";
var imguiPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", imgui);
if (File.Exists(imgui)) File.Move(imgui, imguiPath, true);
@ -233,29 +266,64 @@ public class ApplicationViewModel : ViewModel
}
}
public static async ValueTask InitOodle()
public static async Task InitOodle()
{
var oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_DLL_NAME);
if (File.Exists(OodleHelper.OODLE_DLL_NAME))
if (File.Exists(OodleHelper.OODLE_DLL_NAME_OLD))
{
File.Move(OodleHelper.OODLE_DLL_NAME, oodlePath, true);
try
{
File.Delete(OodleHelper.OODLE_DLL_NAME_OLD);
}
catch { /* ignored */}
}
else if (!File.Exists(oodlePath))
var oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_DLL_NAME_OLD);
if (!File.Exists(oodlePath))
{
await OodleHelper.DownloadOodleDllAsync(oodlePath);
oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_DLL_NAME);
}
if (!File.Exists(oodlePath))
{
if (!await OodleHelper.DownloadOodleDllAsync(oodlePath))
{
FLogger.Append(ELog.Error, () => FLogger.Text("Failed to download Oodle", Constants.WHITE, true));
return;
}
}
OodleHelper.Initialize(oodlePath);
}
public static async ValueTask InitZlib()
public static async Task InitZlib()
{
var zlibPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", ZlibHelper.DLL_NAME);
if (!File.Exists(zlibPath))
var zlibFileInfo = new FileInfo(zlibPath);
if (!zlibFileInfo.Exists || zlibFileInfo.LastWriteTimeUtc < DateTime.UtcNow.AddMonths(-4))
{
await ZlibHelper.DownloadDllAsync(zlibPath);
if (!await ZlibHelper.DownloadDllAsync(zlibPath))
{
FLogger.Append(ELog.Error, () => FLogger.Text("Failed to download Zlib-ng", Constants.WHITE, true));
if (!zlibFileInfo.Exists) return;
}
}
ZlibHelper.Initialize(zlibPath);
}
public static async Task InitDetex()
{
var detexPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", DetexHelper.DLL_NAME);
if (File.Exists(DetexHelper.DLL_NAME))
{
File.Move(DetexHelper.DLL_NAME, detexPath, true);
}
else if (!File.Exists(detexPath))
{
await DetexHelper.LoadDllAsync(detexPath);
}
DetexHelper.Initialize(detexPath);
}
}

View File

@ -1,11 +1,15 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.Versions;
using CUE4Parse.UE4.VirtualFileSystem;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
@ -13,11 +17,11 @@ namespace FModel.ViewModels;
public class TreeItem : ViewModel
{
private string _header;
private readonly string _header;
public string Header
{
get => _header;
private set => SetProperty(ref _header, value);
private init => SetProperty(ref _header, value);
}
private bool _isExpanded;
@ -55,21 +59,139 @@ public class TreeItem : ViewModel
private set => SetProperty(ref _version, value);
}
public string PathAtThisPoint { get; }
public AssetsListViewModel AssetsList { get; }
public RangeObservableCollection<TreeItem> Folders { get; }
public ICollectionView FoldersView { get; }
private string _searchText = string.Empty;
public string SearchText
{
get => _searchText;
set
{
if (SetProperty(ref _searchText, value))
{
RefreshFilters();
}
}
}
public TreeItem(string header, string archive, string mountPoint, FPackageFileVersion version, string pathHere)
private EAssetCategory _selectedCategory = EAssetCategory.All;
public EAssetCategory SelectedCategory
{
get => _selectedCategory;
set
{
if (SetProperty(ref _selectedCategory, value))
_ = OnSelectedCategoryChanged();
}
}
public string PathAtThisPoint { get; }
public AssetsListViewModel AssetsList { get; } = new();
public RangeObservableCollection<TreeItem> Folders { get; } = [];
private ICollectionView _foldersView;
public ICollectionView FoldersView
{
get
{
_foldersView ??= new ListCollectionView(Folders)
{
SortDescriptions = { new SortDescription(nameof(Header), ListSortDirection.Ascending) }
};
return _foldersView;
}
}
private ICollectionView? _filteredFoldersView;
public ICollectionView? FilteredFoldersView
{
get
{
_filteredFoldersView ??= new ListCollectionView(Folders)
{
SortDescriptions = { new SortDescription(nameof(Header), ListSortDirection.Ascending) },
Filter = e => ItemFilter(e, SearchText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries))
};
return _filteredFoldersView;
}
}
private CompositeCollection _combinedEntries;
public CompositeCollection CombinedEntries
{
get
{
if (_combinedEntries == null)
{
void CreateCombinedEntries()
{
_combinedEntries = new CompositeCollection
{
new CollectionContainer { Collection = FilteredFoldersView },
new CollectionContainer { Collection = AssetsList.AssetsView }
};
}
if (!Application.Current.Dispatcher.CheckAccess())
{
Application.Current.Dispatcher.Invoke(CreateCombinedEntries);
}
else
{
CreateCombinedEntries();
}
}
return _combinedEntries;
}
}
public TreeItem Parent { get; init; }
public TreeItem(string header, GameFile entry, string pathHere)
{
Header = header;
Archive = archive;
MountPoint = mountPoint;
Version = version;
if (entry is VfsEntry vfsEntry)
{
Archive = vfsEntry.Vfs.Name;
MountPoint = vfsEntry.Vfs.MountPoint;
Version = vfsEntry.Vfs.Ver;
}
PathAtThisPoint = pathHere;
AssetsList = new AssetsListViewModel();
Folders = new RangeObservableCollection<TreeItem>();
FoldersView = new ListCollectionView(Folders) { SortDescriptions = { new SortDescription("Header", ListSortDirection.Ascending) } };
AssetsList.AssetsView.Filter = o => ItemFilter(o, SearchText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries));
}
private void RefreshFilters()
{
AssetsList.AssetsView.Refresh();
FilteredFoldersView?.Refresh();
}
private bool ItemFilter(object item, IEnumerable<string> filters)
{
var f = filters.ToArray();
switch (item)
{
case GameFileViewModel entry:
{
bool matchesSearch = f.Length == 0 || f.All(x => entry.Asset.Name.Contains(x, StringComparison.OrdinalIgnoreCase));
bool matchesCategory = SelectedCategory == EAssetCategory.All || entry.AssetCategory.IsOfCategory(SelectedCategory);
return matchesSearch && matchesCategory;
}
case TreeItem folder:
{
bool matchesSearch = f.Length == 0 || f.All(x => folder.Header.Contains(x, StringComparison.OrdinalIgnoreCase));
bool matchesCategory = SelectedCategory == EAssetCategory.All;
return matchesSearch && matchesCategory;
}
}
return false;
}
private async Task OnSelectedCategoryChanged()
{
await Task.WhenAll(AssetsList.Assets.Select(asset => asset.ResolveAsync(EResolveCompute.Category)));
RefreshFilters();
}
public override string ToString() => $"{Header} | {Folders.Count} Folders | {AssetsList.Assets.Count} Files";
@ -82,11 +204,11 @@ public class AssetsFolderViewModel
public AssetsFolderViewModel()
{
Folders = new RangeObservableCollection<TreeItem>();
Folders = [];
FoldersView = new ListCollectionView(Folders) { SortDescriptions = { new SortDescription("Header", ListSortDirection.Ascending) } };
}
public void BulkPopulate(IReadOnlyCollection<VfsEntry> entries)
public void BulkPopulate(IReadOnlyCollection<GameFile> entries)
{
if (entries == null || entries.Count == 0)
return;
@ -95,54 +217,59 @@ public class AssetsFolderViewModel
{
var treeItems = new RangeObservableCollection<TreeItem>();
treeItems.SetSuppressionState(true);
var items = new List<AssetItem>(entries.Count);
foreach (var entry in entries)
{
var item = new AssetItem(entry.Path, entry.IsEncrypted, entry.Offset, entry.Size, entry.Vfs.Name, entry.CompressionMethod);
items.Add(item);
TreeItem lastNode = null;
TreeItem parentItem = null;
var folders = entry.Path.Split('/', StringSplitOptions.RemoveEmptyEntries);
var builder = new StringBuilder(64);
var parentNode = treeItems;
for (var i = 0; i < folders.Length - 1; i++)
{
TreeItem lastNode = null;
var folders = item.FullPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
var builder = new StringBuilder(64);
var parentNode = treeItems;
var folder = folders[i];
builder.Append(folder).Append('/');
lastNode = FindByHeaderOrNull(parentNode, folder);
for (var i = 0; i < folders.Length - 1; i++)
static TreeItem FindByHeaderOrNull(IReadOnlyList<TreeItem> list, string header)
{
var folder = folders[i];
builder.Append(folder).Append('/');
lastNode = FindByHeaderOrNull(parentNode, folder);
static TreeItem FindByHeaderOrNull(IReadOnlyList<TreeItem> list, string header)
for (var i = 0; i < list.Count; i++)
{
for (var i = 0; i < list.Count; i++)
{
if (list[i].Header == header)
return list[i];
}
return null;
if (list[i].Header == header)
return list[i];
}
if (lastNode == null)
{
var nodePath = builder.ToString();
lastNode = new TreeItem(folder, item.Archive, entry.Vfs.MountPoint, entry.Vfs.Ver, nodePath[..^1]);
lastNode.Folders.SetSuppressionState(true);
lastNode.AssetsList.Assets.SetSuppressionState(true);
parentNode.Add(lastNode);
}
parentNode = lastNode.Folders;
return null;
}
lastNode?.AssetsList.Assets.Add(item);
if (lastNode == null)
{
var nodePath = builder.ToString();
lastNode = new TreeItem(folder, entry, nodePath[..^1])
{
Parent = parentItem
};
lastNode.Folders.SetSuppressionState(true);
lastNode.AssetsList.Assets.SetSuppressionState(true);
parentNode.Add(lastNode);
}
parentItem = lastNode;
parentNode = lastNode.Folders;
}
lastNode?.AssetsList.Add(entry);
}
if (treeItems.Count > 0)
{
var projectName = ApplicationService.ApplicationView.CUE4Parse.Provider.ProjectName;
(treeItems.FirstOrDefault(x => x.Header.Equals(projectName, StringComparison.OrdinalIgnoreCase)) ?? treeItems[0]).IsSelected = true;
}
Folders.AddRange(treeItems);
ApplicationService.ApplicationView.CUE4Parse.SearchVm.SearchResults.AddRange(items);
ApplicationService.ApplicationView.CUE4Parse.SearchVm.ChangeCollection(entries);
foreach (var folder in Folders)
InvokeOnCollectionChanged(folder);
@ -154,7 +281,6 @@ public class AssetsFolderViewModel
if (item.Folders.Count != 0)
{
item.Folders.SetSuppressionState(false);
item.Folders.InvokeOnCollectionChanged();
foreach (var folderItem in item.Folders)

View File

@ -1,78 +1,26 @@
using System.ComponentModel;
using System.ComponentModel;
using System.Windows.Data;
using CUE4Parse.Compression;
using CUE4Parse.FileProvider.Objects;
using FModel.Framework;
namespace FModel.ViewModels;
public class AssetItem : ViewModel
{
private string _fullPath;
public string FullPath
{
get => _fullPath;
private set => SetProperty(ref _fullPath, value);
}
private bool _isEncrypted;
public bool IsEncrypted
{
get => _isEncrypted;
private set => SetProperty(ref _isEncrypted, value);
}
private long _offset;
public long Offset
{
get => _offset;
private set => SetProperty(ref _offset, value);
}
private long _size;
public long Size
{
get => _size;
private set => SetProperty(ref _size, value);
}
private string _archive;
public string Archive
{
get => _archive;
private set => SetProperty(ref _archive, value);
}
private CompressionMethod _compression;
public CompressionMethod Compression
{
get => _compression;
private set => SetProperty(ref _compression, value);
}
public AssetItem(string fullPath, bool isEncrypted, long offset, long size, string archive, CompressionMethod compression)
{
FullPath = fullPath;
IsEncrypted = isEncrypted;
Offset = offset;
Size = size;
Archive = archive;
Compression = compression;
}
public override string ToString() => FullPath;
}
public class AssetsListViewModel
{
public RangeObservableCollection<AssetItem> Assets { get; }
public ICollectionView AssetsView { get; }
public RangeObservableCollection<GameFileViewModel> Assets { get; } = [];
public AssetsListViewModel()
private ICollectionView _assetsView;
public ICollectionView AssetsView
{
Assets = new RangeObservableCollection<AssetItem>();
AssetsView = new ListCollectionView(Assets)
get
{
SortDescriptions = { new SortDescription("FullPath", ListSortDirection.Ascending) }
};
_assetsView ??= new ListCollectionView(Assets)
{
SortDescriptions = { new SortDescription("Asset.Path", ListSortDirection.Ascending) }
};
return _assetsView;
}
}
public void Add(GameFile gameFile) => Assets.Add(new GameFileViewModel(gameFile));
}

View File

@ -1,7 +1,3 @@
using CSCore;
using CSCore.DSP;
using CSCore.SoundOut;
using CSCore.Streams;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@ -12,7 +8,15 @@ using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Data;
using CSCore;
using CSCore.CoreAudioAPI;
using CSCore.DSP;
using CSCore.SoundOut;
using CSCore.Streams;
using CUE4Parse.UE4.CriWare.Decoders;
using CUE4Parse.UE4.CriWare.Decoders.ADX;
using CUE4Parse.UE4.CriWare.Decoders.HCA;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
@ -293,6 +297,14 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
{
Save(a, true);
}
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved audio from ", Constants.WHITE);
FLogger.Link(_audioFiles.First().FileName, _audioFiles.First().FilePath, true);
});
if (_audioFiles.Count > 1)
FLogger.Append(ELog.Information, () => FLogger.Text($"Successfully saved {_audioFiles.Count} audio files", Constants.WHITE, true));
});
}
@ -328,16 +340,22 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
if (File.Exists(path))
{
Log.Information("{FileName} successfully saved", fileToSave.FileName);
FLogger.Append(ELog.Information, () =>
if (!auto)
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(fileToSave.FileName, path, true);
});
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(fileToSave.FileName, path, true);
});
}
}
else
{
Log.Error("{FileName} could not be saved", fileToSave.FileName);
FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{fileToSave.FileName}'", Constants.WHITE, true));
if (!auto)
{
FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{fileToSave.FileName}'", Constants.WHITE, true));
}
}
}
@ -548,7 +566,9 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
switch (SelectedAudioFile.Extension)
{
case "binka":
case "adpcm":
case "xvag":
case "opus":
case "wem":
case "at9":
@ -563,9 +583,12 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
return false;
}
case "binka":
case "adx":
case "hca":
return TryConvertCriware();
case "rada":
{
if (TryDecode(out var rawFilePath))
if (TryDecode(SelectedAudioFile.Extension, out var rawFilePath))
{
var newAudio = new AudioFile(SelectedAudioFile.Id, new FileInfo(rawFilePath));
Replace(newAudio);
@ -579,6 +602,48 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
return true;
}
private bool TryConvertCriware()
{
try
{
byte[] wavData = SelectedAudioFile.Extension switch
{
"hca" => HcaWaveStream.ConvertHcaToWav(
SelectedAudioFile.Data,
UserSettings.Default.CurrentDir.CriwareDecryptionKey),
"adx" => AdxDecoder.ConvertAdxToWav(
SelectedAudioFile.Data,
UserSettings.Default.CurrentDir.CriwareDecryptionKey),
_ => throw new NotSupportedException()
};
string wavFilePath = Path.Combine(
UserSettings.Default.AudioDirectory,
SelectedAudioFile.FilePath.TrimStart('/'));
wavFilePath = Path.ChangeExtension(wavFilePath, ".wav");
Directory.CreateDirectory(Path.GetDirectoryName(wavFilePath)!);
File.WriteAllBytes(wavFilePath, wavData);
var newAudio = new AudioFile(SelectedAudioFile.Id, new FileInfo(wavFilePath));
Replace(newAudio);
return true;
}
catch (CriwareDecryptionException ex)
{
FLogger.Append(ELog.Error, () => FLogger.Text($"Encrypted {SelectedAudioFile.Extension.ToUpper()}: {ex.Message}", Constants.WHITE, true));
Log.Error($"Encrypted {SelectedAudioFile.Extension.ToUpper()}: {ex.Message}");
return false;
}
catch (Exception ex)
{
FLogger.Append(ELog.Error, () => FLogger.Text($"Failed to convert {SelectedAudioFile.Extension.ToUpper()}: {ex.Message}", Constants.WHITE, true));
Log.Error($"Failed to convert {SelectedAudioFile.Extension.ToUpper()}: {ex.Message}");
return false;
}
}
private bool TryConvert(out string wavFilePath) => TryConvert(SelectedAudioFile.FilePath, SelectedAudioFile.Data, out wavFilePath);
private bool TryConvert(string inputFilePath, byte[] inputFileData, out string wavFilePath)
{
@ -607,11 +672,11 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
return vgmProcess?.ExitCode == 0 && File.Exists(wavFilePath);
}
private bool TryDecode(out string rawFilePath)
private bool TryDecode(string extension, out string rawFilePath)
{
rawFilePath = string.Empty;
var binkadecPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "binkadec.exe");
if (!File.Exists(binkadecPath))
var decoderPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", $"{extension}dec.exe");
if (!File.Exists(decoderPath))
{
return false;
}
@ -620,16 +685,16 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
File.WriteAllBytes(SelectedAudioFile.FilePath, SelectedAudioFile.Data);
rawFilePath = Path.ChangeExtension(SelectedAudioFile.FilePath, ".wav");
var binkadecProcess = Process.Start(new ProcessStartInfo
var decoderProcess = Process.Start(new ProcessStartInfo
{
FileName = binkadecPath,
FileName = decoderPath,
Arguments = $"-i \"{SelectedAudioFile.FilePath}\" -o \"{rawFilePath}\"",
UseShellExecute = false,
CreateNoWindow = true
});
binkadecProcess?.WaitForExit(5000);
decoderProcess?.WaitForExit(5000);
File.Delete(SelectedAudioFile.FilePath);
return binkadecProcess?.ExitCode == 0 && File.Exists(rawFilePath);
return decoderProcess?.ExitCode == 0 && File.Exists(rawFilePath);
}
}

View File

@ -7,7 +7,6 @@ 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;
using FModel.Settings;
@ -67,7 +66,7 @@ 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"));
var func = new Func<GameFile, bool>(x => !x.IsUePackagePayload);
using var fileStream = new FileStream(fullPath, FileMode.Create);
using var compressedStream = LZ4Stream.Encode(fileStream, LZ4Level.L00_FAST);
@ -81,7 +80,7 @@ public class BackupManagerViewModel : ViewModel
if (!func(asset)) continue;
writer.Write(asset.Size);
writer.Write(asset.IsEncrypted);
writer.Write($"/{asset.Path.ToLower()}");
writer.Write(asset.Path);
}
SaveCheck(fullPath, fileName, "created", "create");
@ -122,6 +121,7 @@ public enum EBackupVersion : byte
{
BeforeVersionWasAdded = 0,
Initial,
PerfectPath, // no more leading slash and ToLower
LatestPlusOne,
Latest = LatestPlusOne - 1

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,9 @@
using System.Collections;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using FModel.Extensions;
using CUE4Parse.FileProvider.Objects;
using FModel.Framework;
namespace FModel.ViewModels.Commands;
@ -18,29 +19,37 @@ public class CopyCommand : ViewModelCommand<ApplicationViewModel>
if (parameter is not object[] parameters || parameters[0] is not string trigger)
return;
var assetItems = ((IList) parameters[1]).Cast<AssetItem>().ToArray();
if (!assetItems.Any()) return;
var entries = (parameters[1] as IEnumerable)?.OfType<object>()
.SelectMany(item => item switch
{
GameFile gf => new[] { gf },
GameFileViewModel gvm => new[] { gvm.Asset },
_ => []
}) ?? [];
if (!entries.Any())
return;
var sb = new StringBuilder();
switch (trigger)
{
case "File_Path":
foreach (var asset in assetItems) sb.AppendLine(asset.FullPath);
foreach (var entry in entries) sb.AppendLine(entry.Path);
break;
case "File_Name":
foreach (var asset in assetItems) sb.AppendLine(asset.FullPath.SubstringAfterLast('/'));
foreach (var entry in entries) sb.AppendLine(entry.Name);
break;
case "Directory_Path":
foreach (var asset in assetItems) sb.AppendLine(asset.FullPath.SubstringBeforeLast('/'));
foreach (var entry in entries) sb.AppendLine(entry.Directory);
break;
case "File_Path_No_Extension":
foreach (var asset in assetItems) sb.AppendLine(asset.FullPath.SubstringBeforeLast('.'));
foreach (var entry in entries) sb.AppendLine(entry.PathWithoutExtension);
break;
case "File_Name_No_Extension":
foreach (var asset in assetItems) sb.AppendLine(asset.FullPath.SubstringAfterLast('/').SubstringBeforeLast('.'));
foreach (var entry in entries) sb.AppendLine(entry.NameWithoutExtension);
break;
}
Clipboard.SetText(sb.ToString().TrimEnd());
}
}
}

View File

@ -21,7 +21,7 @@ public class GoToCommand : ViewModelCommand<CustomDirectoriesViewModel>
public TreeItem JumpTo(string directory)
{
MainWindow.YesWeCats.LeftTabControl.SelectedIndex = 1; // folders tab
_applicationView.SelectedLeftTabIndex = 1; // folders tab
var root = _applicationView.CUE4Parse.AssetsFolder.Folders;
if (root is not { Count: > 0 }) return null;
@ -54,4 +54,4 @@ public class GoToCommand : ViewModelCommand<CustomDirectoriesViewModel>
return null;
}
}
}

View File

@ -14,34 +14,34 @@ public class ImageCommand : ViewModelCommand<TabItem>
{
}
public override void Execute(TabItem contextViewModel, object parameter)
public override void Execute(TabItem tabViewModel, object parameter)
{
if (parameter == null || !contextViewModel.HasImage) return;
if (parameter == null || !tabViewModel.HasImage) return;
switch (parameter)
{
case "Open":
{
Helper.OpenWindow<AdonisWindow>(contextViewModel.SelectedImage.ExportName + " (Image)", () =>
Helper.OpenWindow<AdonisWindow>(tabViewModel.SelectedImage.ExportName + " (Image)", () =>
{
var popout = new ImagePopout
{
Title = contextViewModel.SelectedImage.ExportName + " (Image)",
Width = contextViewModel.SelectedImage.Image.Width,
Height = contextViewModel.SelectedImage.Image.Height,
WindowState = contextViewModel.SelectedImage.Image.Height > 1000 ? WindowState.Maximized : WindowState.Normal,
ImageCtrl = { Source = contextViewModel.SelectedImage.Image }
Title = tabViewModel.SelectedImage.ExportName + " (Image)",
Width = tabViewModel.SelectedImage.Image.Width,
Height = tabViewModel.SelectedImage.Image.Height,
WindowState = tabViewModel.SelectedImage.Image.Height > 1000 ? WindowState.Maximized : WindowState.Normal,
ImageCtrl = { Source = tabViewModel.SelectedImage.Image }
};
RenderOptions.SetBitmapScalingMode(popout.ImageCtrl, BoolToRenderModeConverter.Instance.Convert(contextViewModel.SelectedImage.RenderNearestNeighbor));
RenderOptions.SetBitmapScalingMode(popout.ImageCtrl, BoolToRenderModeConverter.Instance.Convert(tabViewModel.SelectedImage.RenderNearestNeighbor));
popout.Show();
});
break;
}
case "Copy":
ClipboardExtensions.SetImage(contextViewModel.SelectedImage.ImageBuffer, $"{contextViewModel.SelectedImage.ExportName}.png");
ClipboardExtensions.SetImage(tabViewModel.SelectedImage.ImageBuffer, $"{tabViewModel.SelectedImage.ExportName}.png");
break;
case "Save":
contextViewModel.SaveImage();
tabViewModel.SaveImage();
break;
}
}

View File

@ -8,9 +8,10 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdonisUI.Controls;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.Readers;
using CUE4Parse.UE4.VirtualFileSystem;
using FModel.Creator;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
@ -37,21 +38,26 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
public override async void Execute(LoadingModesViewModel contextViewModel, object parameter)
{
if (_applicationView.CUE4Parse.GameDirectory.HasNoFile) return;
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));
return;
}
if (_applicationView.CUE4Parse.Provider.Files.Count == 0)
{
FLogger.Append(ELog.Error, () => FLogger.Text("No files were found in the archives or the specified directory", Constants.WHITE, true));
return;
}
#if DEBUG
var loadingTime = Stopwatch.StartNew();
#endif
_applicationView.CUE4Parse.AssetsFolder.Folders.Clear();
_applicationView.CUE4Parse.SearchVm.SearchResults.Clear();
MainWindow.YesWeCats.LeftTabControl.SelectedIndex = 1; // folders tab
Helper.CloseWindow<AdonisWindow>("Search View"); // close search window if opened
_applicationView.SelectedLeftTabIndex = 1; // folders tab
_applicationView.IsAssetsExplorerVisible = true;
Helper.CloseWindow<AdonisWindow>("Search For Packages"); // close search window if opened
await Task.WhenAll(
_applicationView.CUE4Parse.LoadLocalizedResources(), // load locres if not already loaded,
@ -59,12 +65,17 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
_threadWorkerView.Begin(cancellationToken =>
{
// filter what to show
_applicationView.Status.UpdateStatusLabel("Packages", "Filtering");
switch (UserSettings.Default.LoadingMode)
{
case ELoadingMode.Multiple:
{
var l = (IList) parameter;
if (l.Count < 1) return;
if (l.Count == 0)
{
UserSettings.Default.LoadingMode = ELoadingMode.All;
goto case ELoadingMode.All;
}
var directoryFilesToShow = l.Cast<FileItem>();
FilterDirectoryFilesToDisplay(cancellationToken, directoryFilesToShow);
@ -81,6 +92,11 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
FilterNewOrModifiedFilesToDisplay(cancellationToken);
break;
}
case ELoadingMode.AllButPatched:
{
FilterPacthedFilesToDisplay(cancellationToken);
break;
}
default: throw new ArgumentOutOfRangeException();
}
@ -100,42 +116,36 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
if (directoryFiles == null) filter = null;
else
{
filter = new HashSet<string>();
filter = [];
foreach (var directoryFile in directoryFiles)
{
if (!directoryFile.IsEnabled)
continue;
if (!directoryFile.IsEnabled) continue;
filter.Add(directoryFile.Name);
}
}
var hasFilter = filter != null && filter.Count != 0;
var entries = new List<VfsEntry>();
var entries = new List<GameFile>();
foreach (var asset in _applicationView.CUE4Parse.Provider.Files.Values)
{
cancellationToken.ThrowIfCancellationRequested(); // cancel if needed
if (asset is not VfsEntry entry || entry.Path.EndsWith(".uexp") || entry.Path.EndsWith(".ubulk") || entry.Path.EndsWith(".uptnl"))
continue;
if (asset.IsUePackagePayload) continue;
if (hasFilter)
{
if (filter.Contains(entry.Vfs.Name))
if (asset is VfsEntry entry && filter.Contains(entry.Vfs.Name))
{
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
entries.Add(asset);
}
}
else
{
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
entries.Add(asset);
}
}
_applicationView.Status.UpdateStatusLabel("Folders & Packages");
_applicationView.Status.UpdateStatusLabel($"{entries.Count:### ### ###} Packages");
_applicationView.CUE4Parse.AssetsFolder.BulkPopulate(entries);
}
@ -157,11 +167,11 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
var mode = UserSettings.Default.LoadingMode;
var entries = ParseBackup(openFileDialog.FileName, mode, cancellationToken);
_applicationView.Status.UpdateStatusLabel($"{mode.ToString()[6..]} Folders & Packages");
_applicationView.Status.UpdateStatusLabel($"{entries.Count:### ### ###} Packages");
_applicationView.CUE4Parse.AssetsFolder.BulkPopulate(entries);
}
private List<VfsEntry> ParseBackup(string path, ELoadingMode mode, CancellationToken cancellationToken = default)
private List<GameFile> ParseBackup(string path, ELoadingMode mode, CancellationToken cancellationToken = default)
{
using var fileStream = new FileStream(path, FileMode.Open);
using var memoryStream = new MemoryStream();
@ -176,13 +186,13 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
memoryStream.Position = 0;
using var archive = new FStreamArchive(fileStream.Name, memoryStream);
var entries = new List<VfsEntry>();
var entries = new List<GameFile>();
switch (mode)
{
case ELoadingMode.AllButNew:
{
var paths = new HashSet<string>();
var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var magic = archive.Read<uint>();
if (magic != BackupManagerViewModel.FBKP_MAGIC)
{
@ -192,7 +202,7 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
cancellationToken.ThrowIfCancellationRequested();
archive.Position += 29;
paths.Add(archive.ReadString().ToLower()[1..]);
paths.Add(archive.ReadString()[1..]);
archive.Position += 4;
}
}
@ -205,18 +215,19 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
cancellationToken.ThrowIfCancellationRequested();
archive.Position += sizeof(long) + sizeof(byte);
paths.Add(archive.ReadString().ToLower()[1..]);
var fullPath = archive.ReadString();
if (version < EBackupVersion.PerfectPath) fullPath = fullPath[1..];
paths.Add(fullPath);
}
}
foreach (var (key, value) in _applicationView.CUE4Parse.Provider.Files)
foreach (var (key, asset) in _applicationView.CUE4Parse.Provider.Files)
{
cancellationToken.ThrowIfCancellationRequested();
if (value is not VfsEntry entry || paths.Contains(key) || entry.Path.EndsWith(".uexp") ||
entry.Path.EndsWith(".ubulk") || entry.Path.EndsWith(".uptnl")) continue;
if (asset.IsUePackagePayload || paths.Contains(key)) continue;
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
entries.Add(asset);
}
break;
@ -235,7 +246,7 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
var uncompressedSize = archive.Read<long>();
var isEncrypted = archive.ReadFlag();
archive.Position += 4;
var fullPath = archive.ReadString().ToLower()[1..];
var fullPath = archive.ReadString()[1..];
archive.Position += 4;
AddEntry(fullPath, uncompressedSize, isEncrypted, entries);
@ -251,7 +262,8 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
var uncompressedSize = archive.Read<long>();
var isEncrypted = archive.ReadFlag();
var fullPath = archive.ReadString().ToLower()[1..];
var fullPath = archive.ReadString();
if (version < EBackupVersion.PerfectPath) fullPath = fullPath[1..];
AddEntry(fullPath, uncompressedSize, isEncrypted, entries);
}
@ -263,14 +275,34 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
return entries;
}
private void AddEntry(string path, long uncompressedSize, bool isEncrypted, List<VfsEntry> entries)
private void AddEntry(string path, long uncompressedSize, bool isEncrypted, List<GameFile> 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)
if (!_applicationView.CUE4Parse.Provider.Files.TryGetValue(path, out var asset) ||
asset.IsUePackagePayload || asset.Size == uncompressedSize && asset.IsEncrypted == isEncrypted)
return;
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
entries.Add(asset);
}
private void FilterPacthedFilesToDisplay(CancellationToken cancellationToken)
{
var loaded = new Dictionary<string, GameFile>(_applicationView.CUE4Parse.Provider.PathComparer);
foreach (var (key, asset) in _applicationView.CUE4Parse.Provider.Files)
{
cancellationToken.ThrowIfCancellationRequested(); // cancel if needed
if (asset.IsUePackagePayload) continue;
if (asset is VfsEntry entry && loaded.TryGetValue(key, out var file) &&
file is VfsEntry existingEntry && entry.Vfs.ReadOrder < existingEntry.Vfs.ReadOrder)
{
continue;
}
loaded[key] = asset;
}
_applicationView.Status.UpdateStatusLabel($"{loaded.Count:### ### ###} Packages");
_applicationView.CUE4Parse.AssetsFolder.BulkPopulate(loaded.Values);
}
}

View File

@ -29,9 +29,10 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
Helper.OpenWindow<AdonisWindow>("AES Manager", () => new AesManager().Show());
break;
case "Directory_Backup":
Helper.OpenWindow<AdonisWindow>("Backup Manager", () => new BackupManager(contextViewModel.CUE4Parse.Provider.InternalGameName).Show());
Helper.OpenWindow<AdonisWindow>("Backup Manager", () => new BackupManager(contextViewModel.CUE4Parse.Provider.ProjectName).Show());
break;
case "Directory_ArchivesInfo":
ApplicationService.ApplicationView.IsAssetsExplorerVisible = false;
contextViewModel.CUE4Parse.TabControl.AddTab("Archives Info");
contextViewModel.CUE4Parse.TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("json");
contextViewModel.CUE4Parse.TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(contextViewModel.CUE4Parse.GameDirectory.DirectoryFiles, Formatting.Indented), false, false);

View File

@ -1,8 +1,11 @@
using System.Collections;
using System.Collections;
using System.Linq;
using System.Threading;
using CUE4Parse.FileProvider.Objects;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.Views.Resources.Controls;
namespace FModel.ViewModels.Commands;
@ -19,62 +22,192 @@ public class RightClickMenuCommand : ViewModelCommand<ApplicationViewModel>
if (parameter is not object[] parameters || parameters[0] is not string trigger)
return;
var assetItems = ((IList) parameters[1]).Cast<AssetItem>().ToArray();
if (!assetItems.Any()) return;
var param = (parameters[1] as IEnumerable)?.OfType<object>().ToArray() ?? [];
if (param.Length == 0) return;
var updateUi = assetItems.Length > 1 ? EBulkType.Auto : EBulkType.None;
var folders = param.OfType<TreeItem>().ToArray();
var assets = param.SelectMany(item => item switch
{
GameFile gf => new[] { gf }, // search view passes GameFile directly
GameFileViewModel gvm => new[] { gvm.Asset },
_ => []
}).ToArray();
if (folders.Length == 0 && assets.Length == 0)
return;
var updateUi = assets.Length > 1 ? EBulkType.Auto : EBulkType.None;
await _threadWorkerView.Begin(cancellationToken =>
{
switch (trigger)
{
#region Asset Commands
case "Assets_Extract_New_Tab":
foreach (var asset in assetItems)
foreach (var entry in assets)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, true);
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, true);
}
break;
case "Assets_Show_Metadata":
foreach (var entry in assets)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.ShowMetadata(entry);
}
break;
case "Assets_Show_References":
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.FindReferences(assets.FirstOrDefault());
}
break;
case "Assets_Decompile":
foreach (var entry in assets)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Decompile(entry);
}
break;
case "Assets_Export_Data":
foreach (var asset in assetItems)
foreach (var entry in assets)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.ExportData(asset.FullPath);
contextViewModel.CUE4Parse.ExportData(entry);
}
break;
case "Assets_Save_Properties":
foreach (var asset in assetItems)
foreach (var entry in assets)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Properties | updateUi);
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, EBulkType.Properties | updateUi);
}
break;
case "Assets_Save_Textures":
foreach (var asset in assetItems)
foreach (var entry in assets)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Textures | updateUi);
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, EBulkType.Textures | updateUi);
}
break;
case "Assets_Save_Models":
foreach (var asset in assetItems)
foreach (var entry in assets)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Meshes | updateUi);
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, EBulkType.Meshes | updateUi);
}
break;
case "Assets_Save_Animations":
foreach (var asset in assetItems)
foreach (var entry in assets)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Animations | updateUi);
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, EBulkType.Animations | updateUi);
}
break;
case "Assets_Save_Audio":
foreach (var entry in assets)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, EBulkType.Audio | updateUi);
}
break;
#endregion
#region Folder Commands
case "Folders_Export_Data":
foreach (var folder in folders)
{
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.ExportFolder(cancellationToken, folder);
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully exported ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.RawDataDirectory, true);
});
}
break;
case "Folders_Save_Properties":
foreach (var folder in folders)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.SaveFolder(cancellationToken, folder);
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.PropertiesDirectory, true);
});
}
break;
case "Folders_Save_Textures":
foreach (var folder in folders)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.TextureFolder(cancellationToken, folder);
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved textures from ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.TextureDirectory, true);
});
}
break;
case "Folders_Save_Models":
foreach (var folder in folders)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.ModelFolder(cancellationToken, folder);
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved models from ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.ModelDirectory, true);
});
}
break;
case "Folders_Save_Animations":
foreach (var folder in folders)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.AnimationFolder(cancellationToken, folder);
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved animations from ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.ModelDirectory, true);
});
}
break;
case "Folders_Save_Audio":
foreach (var folder in folders)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.AudioFolder(cancellationToken, folder);
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved audio from ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.AudioDirectory, true);
});
}
break;
#endregion
}
});
}

View File

@ -1,4 +1,4 @@
using System.Windows;
using System.Windows;
using AdonisUI.Controls;
using FModel.Framework;
using FModel.Services;
@ -15,7 +15,7 @@ public class TabCommand : ViewModelCommand<TabItem>
{
}
public override async void Execute(TabItem contextViewModel, object parameter)
public override async void Execute(TabItem tabViewModel, object parameter)
{
switch (parameter)
{
@ -23,53 +23,62 @@ public class TabCommand : ViewModelCommand<TabItem>
_applicationView.CUE4Parse.TabControl.RemoveTab(mdlClick);
break;
case "Close_Tab":
_applicationView.CUE4Parse.TabControl.RemoveTab(contextViewModel);
_applicationView.CUE4Parse.TabControl.RemoveTab(tabViewModel);
break;
case "Close_All_Tabs":
_applicationView.CUE4Parse.TabControl.RemoveAllTabs();
break;
case "Close_Other_Tabs":
_applicationView.CUE4Parse.TabControl.RemoveOtherTabs(contextViewModel);
_applicationView.CUE4Parse.TabControl.RemoveOtherTabs(tabViewModel);
break;
case "Find_References":
_applicationView.CUE4Parse.FindReferences(tabViewModel.Entry);
break;
case "Asset_Export_Data":
await _threadWorkerView.Begin(_ => _applicationView.CUE4Parse.ExportData(contextViewModel.FullPath));
await _threadWorkerView.Begin(_ => _applicationView.CUE4Parse.ExportData(tabViewModel.Entry));
break;
case "Asset_Save_Properties":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, contextViewModel.FullPath, false, EBulkType.Properties);
_applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Properties);
});
break;
case "Asset_Save_Textures":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, contextViewModel.FullPath, false, EBulkType.Textures);
_applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Textures);
});
break;
case "Asset_Save_Models":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, contextViewModel.FullPath, false, EBulkType.Meshes | EBulkType.Auto);
_applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Meshes);
});
break;
case "Asset_Save_Animations":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, contextViewModel.FullPath, false, EBulkType.Animations | EBulkType.Auto);
_applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Animations);
});
break;
case "Asset_Save_Audio":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Audio);
});
break;
case "Open_Properties":
if (contextViewModel.Header == "New Tab" || contextViewModel.Document == null) return;
Helper.OpenWindow<AdonisWindow>(contextViewModel.Header + " (Properties)", () =>
if (tabViewModel.Header == "New Tab" || tabViewModel.Document == null) return;
Helper.OpenWindow<AdonisWindow>(tabViewModel.Header + " (Properties)", () =>
{
new PropertiesPopout(contextViewModel)
new PropertiesPopout(tabViewModel)
{
Title = contextViewModel.Header + " (Properties)"
Title = tabViewModel.Header + " (Properties)"
}.Show();
});
break;
case "Copy_Asset_Path":
Clipboard.SetText(contextViewModel.FullPath);
Clipboard.SetText(tabViewModel.Entry.Path);
break;
}
}

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Data;
using CUE4Parse.Compression;
using CUE4Parse.UE4.IO;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.UE4.VirtualFileSystem;
@ -69,6 +70,13 @@ public class FileItem : ViewModel
set => SetProperty(ref _guid, value);
}
private CompressionMethod[] _compressionMethods;
public CompressionMethod[] CompressionMethods
{
get => _compressionMethods;
set => SetProperty(ref _compressionMethods, value);
}
public FileItem(string name, long length)
{
Name = name;
@ -84,6 +92,7 @@ public class FileItem : ViewModel
IsEnabled = false;
Key = string.Empty;
FileCount = reader is IoStoreReader storeReader ? (int) storeReader.TocResource.Header.TocEntryCount - 1 : 0;
CompressionMethods = reader.CompressionMethods;
}
public override string ToString()

View File

@ -0,0 +1,407 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.GameTypes.FN.Assets.Exports.DataAssets;
using CUE4Parse.UE4.Assets;
using CUE4Parse.UE4.Assets.Exports.Animation;
using CUE4Parse.UE4.Assets.Exports.BuildData;
using CUE4Parse.UE4.Assets.Exports.Component;
using CUE4Parse.UE4.Assets.Exports.CriWare;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.Engine.Font;
using CUE4Parse.UE4.Assets.Exports.Fmod;
using CUE4Parse.UE4.Assets.Exports.Foliage;
using CUE4Parse.UE4.Assets.Exports.Internationalization;
using CUE4Parse.UE4.Assets.Exports.LevelSequence;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Material.Editor;
using CUE4Parse.UE4.Assets.Exports.Niagara;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.Sound;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Exports.Wwise;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Engine;
using CUE4Parse.UE4.Objects.Engine.Animation;
using CUE4Parse.UE4.Objects.Engine.Curves;
using CUE4Parse.UE4.Objects.MediaAssets;
using CUE4Parse.UE4.Objects.Niagara;
using CUE4Parse.UE4.Objects.PhysicsEngine;
using CUE4Parse.UE4.Objects.RigVM;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.UE4.Objects.UObject.Editor;
using CUE4Parse.Utils;
using CUE4Parse_Conversion.Textures;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using Serilog;
using SkiaSharp;
using Svg.Skia;
namespace FModel.ViewModels;
public class GameFileViewModel(GameFile asset) : ViewModel
{
private const int MaxPreviewSize = 128;
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
public EResolveCompute Resolved { get; private set; } = EResolveCompute.None;
public GameFile Asset { get; } = asset;
private string _resolvedAssetType = asset.Extension;
public string ResolvedAssetType
{
get => _resolvedAssetType;
private set => SetProperty(ref _resolvedAssetType, value);
}
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
private EAssetCategory _assetCategory = EAssetCategory.All;
public EAssetCategory AssetCategory
{
get => _assetCategory;
private set
{
SetProperty(ref _assetCategory, value);
Resolved |= EResolveCompute.Category; // blindly assume category is resolved when set, even if unchanged
}
}
private ImageSource _previewImage;
public ImageSource PreviewImage
{
get => _previewImage;
private set
{
if (SetProperty(ref _previewImage, value))
{
Resolved |= EResolveCompute.Preview;
}
}
}
public Task ExtractAsync()
=> ApplicationService.ThreadWorkerView.Begin(cancellationToken =>
_applicationView.CUE4Parse.ExtractSelected(cancellationToken, [Asset]));
public Task ResolveAsync(EResolveCompute resolve)
{
try
{
return ResolveInternalAsync(resolve);
}
catch (Exception e)
{
Log.Error(e, "Failed to resolve asset {AssetName} ({Resolver})", Asset.Path, resolve.ToStringBitfield());
Resolved = EResolveCompute.All;
return Task.CompletedTask;
}
}
private Task ResolveInternalAsync(EResolveCompute resolve)
{
if (!_applicationView.IsAssetsExplorerVisible || !UserSettings.Default.PreviewTexturesAssetExplorer)
{
resolve &= ~EResolveCompute.Preview;
}
resolve &= ~Resolved;
if (resolve == EResolveCompute.None)
return Task.CompletedTask;
if (!Asset.IsUePackage || _applicationView.CUE4Parse is null)
return ResolveByExtensionAsync(resolve);
return ResolveByPackageAsync(resolve);
}
private Task ResolveByPackageAsync(EResolveCompute resolve)
{
if (Asset.Extension is "umap")
{
AssetCategory = EAssetCategory.World;
ResolvedAssetType = "World";
Resolved |= EResolveCompute.Preview;
return Task.CompletedTask;
}
if (Asset.NameWithoutExtension.EndsWith("_BuiltData"))
{
AssetCategory = EAssetCategory.BuildData;
ResolvedAssetType = "MapBuildDataRegistry";
Resolved |= EResolveCompute.Preview;
return Task.CompletedTask;
}
return Task.Run(() =>
{
// TODO: cache and reuse packages
var pkg = _applicationView.CUE4Parse?.Provider.LoadPackage(Asset);
if (pkg is null)
throw new InvalidOperationException($"Failed to load {Asset.Path} as UE package.");
var mainIndex = pkg.GetExportIndex(Asset.NameWithoutExtension, StringComparison.OrdinalIgnoreCase);
if (mainIndex < 0) mainIndex = pkg.GetExportIndex($"{Asset.NameWithoutExtension}_C", StringComparison.OrdinalIgnoreCase);
if (mainIndex < 0) mainIndex = 0;
var pointer = new FPackageIndex(pkg, mainIndex + 1).ResolvedObject;
if (pointer?.Object is null)
return;
var dummy = ((AbstractUePackage) pkg).ConstructObject(pointer.Class?.Object?.Value as UStruct, pkg);
ResolvedAssetType = dummy.ExportType;
AssetCategory = dummy switch
{
URigVMBlueprintGeneratedClass => EAssetCategory.RigVMBlueprintGeneratedClass,
UAnimBlueprintGeneratedClass => EAssetCategory.AnimBlueprintGeneratedClass,
UWidgetBlueprintGeneratedClass => EAssetCategory.WidgetBlueprintGeneratedClass,
UBlueprintGeneratedClass or UFunction => EAssetCategory.BlueprintGeneratedClass,
UUserDefinedEnum => EAssetCategory.UserDefinedEnum,
UUserDefinedStruct => EAssetCategory.UserDefinedStruct,
UBlueprintCore => EAssetCategory.Blueprint,
UClassCookedMetaData or UStructCookedMetaData or UEnumCookedMetaData => EAssetCategory.CookedMetaData,
UStaticMesh => EAssetCategory.StaticMesh,
USkeletalMesh => EAssetCategory.SkeletalMesh,
UPhysicsAsset => EAssetCategory.PhysicsAsset,
UTexture => EAssetCategory.Texture,
UMaterialInterface => EAssetCategory.Material,
UMaterialInterfaceEditorOnlyData => EAssetCategory.MaterialEditorData,
UMaterialFunction => EAssetCategory.MaterialFunction,
UMaterialParameterCollection => EAssetCategory.MaterialParameterCollection,
UAnimationAsset => EAssetCategory.Animation,
USkeleton => EAssetCategory.Skeleton,
UWorld => EAssetCategory.World,
UMapBuildDataRegistry => EAssetCategory.BuildData,
ULevelSequence => EAssetCategory.LevelSequence,
UFoliageType => EAssetCategory.Foliage,
UItemDefinitionBase => EAssetCategory.ItemDefinitionBase,
UDataAsset or UDataTable or UCurveTable or UStringTable => EAssetCategory.Data,
UCurveBase => EAssetCategory.CurveBase,
UWwiseAssetLibrary or USoundBase or UAkMediaAssetData or UAtomWaveBank or USoundAtomCue
or UAtomCueSheet or USoundAtomCueSheet or UFMODBank or UFMODEvent or UAkAudioType => EAssetCategory.Audio,
UFileMediaSource => EAssetCategory.Video,
UFont or UFontFace => EAssetCategory.Font,
UNiagaraSystem or UNiagaraScriptBase or UParticleSystem => EAssetCategory.Particle,
_ => EAssetCategory.All
};
switch (AssetCategory)
{
case EAssetCategory.Texture when pointer.Object.Value is UTexture texture:
{
if (!resolve.HasFlag(EResolveCompute.Preview))
break;
var img = texture.Decode(MaxPreviewSize, UserSettings.Default.CurrentDir.TexturePlatform);
if (img != null)
{
using var bitmap = img.ToSkBitmap();
using var image = bitmap.Encode(SKEncodedImageFormat.Png, 100);
SetPreviewImage(image);
}
break;
}
case EAssetCategory.ItemDefinitionBase:
if (!resolve.HasFlag(EResolveCompute.Preview))
break;
if (pointer.Object.Value is UItemDefinitionBase itemDef)
{
if (LookupPreview(itemDef.DataList)) break;
if (itemDef is UAthenaPickaxeItemDefinition pickaxe && pickaxe.WeaponDefinition.TryLoad(out UItemDefinitionBase weaponDef))
{
LookupPreview(weaponDef.DataList);
}
bool LookupPreview(FInstancedStruct[] dataList)
{
foreach (var data in dataList)
{
if (!data.NonConstStruct.TryGetValue(out FSoftObjectPath icon, "Icon", "LargeIcon") ||
!icon.TryLoad<UTexture2D>(out var texture))
continue;
var img = texture.Decode(MaxPreviewSize, UserSettings.Default.CurrentDir.TexturePlatform);
if (img == null) return false;
using var bitmap = img.ToSkBitmap();
using var image = bitmap.Encode(SKEncodedImageFormat.Png, 100);
SetPreviewImage(image);
return true;
}
return false;
}
}
break;
default:
Resolved |= EResolveCompute.Preview;
break;
}
});
}
private Task ResolveByExtensionAsync(EResolveCompute resolve)
{
Resolved |= EResolveCompute.Preview;
switch (Asset.Extension)
{
case "uproject":
case "uefnproject":
case "upluginmanifest":
case "uplugin":
case "ini":
case "locmeta":
case "locres":
case "verse":
case "lua":
case "luac":
case "json5":
case "json":
case "bin":
case "txt":
case "log":
case "pem":
case "xml":
AssetCategory = EAssetCategory.Data;
break;
case "wav":
case "bank":
case "bnk":
case "pck":
case "awb":
case "acb":
case "xvag":
case "flac":
case "at9":
case "wem":
case "ogg":
AssetCategory = EAssetCategory.Audio;
break;
case "ufont":
case "otf":
case "ttf":
AssetCategory = EAssetCategory.Font;
break;
case "mp4":
AssetCategory = EAssetCategory.Video;
break;
case "jpg":
case "png":
case "bmp":
case "svg":
{
Resolved |= ~EResolveCompute.Preview;
AssetCategory = EAssetCategory.Texture;
if (!resolve.HasFlag(EResolveCompute.Preview))
break;
return Task.Run(() =>
{
var data = _applicationView.CUE4Parse.Provider.SaveAsset(Asset);
using var stream = new MemoryStream(data);
stream.Position = 0;
SKBitmap bitmap;
if (Asset.Extension == "svg")
{
var svg = new SKSvg();
svg.Load(stream);
if (svg.Picture == null)
return;
bitmap = new SKBitmap(MaxPreviewSize, MaxPreviewSize);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
var bounds = svg.Picture.CullRect;
float scale = Math.Min(MaxPreviewSize / bounds.Width, MaxPreviewSize / bounds.Height);
canvas.Scale(scale);
canvas.Translate(-bounds.Left, -bounds.Top);
canvas.DrawPicture(svg.Picture);
}
else
{
bitmap = SKBitmap.Decode(stream);
}
using var image = bitmap.Encode(Asset.Extension == "jpg" ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 100);
SetPreviewImage(image);
bitmap.Dispose();
});
}
default:
AssetCategory = EAssetCategory.All; // just so it sets resolved
break;
}
return Task.CompletedTask;
}
private void SetPreviewImage(SKData data)
{
using var ms = new MemoryStream(data.ToArray());
ms.Position = 0;
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = ms;
bitmap.EndInit();
bitmap.Freeze();
Application.Current.Dispatcher.InvokeAsync(() => PreviewImage = bitmap);
}
private CancellationTokenSource _previewCts;
public void OnIsVisible()
{
if (Resolved == EResolveCompute.All)
return;
_previewCts?.Cancel();
_previewCts = new CancellationTokenSource();
var token = _previewCts.Token;
Task.Delay(100, token).ContinueWith(t =>
{
if (t.IsCanceled) return;
ResolveAsync(EResolveCompute.All);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
[Flags]
public enum EResolveCompute
{
None = 0,
Category = 1 << 0,
Preview = 1 << 1,
All = Category | Preview
}

View File

@ -1,4 +1,3 @@
using FModel.Extensions;
using FModel.Framework;
using Newtonsoft.Json;
using Serilog;
@ -10,6 +9,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using CUE4Parse.UE4.Objects.Core.Serialization;
using CUE4Parse.UE4.Versions;
using CUE4Parse.Utils;
using FModel.Settings;
using FModel.ViewModels.ApiEndpoints.Models;
using Microsoft.Win32;
@ -84,11 +84,11 @@ public class GameSelectorViewModel : ViewModel
=> Enum.GetValues<EGame>()
.GroupBy(value => (int)value)
.Select(group => group.First())
.OrderBy(value => (int)value == ((int)value & ~0xF));
.OrderBy(value => ((int)value & 0xFF) == 0);
private IEnumerable<DirectorySettings> EnumerateDetectedGames()
{
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("Fortnite", "\\FortniteGame\\Content\\Paks", EGame.GAME_UE5_7);
yield return DirectorySettings.Default("Fortnite [LIVE]", Constants._FN_LIVE_TRIGGER, ue: EGame.GAME_UE5_7);
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);
@ -99,7 +99,7 @@ public class GameSelectorViewModel : ViewModel
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(381210, "\\DeadByDaylight\\Content\\Paks", EGame.GAME_DeadByDaylight, aesKey: "0x22b1639b548124925cf7b9cbaa09f9ac295fcf0324586d6b37ee1d42670b39b3"); // 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
@ -151,13 +151,13 @@ public class GameSelectorViewModel : ViewModel
return null;
}
private DirectorySettings GetSteamGame(int id, string pakDirectory, EGame ueVersion)
private DirectorySettings GetSteamGame(int id, string pakDirectory, EGame ueVersion, string aesKey = "")
{
var steamInfo = SteamDetection.GetSteamGameById(id);
if (steamInfo is not null)
{
Log.Debug("Found {GameName} in steam manifests", steamInfo.Name);
return DirectorySettings.Default(steamInfo.Name, $"{steamInfo.GameRoot}{pakDirectory}", ue: ueVersion);
return DirectorySettings.Default(steamInfo.Name, $"{steamInfo.GameRoot}{pakDirectory}", ue: ueVersion, aes: aesKey);
}
return null;

View File

@ -1,16 +1,25 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Data;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.VirtualFileSystem;
using FModel.Framework;
namespace FModel.ViewModels;
public class SearchViewModel : ViewModel
{
private string _filterText;
public enum ESortSizeMode
{
None,
Ascending,
Descending
}
private string _filterText = string.Empty;
public string FilterText
{
get => _filterText;
@ -31,34 +40,106 @@ public class SearchViewModel : ViewModel
set => SetProperty(ref _hasMatchCaseEnabled, value);
}
public int ResultsCount => SearchResults?.Count ?? 0;
public RangeObservableCollection<AssetItem> SearchResults { get; }
public ICollectionView SearchResultsView { get; }
private ESortSizeMode _currentSortSizeMode = ESortSizeMode.None;
public ESortSizeMode CurrentSortSizeMode
{
get => _currentSortSizeMode;
set => SetProperty(ref _currentSortSizeMode, value);
}
private int _resultsCount = 0;
public int ResultsCount
{
get => _resultsCount;
private set => SetProperty(ref _resultsCount, value);
}
private GameFile _refFile;
public GameFile RefFile
{
get => _refFile;
private set => SetProperty(ref _refFile, value);
}
public RangeObservableCollection<GameFile> SearchResults { get; }
public ListCollectionView SearchResultsView { get; }
public SearchViewModel()
{
SearchResults = new RangeObservableCollection<AssetItem>();
SearchResultsView = new ListCollectionView(SearchResults);
SearchResults = [];
SearchResultsView = new ListCollectionView(SearchResults)
{
Filter = e => ItemFilter(e, FilterText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries)),
};
ResultsCount = SearchResultsView.Count;
}
public void RefreshFilter()
{
if (SearchResultsView.Filter == null)
SearchResultsView.Filter = e => ItemFilter(e, FilterText.Trim().Split(' '));
else
SearchResultsView.Refresh();
SearchResultsView.Refresh();
ResultsCount = SearchResultsView.Count;
}
public void ChangeCollection(IEnumerable<GameFile> files, GameFile refFile = null)
{
SearchResults.Clear();
SearchResults.AddRange(files);
RefFile = refFile;
ResultsCount = SearchResultsView.Count;
}
public async Task CycleSortSizeMode()
{
CurrentSortSizeMode = CurrentSortSizeMode switch
{
ESortSizeMode.None => ESortSizeMode.Descending,
ESortSizeMode.Descending => ESortSizeMode.Ascending,
_ => ESortSizeMode.None
};
var sorted = await Task.Run(() =>
{
var archiveDict = SearchResults
.OfType<VfsEntry>()
.Select(f => f.Vfs.Name)
.Distinct()
.Select((name, idx) => (name, idx))
.ToDictionary(x => x.name, x => x.idx);
var keyed = SearchResults.Select(f =>
{
int archiveKey = f is VfsEntry ve && archiveDict.TryGetValue(ve.Vfs.Name, out var key) ? key : -1;
return (File: f, f.Size, ArchiveKey: archiveKey);
});
return CurrentSortSizeMode switch
{
ESortSizeMode.Ascending => keyed
.OrderBy(x => x.Size).ThenBy(x => x.ArchiveKey)
.Select(x => x.File).ToList(),
ESortSizeMode.Descending => keyed
.OrderByDescending(x => x.Size).ThenBy(x => x.ArchiveKey)
.Select(x => x.File).ToList(),
_ => keyed
.OrderBy(x => x.ArchiveKey).ThenBy(x => x.File.Path, StringComparer.OrdinalIgnoreCase)
.Select(x => x.File).ToList()
};
});
SearchResults.Clear();
SearchResults.AddRange(sorted);
}
private bool ItemFilter(object item, IEnumerable<string> filters)
{
if (item is not AssetItem assetItem)
if (item is not GameFile entry)
return true;
if (!HasRegexEnabled)
return filters.All(x => assetItem.FullPath.Contains(x, HasMatchCaseEnabled ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));
return filters.All(x => entry.Path.Contains(x, HasMatchCaseEnabled ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));
var o = RegexOptions.None;
if (HasMatchCaseEnabled) o |= RegexOptions.IgnoreCase;
return new Regex(FilterText, o).Match(assetItem.FullPath).Success;
if (!HasMatchCaseEnabled) o |= RegexOptions.IgnoreCase;
return new Regex(FilterText, o).Match(entry.Path).Success;
}
}
}

View File

@ -2,13 +2,14 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Nanite;
using CUE4Parse.UE4.Assets.Exports.Texture;
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.Framework;
using FModel.Services;
using FModel.Settings;
@ -143,6 +144,13 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _selectedLodExportFormat, value);
}
private ENaniteMeshFormat _selectedNaniteMeshExportFormat;
public ENaniteMeshFormat SelectedNaniteMeshExportFormat
{
get => _selectedNaniteMeshExportFormat;
set => SetProperty(ref _selectedNaniteMeshExportFormat, value);
}
private EMaterialFormat _selectedMaterialExportFormat;
public EMaterialFormat SelectedMaterialExportFormat
{
@ -157,6 +165,13 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _selectedTextureExportFormat, value);
}
private ulong _criwareDecryptionKey;
public ulong CriwareDecryptionKey
{
get => _criwareDecryptionKey;
set => SetProperty(ref _criwareDecryptionKey, value);
}
public bool SocketSettingsEnabled => SelectedMeshExportFormat == EMeshFormat.ActorX;
public bool CompressionSettingsEnabled => SelectedMeshExportFormat == EMeshFormat.UEFormat;
@ -170,6 +185,7 @@ public class SettingsViewModel : ViewModel
public ReadOnlyObservableCollection<ESocketFormat> SocketExportFormats { get; private set; }
public ReadOnlyObservableCollection<EFileCompressionFormat> CompressionFormats { get; private set; }
public ReadOnlyObservableCollection<ELodFormat> LodExportFormats { get; private set; }
public ReadOnlyObservableCollection<ENaniteMeshFormat> NaniteMeshExportFormats { get; private set; }
public ReadOnlyObservableCollection<EMaterialFormat> MaterialExportFormats { get; private set; }
public ReadOnlyObservableCollection<ETextureFormat> TextureExportFormats { get; private set; }
public ReadOnlyObservableCollection<ETexturePlatform> Platforms { get; private set; }
@ -193,6 +209,7 @@ public class SettingsViewModel : ViewModel
private ESocketFormat _socketExportFormatSnapshot;
private EFileCompressionFormat _compressionFormatSnapshot;
private ELodFormat _lodExportFormatSnapshot;
private ENaniteMeshFormat _naniteMeshExportFormatSnapshot;
private EMaterialFormat _materialExportFormatSnapshot;
private ETextureFormat _textureExportFormatSnapshot;
@ -217,6 +234,7 @@ public class SettingsViewModel : ViewModel
_customVersionsSnapshot = UserSettings.Default.CurrentDir.Versioning.CustomVersions;
_optionsSnapshot = UserSettings.Default.CurrentDir.Versioning.Options;
_mapStructTypesSnapshot = UserSettings.Default.CurrentDir.Versioning.MapStructTypes;
_criwareDecryptionKey = UserSettings.Default.CurrentDir.CriwareDecryptionKey;
AesEndpoint = UserSettings.Default.CurrentDir.Endpoints[0];
MappingEndpoint = UserSettings.Default.CurrentDir.Endpoints[1];
@ -233,6 +251,7 @@ public class SettingsViewModel : ViewModel
_socketExportFormatSnapshot = UserSettings.Default.SocketExportFormat;
_compressionFormatSnapshot = UserSettings.Default.CompressionFormat;
_lodExportFormatSnapshot = UserSettings.Default.LodExportFormat;
_naniteMeshExportFormatSnapshot = UserSettings.Default.NaniteMeshExportFormat;
_materialExportFormatSnapshot = UserSettings.Default.MaterialExportFormat;
_textureExportFormatSnapshot = UserSettings.Default.TextureExportFormat;
@ -248,8 +267,10 @@ public class SettingsViewModel : ViewModel
SelectedSocketExportFormat = _socketExportFormatSnapshot;
SelectedCompressionFormat = _selectedCompressionFormat;
SelectedLodExportFormat = _lodExportFormatSnapshot;
SelectedNaniteMeshExportFormat = _naniteMeshExportFormatSnapshot;
SelectedMaterialExportFormat = _materialExportFormatSnapshot;
SelectedTextureExportFormat = _textureExportFormatSnapshot;
CriwareDecryptionKey = _criwareDecryptionKey;
SelectedAesReload = UserSettings.Default.AesReload;
SelectedDiscordRpc = UserSettings.Default.DiscordRpc;
@ -263,6 +284,7 @@ public class SettingsViewModel : ViewModel
SocketExportFormats = new ReadOnlyObservableCollection<ESocketFormat>(new ObservableCollection<ESocketFormat>(EnumerateSocketExportFormat()));
CompressionFormats = new ReadOnlyObservableCollection<EFileCompressionFormat>(new ObservableCollection<EFileCompressionFormat>(EnumerateCompressionFormat()));
LodExportFormats = new ReadOnlyObservableCollection<ELodFormat>(new ObservableCollection<ELodFormat>(EnumerateLodExportFormat()));
NaniteMeshExportFormats = new ReadOnlyObservableCollection<ENaniteMeshFormat>(new ObservableCollection<ENaniteMeshFormat>(EnumerateNaniteMeshExportFormat()));
MaterialExportFormats = new ReadOnlyObservableCollection<EMaterialFormat>(new ObservableCollection<EMaterialFormat>(EnumerateMaterialExportFormat()));
TextureExportFormats = new ReadOnlyObservableCollection<ETextureFormat>(new ObservableCollection<ETextureFormat>(EnumerateTextureExportFormat()));
Platforms = new ReadOnlyObservableCollection<ETexturePlatform>(new ObservableCollection<ETexturePlatform>(EnumerateUePlatforms()));
@ -295,6 +317,7 @@ public class SettingsViewModel : ViewModel
UserSettings.Default.CurrentDir.Versioning.CustomVersions = SelectedCustomVersions;
UserSettings.Default.CurrentDir.Versioning.Options = SelectedOptions;
UserSettings.Default.CurrentDir.Versioning.MapStructTypes = SelectedMapStructTypes;
UserSettings.Default.CurrentDir.CriwareDecryptionKey = CriwareDecryptionKey;
UserSettings.Default.AssetLanguage = SelectedAssetLanguage;
UserSettings.Default.CompressedAudioMode = SelectedCompressedAudio;
@ -303,6 +326,7 @@ public class SettingsViewModel : ViewModel
UserSettings.Default.SocketExportFormat = SelectedSocketExportFormat;
UserSettings.Default.CompressionFormat = SelectedCompressionFormat;
UserSettings.Default.LodExportFormat = SelectedLodExportFormat;
UserSettings.Default.NaniteMeshExportFormat = SelectedNaniteMeshExportFormat;
UserSettings.Default.MaterialExportFormat = SelectedMaterialExportFormat;
UserSettings.Default.TextureExportFormat = SelectedTextureExportFormat;
UserSettings.Default.AesReload = SelectedAesReload;
@ -318,7 +342,7 @@ public class SettingsViewModel : ViewModel
=> Enum.GetValues<EGame>()
.GroupBy(value => (int)value)
.Select(group => group.First())
.OrderBy(value => (int)value == ((int)value & ~0xF));
.OrderBy(value => ((int)value & 0xFF) == 0);
private IEnumerable<ELanguage> EnumerateAssetLanguages() => Enum.GetValues<ELanguage>();
private IEnumerable<EAesReload> EnumerateAesReloads() => Enum.GetValues<EAesReload>();
private IEnumerable<EDiscordRpc> EnumerateDiscordRpcs() => Enum.GetValues<EDiscordRpc>();
@ -328,6 +352,7 @@ public class SettingsViewModel : ViewModel
private IEnumerable<ESocketFormat> EnumerateSocketExportFormat() => Enum.GetValues<ESocketFormat>();
private IEnumerable<EFileCompressionFormat> EnumerateCompressionFormat() => Enum.GetValues<EFileCompressionFormat>();
private IEnumerable<ELodFormat> EnumerateLodExportFormat() => Enum.GetValues<ELodFormat>();
private IEnumerable<ENaniteMeshFormat> EnumerateNaniteMeshExportFormat() => Enum.GetValues<ENaniteMeshFormat>();
private IEnumerable<EMaterialFormat> EnumerateMaterialExportFormat() => Enum.GetValues<EMaterialFormat>();
private IEnumerable<ETextureFormat> EnumerateTextureExportFormat() => Enum.GetValues<ETextureFormat>();
private IEnumerable<ETexturePlatform> EnumerateUePlatforms() => Enum.GetValues<ETexturePlatform>();

View File

@ -8,7 +8,6 @@ using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
using Serilog;
using SkiaSharp;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
@ -16,12 +15,15 @@ using System.Windows;
using System.Windows.Media.Imaging;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.Utils;
namespace FModel.ViewModels;
public class TabImage : ViewModel
{
public string ExportName { get; }
public string ExportName { get; set; }
public byte[] ImageBuffer { get; set; }
public TabImage(string name, bool rnn, SKBitmap img)
@ -31,6 +33,13 @@ public class TabImage : ViewModel
SetImage(img);
}
public TabImage(string name, bool rnn, CTexture img)
{
ExportName = name;
RenderNearestNeighbor = rnn;
SetImage(img);
}
private BitmapImage _image;
public BitmapImage Image
{
@ -70,11 +79,42 @@ public class TabImage : ViewModel
}
_bmp = bitmap;
using var data = _bmp.Encode(NoAlpha ? ETextureFormat.Jpeg : UserSettings.Default.TextureExportFormat, 100);
ExportName += "." + (NoAlpha ? "jpg" : "png");
using var data = _bmp.Encode(NoAlpha ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 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;
image.StreamSource = stream;
image.EndInit();
image.Freeze();
Image = image;
}
private void SetImage(CTexture bitmap)
{
if (bitmap is null)
{
ImageBuffer = null;
Image = null;
return;
}
_bmp = bitmap.ToSkBitmap();
byte[] imageData = _bmp.Encode(NoAlpha ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 100).ToArray();
if (PixelFormatUtils.IsHDR(bitmap.PixelFormat) || (UserSettings.Default.TextureExportFormat != ETextureFormat.Jpeg && UserSettings.Default.TextureExportFormat != ETextureFormat.Png))
{
ImageBuffer = bitmap.Encode(UserSettings.Default.TextureExportFormat, UserSettings.Default.SaveHdrTexturesAsHdr, out var ext);
ExportName += "." + ext;
}
else
{
ImageBuffer = imageData;
ExportName += "." + (NoAlpha ? "jpg" : "png");
}
using var stream = new MemoryStream(imageData);
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
@ -92,22 +132,28 @@ public class TabItem : ViewModel
{
public string ParentExportType { get; private set; }
private string _header;
public string Header
private GameFile _entry;
public GameFile Entry
{
get => _header;
set => SetProperty(ref _header, value);
get => _entry;
set
{
SetProperty(ref _entry, value);
RaisePropertyChanged(nameof(Header));
}
}
private string _directory;
public string Directory
private string _titleExtra;
public string TitleExtra
{
get => _directory;
set => SetProperty(ref _directory, value);
get => _titleExtra;
set
{
SetProperty(ref _titleExtra, value);
RaisePropertyChanged(nameof(Header));
}
}
public string FullPath => this.Directory + "/" + this.Header;
private bool _hasSearchOpen;
public bool HasSearchOpen
{
@ -202,6 +248,8 @@ public class TabItem : ViewModel
}
}
public string Header => $"{Entry.Name}{(string.IsNullOrEmpty(TitleExtra) ? "" : $" ({TitleExtra})")}";
public bool HasImage => SelectedImage != null;
public bool HasMultipleImages => _images.Count > 1;
public string Page => $"{_images.IndexOf(_selectedImage) + 1} / {_images.Count}";
@ -217,18 +265,17 @@ public class TabItem : ViewModel
private GoToCommand _goToCommand;
public GoToCommand GoToCommand => _goToCommand ??= new GoToCommand(null);
public TabItem(string header, string directory, string parentExportType)
public TabItem(GameFile entry, string parentExportType)
{
Header = header;
Directory = directory;
Entry = entry;
ParentExportType = parentExportType;
_images = new ObservableCollection<TabImage>();
}
public void SoftReset(string header, string directory)
public void SoftReset(GameFile entry)
{
Header = header;
Directory = directory;
Entry = entry;
TitleExtra = string.Empty;
ParentExportType = string.Empty;
ScrollTrigger = null;
Application.Current.Dispatcher.Invoke(() =>
@ -245,7 +292,7 @@ public class TabItem : ViewModel
public void AddImage(UTexture texture, bool save, bool updateUi)
{
var appendLayerNumber = false;
var img = new SKBitmap[1];
var img = new CTexture[1];
if (texture is UTexture2DArray textureArray)
{
img = textureArray.DecodeTextureArray(UserSettings.Default.CurrentDir.TexturePlatform);
@ -256,7 +303,7 @@ public class TabItem : ViewModel
img[0] = texture.Decode(UserSettings.Default.CurrentDir.TexturePlatform);
if (texture is UTextureCube)
{
img[0] = img[0]?.ToPanorama();
img[0] = img[0].ToPanorama();
}
}
@ -271,6 +318,29 @@ public class TabItem : ViewModel
}
}
public void AddImage(string name, bool rnn, CTexture[] 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, CTexture img, bool save, bool updateUi)
{
Application.Current.Dispatcher.Invoke(() =>
{
var t = new TabImage(name, rnn, img);
if (save) SaveImage(t, updateUi);
if (!updateUi) return;
_images.Add(t);
SelectedImage ??= t;
RaisePropertyChanged("Page");
RaisePropertyChanged("HasMultipleImages");
});
}
public void AddImage(string name, bool rnn, SKBitmap img, bool save, bool updateUi)
{
Application.Current.Dispatcher.Invoke(() =>
@ -304,23 +374,14 @@ public class TabItem : ViewModel
public void SaveImage() => SaveImage(SelectedImage, true);
private void SaveImage(TabImage image, bool updateUi)
{
if (image == null) return;
if (image == null)
return;
var ext = UserSettings.Default.TextureExportFormat switch
{
ETextureFormat.Png => ".png",
ETextureFormat.Jpeg => ".jpg",
ETextureFormat.Tga => ".tga",
_ => ".png"
};
var path = Path.Combine(UserSettings.Default.TextureDirectory, UserSettings.Default.KeepDirectoryStructure ? Entry.Directory : "", image.ExportName).Replace('\\', '/');
var fileName = image.ExportName + ext;
var path = Path.Combine(UserSettings.Default.TextureDirectory,
UserSettings.Default.KeepDirectoryStructure ? Directory : "", fileName!).Replace('\\', '/');
Directory.CreateDirectory(path.SubstringBeforeLast('/'));
System.IO.Directory.CreateDirectory(path.SubstringBeforeLast('/'));
SaveImage(image, path, fileName, updateUi);
SaveImage(image, path, image.ExportName, updateUi);
}
private void SaveImage(TabImage image, string path, string fileName, bool updateUi)
@ -337,11 +398,11 @@ public class TabItem : ViewModel
public void SaveProperty(bool updateUi)
{
var fileName = Path.ChangeExtension(Header, ".json");
var fileName = Path.ChangeExtension(Entry.Name, ".json");
var directory = Path.Combine(UserSettings.Default.PropertiesDirectory,
UserSettings.Default.KeepDirectoryStructure ? Directory : "", fileName).Replace('\\', '/');
UserSettings.Default.KeepDirectoryStructure ? Entry.Directory : "", fileName).Replace('\\', '/');
System.IO.Directory.CreateDirectory(directory.SubstringBeforeLast('/'));
Directory.CreateDirectory(directory.SubstringBeforeLast('/'));
Application.Current.Dispatcher.Invoke(() => File.WriteAllText(directory, Document.Text));
SaveCheck(directory, fileName, updateUi);
@ -390,28 +451,25 @@ public class TabControlViewModel : ViewModel
public TabControlViewModel()
{
_tabItems = new ObservableCollection<TabItem>(EnumerateTabs());
_tabItems = [];
TabsItems = new ReadOnlyObservableCollection<TabItem>(_tabItems);
SelectedTab = TabsItems.FirstOrDefault();
AddTab();
}
public void AddTab(string header = null, string directory = null, string parentExportType = null)
public void AddTab() => AddTab("New Tab");
public void AddTab(string title) => AddTab(new FakeGameFile(title));
public void AddTab(GameFile entry, 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" })
if (SelectedTab?.Header == "New Tab")
{
SelectedTab.Header = h;
SelectedTab.Directory = d;
SelectedTab.Entry = entry;
return;
}
if (!CanAddTabs) return;
Application.Current.Dispatcher.Invoke(() =>
{
_tabItems.Add(new TabItem(h, d, p));
_tabItems.Add(new TabItem(entry, parentExportType ?? string.Empty));
SelectedTab = _tabItems.Last();
});
}
@ -470,9 +528,4 @@ public class TabControlViewModel : ViewModel
_tabItems.Clear();
});
}
private static IEnumerable<TabItem> EnumerateTabs()
{
yield return new TabItem("New Tab", string.Empty, string.Empty);
}
}

View File

@ -40,10 +40,6 @@ public class ThreadWorkerViewModel : ViewModel
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
private readonly AsyncQueue<Action<CancellationToken>> _jobs;
private const string _at = " at ";
private const char _dot = '.';
private const char _colon = ':';
private const string _gray = "#999";
public ThreadWorkerViewModel()
{
@ -104,37 +100,7 @@ public class ThreadWorkerViewModel : ViewModel
CurrentCancellationTokenSource = null; // kill token
Log.Error("{Exception}", e);
FLogger.Append(ELog.Error, () =>
{
if ((e.InnerException ?? e) is { TargetSite.DeclaringType: not null } exception)
{
if (exception.TargetSite.ToString() == "CUE4Parse.FileProvider.GameFile get_Item(System.String)")
{
FLogger.Text(e.Message, Constants.WHITE, true);
}
else
{
var t = exception.GetType();
FLogger.Text(t.Namespace + _dot, Constants.GRAY);
FLogger.Text(t.Name, Constants.WHITE);
FLogger.Text(_colon + " ", Constants.GRAY);
FLogger.Text(exception.Message, Constants.RED, true);
FLogger.Text(_at, _gray);
FLogger.Text(exception.TargetSite.DeclaringType.FullName + _dot, Constants.GRAY);
FLogger.Text(exception.TargetSite.Name, Constants.YELLOW);
var p = exception.TargetSite.GetParameters();
var parameters = new string[p.Length];
for (int i = 0; i < parameters.Length; i++)
{
parameters[i] = p[i].ParameterType.Name + " " + p[i].Name;
}
FLogger.Text("(" + string.Join(", ", parameters) + ")", Constants.GRAY, true);
}
}
});
FLogger.Append(e);
return;
}
}

View File

@ -1,9 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Data;
using FModel.Extensions;
using CUE4Parse.Utils;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
@ -13,7 +15,7 @@ using FModel.Views.Resources.Converters;
namespace FModel.ViewModels;
public class UpdateViewModel : ViewModel
public partial class UpdateViewModel : ViewModel
{
private ApiEndpointViewModel _apiEndpointView => ApplicationService.ApiEndpointView;
@ -25,7 +27,7 @@ public class UpdateViewModel : ViewModel
public UpdateViewModel()
{
Commits = new RangeObservableCollection<GitHubCommit>();
Commits = [];
CommitsView = new ListCollectionView(Commits)
{
GroupDescriptions = { new PropertyGroupDescription("Commit.Author.Date", new DateTimeToDateConverter()) }
@ -35,40 +37,121 @@ public class UpdateViewModel : ViewModel
RemindMeCommand.Execute(this, null);
}
public async Task Load()
public async Task LoadAsync()
{
Commits.AddRange(await _apiEndpointView.GitHubApi.GetCommitHistoryAsync());
var commits = await _apiEndpointView.GitHubApi.GetCommitHistoryAsync();
if (commits == null || commits.Length == 0)
return;
var qa = await _apiEndpointView.GitHubApi.GetReleaseAsync("qa");
qa.Assets.OrderByDescending(x => x.CreatedAt).First().IsLatest = true;
Commits.AddRange(commits);
foreach (var asset in qa.Assets)
try
{
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
});
}
_ = LoadCoAuthors();
_ = LoadAssets();
}
catch
{
//
}
}
private Task LoadCoAuthors()
{
return Task.Run(async () =>
{
var coAuthorMap = new Dictionary<GitHubCommit, HashSet<string>>();
foreach (var commit in Commits)
{
if (!commit.Commit.Message.Contains("Co-authored-by"))
continue;
var regex = GetCoAuthorRegex();
var matches = regex.Matches(commit.Commit.Message);
if (matches.Count == 0) continue;
commit.Commit.Message = regex.Replace(commit.Commit.Message, string.Empty).Trim();
coAuthorMap[commit] = [];
foreach (Match match in matches)
{
if (match.Groups.Count < 3) continue;
coAuthorMap[commit].Add(match.Groups[1].Value);
}
}
if (coAuthorMap.Count == 0) return;
var uniqueUsernames = coAuthorMap.Values.SelectMany(x => x).Distinct().ToArray();
var authorCache = new Dictionary<string, Author>();
foreach (var username in uniqueUsernames)
{
try
{
var author = await _apiEndpointView.GitHubApi.GetUserAsync(username);
if (author != null)
authorCache[username] = author;
}
catch
{
//
}
}
foreach (var (commit, usernames) in coAuthorMap)
{
var coAuthors = usernames
.Where(username => authorCache.ContainsKey(username))
.Select(username => authorCache[username])
.ToArray();
if (coAuthors.Length > 0)
commit.CoAuthors = coAuthors;
}
});
}
private Task LoadAssets()
{
return Task.Run(async () =>
{
var qa = await _apiEndpointView.GitHubApi.GetReleaseAsync("qa");
var assets = qa.Assets.OrderByDescending(x => x.CreatedAt).ToList();
for (var i = 0; i < assets.Count; i++)
{
var asset = assets[i];
asset.IsLatest = i == 0;
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();
Commits.FirstOrDefault(x => x.IsDownloadable && x.Asset.IsLatest)?.Download();
}
[GeneratedRegex(@"Co-authored-by:\s*(.+?)\s*<(.+?)>", RegexOptions.IgnoreCase | RegexOptions.Multiline, "en-US")]
private static partial Regex GetCoAuthorRegex();
}

View File

@ -11,13 +11,6 @@
<Setter Property="Title" Value="About" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<StackPanel Margin="30 10">
<StackPanel HorizontalAlignment="Center" Margin="0 0 0 30">
<TextBlock Text="{Binding Source={x:Static local:Constants.APP_VERSION}, StringFormat={}FModel {0}}" FontSize="15" FontWeight="500" Foreground="#9DA3DD" FontStretch="Expanded" />

View File

@ -14,13 +14,6 @@
<Setter Property="Title" Value="AES Manager" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>

View File

@ -16,13 +16,6 @@
<Setter Property="Title" Value="Audio Player" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="350" />

View File

@ -12,13 +12,6 @@
<Setter Property="Title" Value="Backup Manager" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid Column="2" adonisExtensions:LayerExtension.Layer="2" Margin="10" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />

View File

@ -14,13 +14,6 @@
<Setter Property="Title" Value="Directory Selector" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>

View File

@ -14,14 +14,6 @@
<Setter Property="Title" Value="Image Merger"/>
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>

View File

@ -0,0 +1,35 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="NeutralBrush" Color="White" />
<SolidColorBrush x:Key="BlueprintBrush" Color="DodgerBlue" />
<SolidColorBrush x:Key="BlueprintWidgetBrush" Color="DarkViolet" />
<SolidColorBrush x:Key="BlueprintAnimBrush" Color="Crimson" />
<SolidColorBrush x:Key="BlueprintRigVMBrush" Color="Teal" />
<SolidColorBrush x:Key="CookedMetaDataBrush" Color="Yellow" />
<SolidColorBrush x:Key="UserDefinedEnumBrush" Color="DarkGoldenrod" />
<SolidColorBrush x:Key="UserDefinedStructBrush" Color="Tan" />
<SolidColorBrush x:Key="MaterialBrush" Color="BurlyWood" />
<SolidColorBrush x:Key="MaterialEditorBrush" Color="Yellow" />
<SolidColorBrush x:Key="BinaryBrush" Color="Yellow" />
<SolidColorBrush x:Key="TextureBrush" Color="MediumPurple" />
<SolidColorBrush x:Key="ConfigBrush" Color="LightSlateGray" />
<SolidColorBrush x:Key="AudioBrush" Color="MediumSeaGreen" />
<SolidColorBrush x:Key="VideoBrush" Color="IndianRed" />
<SolidColorBrush x:Key="DataTableBrush" Color="SteelBlue" />
<SolidColorBrush x:Key="CurveBrush" Color="HotPink" />
<SolidColorBrush x:Key="PluginBrush" Color="GreenYellow" />
<SolidColorBrush x:Key="ProjectBrush" Color="DeepSkyBlue" />
<SolidColorBrush x:Key="LocalizationBrush" Color="CornflowerBlue" />
<SolidColorBrush x:Key="WorldBrush" Color="Orange" />
<SolidColorBrush x:Key="BuildDataBrush" Color="Tomato" />
<SolidColorBrush x:Key="LevelSequenceBrush" Color="Coral" />
<SolidColorBrush x:Key="FoliageBrush" Color="ForestGreen" />
<SolidColorBrush x:Key="ParticleBrush" Color="Gold" />
<SolidColorBrush x:Key="AnimationBrush" Color="Coral" />
<SolidColorBrush x:Key="LuaBrush" Color="DarkBlue" />
<SolidColorBrush x:Key="JsonXmlBrush" Color="LightGreen" />
</ResourceDictionary>

View File

@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Services;
using FModel.ViewModels;
@ -69,31 +70,24 @@ public class GamePathVisualLineText : VisualLineText
{
var obj = gamePath.SubstringAfterLast('.');
var package = gamePath.SubstringBeforeLast('.');
var fullPath = _applicationView.CUE4Parse.Provider.FixPath(package, StringComparison.Ordinal);
if (a.ParentVisualLine.Document.FileName.Equals(fullPath.SubstringBeforeLast('.'), StringComparison.OrdinalIgnoreCase))
{
int lineNumber;
DocumentLine line;
var fullPath = _applicationView.CUE4Parse.Provider.FixPath(package);
if (Regex.IsMatch(obj, @"^(.+)\[(\d+)\]$"))
{
lineNumber = a.ParentVisualLine.Document.Text.GetKismetLineNumber(obj);
line = a.ParentVisualLine.Document.GetLineByNumber(lineNumber);
}
else
{
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
var firstLine = a.ParentVisualLine.Document.GetLineByNumber(2);
if (a.ParentVisualLine.Document.FileName.Equals(fullPath.SubstringBeforeLast('.'), StringComparison.OrdinalIgnoreCase) &&
!a.ParentVisualLine.Document.GetText(firstLine.Offset, firstLine.Length).Equals(" \"Summary\": {")) // Show Metadata case
{
await _threadWorkerView.Begin(cancellationToken =>
_applicationView.CUE4Parse.ExtractAndScroll(cancellationToken, fullPath, obj, parentExportType));
var lineNumber = a.ParentVisualLine.Document.Text.GetNameLineNumber(obj);
if (lineNumber > -1)
{
var line = a.ParentVisualLine.Document.GetLineByNumber(lineNumber);
AvalonEditor.YesWeEditor.Select(line.Offset, line.Length);
AvalonEditor.YesWeEditor.ScrollToLine(lineNumber);
return;
}
}
await _threadWorkerView.Begin(cancellationToken =>
_applicationView.CUE4Parse.ExtractAndScroll(cancellationToken, fullPath, obj, parentExportType));
};
return a;
}

View File

@ -0,0 +1,34 @@
using System.Text.RegularExpressions;
using ICSharpCode.AvalonEdit.Rendering;
namespace FModel.Views.Resources.Controls;
public class JumpElementGenerator : VisualLineElementGenerator
{
private readonly Regex _JumpRegex = new(
@"\b(?:goto\s+Label_(?'target'\d+);)",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
private Match FindMatch(int startOffset)
{
var endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset;
var relevantText = CurrentContext.Document.GetText(startOffset, endOffset - startOffset);
return _JumpRegex.Match(relevantText);
}
public override int GetFirstInterestedOffset(int startOffset)
{
var m = FindMatch(startOffset);
return m.Success ? startOffset + m.Index : -1;
}
public override VisualLineElement ConstructElement(int offset)
{
var m = FindMatch(offset);
if (!m.Success || m.Index != 0 ||
!m.Groups.TryGetValue("target", out var g))
return null;
return new JumpVisualLineText(g.Value, CurrentContext.VisualLine, g.Length + g.Index + 1);
}
}

View File

@ -0,0 +1,75 @@
using System;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using FModel.Extensions;
using FModel.Services;
using FModel.ViewModels;
using ICSharpCode.AvalonEdit.Rendering;
namespace FModel.Views.Resources.Controls;
public class JumpVisualLineText : VisualLineText
{
public delegate void JumpOnClick(string Jump);
public event JumpOnClick OnJumpClicked;
private readonly string _jump;
public JumpVisualLineText(string jump, VisualLine parentVisualLine, int length) : base(parentVisualLine, length)
{
_jump = jump;
}
public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
var relativeOffset = startVisualColumn - VisualColumn;
var text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset, DocumentLength - relativeOffset);
if (text.Count != 2) // ": "
TextRunProperties.SetForegroundBrush(Brushes.Plum);
return new TextCharacters(text.Text, text.Offset, text.Count, TextRunProperties);
}
private bool JumpIsClickable() => !string.IsNullOrEmpty(_jump) && Keyboard.Modifiers == ModifierKeys.None;
protected override void OnQueryCursor(QueryCursorEventArgs e)
{
if (!JumpIsClickable())
return;
e.Handled = true;
e.Cursor = Cursors.Hand;
}
protected override void OnMouseDown(MouseButtonEventArgs e)
{
if (e.ChangedButton != MouseButton.Left || !JumpIsClickable())
return;
if (e.Handled || OnJumpClicked == null)
return;
OnJumpClicked(_jump);
e.Handled = true;
}
protected override VisualLineText CreateInstance(int length)
{
var a = new JumpVisualLineText(_jump, ParentVisualLine, length);
a.OnJumpClicked += jump =>
{
var lineNumber = a.ParentVisualLine.Document.Text.GetNameLineNumberText($" Label_{jump}:"); // impossible for different indentation
if (lineNumber > -1)
{
var line = a.ParentVisualLine.Document.GetLineByNumber(lineNumber);
AvalonEditor.YesWeEditor.Select(line.Offset, line.Length);
AvalonEditor.YesWeEditor.ScrollToLine(lineNumber);
}
};
return a;
}
}

View File

@ -5,13 +5,6 @@
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI"
PreviewKeyDown="OnPreviewKeyDown">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />

View File

@ -43,6 +43,7 @@ public partial class AvalonEditor
MyAvalonEditor.TextArea.TextView.LinkTextBackgroundBrush = null;
MyAvalonEditor.TextArea.TextView.LinkTextForegroundBrush = Brushes.Cornsilk;
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new GamePathElementGenerator());
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new JumpElementGenerator());
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new HexColorElementGenerator());
ApplicationService.ApplicationView.CUE4Parse.TabControl.OnTabRemove += OnTabClose;
@ -119,7 +120,7 @@ public partial class AvalonEditor
if (sender is not TextEditor avalonEditor || DataContext is not TabItem tabItem ||
avalonEditor.Document == null || string.IsNullOrEmpty(avalonEditor.Document.Text))
return;
avalonEditor.Document.FileName = tabItem.Directory + '/' + StringExtensions.SubstringBeforeLast(tabItem.Header, '.');
avalonEditor.Document.FileName = tabItem.Entry.PathWithoutExtension;
if (!_savedCarets.ContainsKey(avalonEditor.Document.FileName))
_ignoreCaret = true;
@ -127,6 +128,8 @@ public partial class AvalonEditor
if (!tabItem.ShouldScroll) return;
var lineNumber = avalonEditor.Document.Text.GetNameLineNumber(tabItem.ScrollTrigger);
if (lineNumber == -1) lineNumber = 1;
var line = avalonEditor.Document.GetLineByNumber(lineNumber);
avalonEditor.Select(line.Offset, line.Length);
avalonEditor.ScrollToLine(lineNumber);

View File

@ -2,12 +2,5 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContextChanged="OnDataContextChanged">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel x:Name="InMeDaddy" Orientation="Horizontal" HorizontalAlignment="Right" Height="24" />
</UserControl>

View File

@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
@ -10,7 +10,7 @@ namespace FModel.Views.Resources.Controls;
public partial class Breadcrumb
{
private const string _NAVIGATE_NEXT = "M9.31 6.71c-.39.39-.39 1.02 0 1.41L13.19 12l-3.88 3.88c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0l4.59-4.59c.39-.39.39-1.02 0-1.41L10.72 6.7c-.38-.38-1.02-.38-1.41.01z";
private const string NavigateNext = "M9.31 6.71c-.39.39-.39 1.02 0 1.41L13.19 12l-3.88 3.88c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0l4.59-4.59c.39-.39.39-1.02 0-1.41L10.72 6.7c-.38-.38-1.02-.38-1.41.01z";
public Breadcrumb()
{
@ -25,17 +25,27 @@ public partial class Breadcrumb
var folders = pathAtThisPoint.Split('/');
for (var i = 0; i < folders.Length; i++)
{
var textBlock = new TextBlock
var border = new Border
{
Text = folders[i],
BorderThickness = new Thickness(1),
BorderBrush = Brushes.Transparent,
Background = Brushes.Transparent,
Padding = new Thickness(6, 3, 6, 3),
Cursor = Cursors.Hand,
Tag = i + 1,
Margin = new Thickness(0, 3, 0, 0)
IsEnabled = i < folders.Length - 1,
Child = new TextBlock
{
Text = folders[i],
VerticalAlignment = VerticalAlignment.Center
}
};
textBlock.MouseUp += OnMouseClick;
InMeDaddy.Children.Add(textBlock);
border.MouseEnter += OnMouseEnter;
border.MouseLeave += OnMouseLeave;
border.MouseUp += OnMouseClick;
InMeDaddy.Children.Add(border);
if (i >= folders.Length - 1) continue;
InMeDaddy.Children.Add(new Viewbox
@ -52,7 +62,8 @@ public partial class Breadcrumb
new Path
{
Fill = Brushes.White,
Data = Geometry.Parse(_NAVIGATE_NEXT)
Data = Geometry.Parse(NavigateNext),
Opacity = 0.6
}
}
}
@ -60,13 +71,31 @@ public partial class Breadcrumb
}
}
private void OnMouseEnter(object sender, MouseEventArgs e)
{
if (sender is Border border)
{
border.BorderBrush = new SolidColorBrush(Color.FromRgb(127, 127, 144));
border.Background = new SolidColorBrush(Color.FromRgb(72, 73, 92));
}
}
private void OnMouseLeave(object sender, MouseEventArgs e)
{
if (sender is Border border)
{
border.BorderBrush = Brushes.Transparent;
border.Background = Brushes.Transparent;
}
}
private void OnMouseClick(object sender, MouseButtonEventArgs e)
{
if (sender is not TextBlock { DataContext: string pathAtThisPoint, Tag: int index }) return;
if (sender is not Border { DataContext: string pathAtThisPoint, Tag: int index }) return;
var directory = string.Join('/', pathAtThisPoint.Split('/').Take(index));
if (pathAtThisPoint.Equals(directory)) return;
ApplicationService.ApplicationView.CustomDirectories.GoToCommand.JumpTo(directory);
}
}
}

View File

@ -4,13 +4,6 @@
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">
@ -42,22 +35,33 @@
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="Auto"/>
<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>
<ItemsControl Grid.Column="0" ItemsSource="{Binding Authors}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Width="16" Height="16" Margin="0,0,2,0">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding AvatarUrl}" />
</Ellipse.Fill>
</Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Grid.Column="2" FontSize="11">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} committed {1}">
<Binding Path="Author.Login" />
<Binding Path="AuthorNames" />
<Binding Path="Commit.Author.Date" Converter="{x:Static converters:RelativeDateTimeConverter.Instance}" />
</MultiBinding>
</TextBlock.Text>
@ -65,7 +69,7 @@
</Grid>
</Grid>
<Grid Grid.Column="1">
<Grid Grid.Column="1" MaxHeight="96">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="15" />
@ -128,4 +132,3 @@
</Grid>
</Border>
</UserControl>

View File

@ -3,13 +3,6 @@
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" />

View File

@ -0,0 +1,305 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:settings="clr-namespace:FModel.Settings"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters">
<ContextMenu x:Key="FileContextMenu" x:Shared="False"
DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType=Window}}">
<MenuItem Header="Extract in New Tab" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Extract_New_Tab" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<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>
<MenuItem Header="Show Metadata" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Show_Metadata" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemIsUePackageCondition />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource InfoIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Find References" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Show_References" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemIsUePackageCondition />
<converters:ItemIsIoStoreCondition />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Decompile Blueprint"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Decompile" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemCategoryCondition Category="Blueprint" />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource CppIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
<MenuItem.Style>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource {x:Type MenuItem}}">
<Style.Triggers>
<DataTrigger Binding="{Binding ShowDecompileOption, Source={x:Static settings:UserSettings.Default}}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
<Separator />
<MenuItem Command="{Binding RightClickMenuCommand}">
<MenuItem.Header>
<TextBlock
Text="{Binding PlacementTarget.SelectedItem.Asset.Extension,
FallbackValue='Export Raw Data',
StringFormat='Export Raw Data (.{0})',
RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
</MenuItem.Header>
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Export_Data" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ExportIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Properties (.json)" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Properties" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SaveIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Texture" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Textures" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemCategoryCondition Category="Texture" />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource TextureIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Model" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Models" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemCategoryCondition Category="Mesh" />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ModelIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Animation" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Animations" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemCategoryCondition Category="Animation" />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AnimationIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Audio" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Audio" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemCategoryCondition Category="Audio" />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Copy">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource CopyIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
<MenuItem Header="Package Path" Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Path" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Name" Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Name" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Directory Path" Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Directory_Path" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Path w/o Extension" Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Path_No_Extension" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Name w/o Extension" Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Name_No_Extension" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
</MenuItem>
</ContextMenu>
</ResourceDictionary>

View File

@ -0,0 +1,150 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
x:Class="FModel.Views.Resources.Controls.ContextMenus.FolderContextMenuDictionary">
<ContextMenu x:Key="FolderContextMenu" x:Shared="False"
Opened="FolderContextMenu_OnOpened">
<MenuItem Header="Export Folder's Packages Raw Data (.uasset)"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Export_Data" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
Data="{StaticResource ExportIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Properties (.json)"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Save_Properties" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
Data="{StaticResource SaveIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Textures"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Save_Textures" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
Data="{StaticResource TextureIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Models"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Save_Models" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
Data="{StaticResource ModelIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Animations"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Save_Animations" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
Data="{StaticResource AnimationIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Audio"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Save_Audio" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Favorite Directory" Click="OnFavoriteDirectoryClick"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoriesAddIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy Directory Path" Click="OnCopyDirectoryPathClick"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource CopyIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ResourceDictionary>

View File

@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels;
namespace FModel.Views.Resources.Controls.ContextMenus;
public partial class FolderContextMenuDictionary
{
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
public FolderContextMenuDictionary()
{
InitializeComponent();
}
private void FolderContextMenu_OnOpened(object sender, RoutedEventArgs e)
{
if (sender is not ContextMenu { PlacementTarget: FrameworkElement fe } menu)
return;
var listBox = FindAncestor<ListBox>(fe);
if (listBox != null)
{
menu.DataContext = listBox.DataContext;
menu.Tag = listBox.SelectedItems;
return;
}
var treeView = FindAncestor<TreeView>(fe);
if (treeView != null)
{
menu.DataContext = treeView.DataContext;
menu.Tag = new[] { treeView.SelectedItem }.ToList();
}
}
private static T FindAncestor<T>(DependencyObject current) where T : DependencyObject
{
while (current != null)
{
if (current is T t)
return t;
current = VisualTreeHelper.GetParent(current);
}
return null;
}
private void OnFavoriteDirectoryClick(object sender, RoutedEventArgs e)
{
if (sender is not MenuItem { CommandParameter: IEnumerable<object> list } || list.FirstOrDefault() 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 favorite directory", Constants.WHITE, true));
}
private void OnCopyDirectoryPathClick(object sender, RoutedEventArgs e)
{
if (sender is not MenuItem { CommandParameter: IEnumerable<object> list } || list.FirstOrDefault() is not TreeItem folder)
return;
Clipboard.SetText(folder.PathAtThisPoint);
}
}

View File

@ -9,13 +9,6 @@
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" ResizeMode="NoResize" SizeToContent="Width"
MinWidth="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.30'}"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.20'}">
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>

View File

@ -9,13 +9,6 @@
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" ResizeMode="NoResize"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.50'}"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.35'}">
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>

View File

@ -4,18 +4,13 @@
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed">
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<DockPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<controls:MagnifierManager.Magnifier>
<controls:Magnifier Radius="150" ZoomFactor=".7" />
</controls:MagnifierManager.Magnifier>
<Image x:Name="ImageCtrl" UseLayoutRounding="True" />
<Border BorderBrush="#3b3d4a" BorderThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center">
<Image x:Name="ImageCtrl" UseLayoutRounding="True" />
</Border>
</DockPanel>
</adonisControls:AdonisWindow>

View File

@ -0,0 +1,41 @@
<UserControl x:Class="FModel.Views.Resources.Controls.Inputs.SearchTextBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignWidth="300"
x:Name="Root"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" ZIndex="1" HorizontalAlignment="Left" Margin="5 2 0 0">
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</Grid>
<TextBox x:Name="TextBox" Grid.Column="0" Grid.ColumnSpan="2" AcceptsTab="False" AcceptsReturn="False"
Text="{Binding Text, ElementName=Root, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Padding="25 0 0 0" HorizontalAlignment="Stretch"
adonisExtensions:WatermarkExtension.Watermark="{Binding Watermark, ElementName=Root}" />
<Button Grid.Column="1" ToolTip="Clear Search Filter" Padding="5"
Click="OnClearButtonClick"
Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarButton}}">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
Data="{StaticResource BackspaceIcon}"/>
</Canvas>
</Viewbox>
</Button>
</Grid>
</UserControl>

View File

@ -0,0 +1,48 @@
using System.Windows;
using System.Windows.Controls;
namespace FModel.Views.Resources.Controls.Inputs;
public partial class SearchTextBox : UserControl
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(SearchTextBox),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty WatermarkProperty =
DependencyProperty.Register(nameof(Watermark), typeof(string), typeof(SearchTextBox),
new PropertyMetadata("Search by name..."));
public static readonly RoutedEvent ClearButtonClickEvent =
EventManager.RegisterRoutedEvent(nameof(ClearButtonClick), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(SearchTextBox));
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public string Watermark
{
get => (string)GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
public event RoutedEventHandler ClearButtonClick
{
add => AddHandler(ClearButtonClickEvent, value);
remove => RemoveHandler(ClearButtonClickEvent, value);
}
public SearchTextBox()
{
InitializeComponent();
}
private void OnClearButtonClick(object sender, RoutedEventArgs e)
{
Text = string.Empty;
RaiseEvent(new RoutedEventArgs(ClearButtonClickEvent, this));
}
}

View File

@ -0,0 +1,42 @@
using System.Windows;
using System.Windows.Controls;
namespace FModel.Views.Resources.Controls;
public sealed class ListBoxItemBehavior
{
public static bool GetIsBroughtIntoViewWhenSelected(ListBoxItem listBoxItem)
{
return (bool) listBoxItem.GetValue(IsBroughtIntoViewWhenSelectedProperty);
}
public static void SetIsBroughtIntoViewWhenSelected(ListBoxItem listBoxItem, bool value)
{
listBoxItem.SetValue(IsBroughtIntoViewWhenSelectedProperty, value);
}
public static readonly DependencyProperty IsBroughtIntoViewWhenSelectedProperty =
DependencyProperty.RegisterAttached("IsBroughtIntoViewWhenSelected", typeof(bool), typeof(ListBoxItemBehavior),
new UIPropertyMetadata(false, OnIsBroughtIntoViewWhenSelectedChanged));
private static void OnIsBroughtIntoViewWhenSelectedChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
if (depObj is not ListBoxItem item)
return;
if (e.NewValue is not bool value)
return;
if (value)
item.Selected += OnListBoxItemSelected;
else
item.Selected -= OnListBoxItemSelected;
}
private static void OnListBoxItemSelected(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is ListBoxItem item)
item.BringIntoView();
}
}

View File

@ -7,13 +7,6 @@
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" PreviewKeyDown="OnPreviewKeyDown"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.40'}">
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<avalonEdit:TextEditor x:Name="MyAvalonEditor" Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
FontFamily="Consolas" FontSize="8pt" IsReadOnly="True" ShowLineNumbers="True" Foreground="#DAE5F2"

View File

@ -1,8 +1,8 @@
using System;
using System;
using System.Text.RegularExpressions;
using System.Windows.Input;
using System.Windows.Media;
using FModel.Extensions;
using CUE4Parse.Utils;
using FModel.ViewModels;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
@ -24,7 +24,7 @@ public partial class PropertiesPopout
MyAvalonEditor.Document = new TextDocument
{
Text = contextViewModel.Document.Text,
FileName = contextViewModel.Directory + '/' + contextViewModel.Header.SubstringBeforeLast('.')
FileName = contextViewModel.Entry.PathWithoutExtension
};
MyAvalonEditor.FontSize = contextViewModel.FontSize;
MyAvalonEditor.SyntaxHighlighting = contextViewModel.Highlighter;
@ -32,6 +32,7 @@ public partial class PropertiesPopout
MyAvalonEditor.TextArea.TextView.LinkTextBackgroundBrush = null;
MyAvalonEditor.TextArea.TextView.LinkTextForegroundBrush = Brushes.Cornsilk;
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new GamePathElementGenerator());
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new JumpElementGenerator());
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new HexColorElementGenerator());
_manager = new JsonFoldingStrategies(MyAvalonEditor);
_manager.UpdateFoldings(MyAvalonEditor.Document);

View File

@ -6,6 +6,7 @@ using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace FModel.Views.Resources.Controls;
@ -33,6 +34,11 @@ public class FLogger : ITextFormatter
private static readonly BrushConverter _brushConverter = new();
private static int _previous;
private const string _at = " at ";
private const char _dot = '.';
private const char _colon = ':';
private const string _gray = "#999";
public static void Append(ELog type, Action job)
{
Application.Current.Dispatcher.Invoke(delegate
@ -54,6 +60,45 @@ public class FLogger : ITextFormatter
}
job();
}, DispatcherPriority.Background);
}
public static void Append(Exception e)
{
Append(ELog.Error, () =>
{
if ((e.InnerException ?? e) is { TargetSite.DeclaringType: not null } exception)
{
if (exception.TargetSite.ToString() == "CUE4Parse.FileProvider.GameFile get_Item(System.String)")
{
Text(e.Message, Constants.WHITE, true);
}
else
{
var t = exception.GetType();
Text(t.Namespace + _dot, Constants.GRAY);
Text(t.Name, Constants.WHITE);
Text(_colon + " ", Constants.GRAY);
Text(exception.Message, Constants.RED, true);
Text(_at, _gray);
Text(exception.TargetSite.DeclaringType.FullName + _dot, Constants.GRAY);
Text(exception.TargetSite.Name, Constants.YELLOW);
var p = exception.TargetSite.GetParameters();
var parameters = new string[p.Length];
for (int i = 0; i < parameters.Length; i++)
{
parameters[i] = p[i].ParameterType.Name + " " + p[i].Name;
}
Text("(" + string.Join(", ", parameters) + ")", Constants.GRAY, true);
}
}
else
{
Text(e.Message, Constants.WHITE, true);
}
});
}

View File

@ -0,0 +1,81 @@
<UserControl x:Class="FModel.Views.Resources.Controls.TiledExplorer.FileButton2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignWidth="128" d:DesignHeight="192"
d:DataContext="{d:DesignInstance Type=vm:GameFileViewModel, IsDesignTimeCreatable=False}"
xmlns:vm="clr-namespace:FModel.ViewModels"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
Width="128" Height="192"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer4BackgroundBrush}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="2" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Height="128" Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BorderBrush}}">
<Image Stretch="Uniform" Source="{Binding PreviewImage}" />
<Path x:Name="FallbackIcon" Width="64" Stretch="Uniform">
<Path.Style>
<Style TargetType="{x:Type Path}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding PreviewImage}" Value="{x:Null}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
<Path.Data>
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" />
<Binding Path="ResolvedAssetType" />
</MultiBinding>
</Path.Data>
<Path.Fill>
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" />
<Binding Path="ResolvedAssetType" />
</MultiBinding>
</Path.Fill>
</Path>
</Grid>
<Rectangle Grid.Row="1" Fill="{Binding Fill, ElementName=FallbackIcon, FallbackValue=Red}" />
<Grid Grid.Row="2" Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Asset.NameWithoutExtension, FallbackValue=Asset Name}"
FontSize="13" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" FontWeight="DemiBold"
TextAlignment="Left" HorizontalAlignment="Left" VerticalAlignment="Top"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"/>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding ResolvedAssetType, FallbackValue=Asset Type}"
FontSize="9" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" FontWeight="Normal"
TextAlignment="Left" HorizontalAlignment="Left" VerticalAlignment="Bottom"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}" />
<TextBlock Grid.Column="2" Text="{Binding Asset.Size, Converter={x:Static converters:SizeToStringConverter.Instance}, FallbackValue=0 B}"
FontSize="9" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" FontWeight="Normal"
TextAlignment="Right" HorizontalAlignment="Right" VerticalAlignment="Bottom"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}" />
</Grid>
</Grid>
</Grid>
</UserControl>

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