├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── question.md └── workflows │ └── build.yml ├── .gitignore ├── FreeMote.Plugins.x64 ├── FreeMote.Plugins.x64.csproj ├── Images │ └── TlgFormatter.cs ├── Properties │ └── AssemblyInfo.cs └── Shells │ └── MflShell.cs ├── FreeMote.Plugins ├── Audio │ ├── At9Formatter.cs │ ├── NxAdpcmFormatter.cs │ ├── OpusFormatter.cs │ ├── VagFile.cs │ ├── VagFormatter.cs │ └── XwmaFormatter.cs ├── FreeMote.Plugins.csproj ├── FreeMote.Plugins.csproj.DotSettings ├── Images │ ├── AstcFormatter.cs │ └── Bc7Formatter.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx └── Shells │ ├── Lz4Shell.cs │ ├── MdfShell.cs │ ├── MxbShell.cs │ ├── MzsShell.cs │ ├── PsdShell.cs │ ├── PspShell.cs │ └── PszShell.cs ├── FreeMote.PsBuild ├── Converters │ ├── Common2KrkrConverter.cs │ ├── CommonWinConverter.cs │ ├── ISpecConverter.cs │ └── Krkr2CommonConverter.cs ├── FreeMote.PsBuild.csproj ├── MmoBuilder.cs ├── MmoCompiler.cs ├── MmoTypes.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── PsBuildHelper.cs ├── PsbCompiler.cs ├── PsbDecompiler.cs ├── PsbJsonConverter.cs ├── PsbResourceJson.cs └── PsbSpecConverter.cs ├── FreeMote.Psb ├── EmtPainter.cs ├── FreeMote.Psb.csproj ├── FreeMote.Psb.csproj.DotSettings ├── IPsbType.cs ├── IResourceMetadata.cs ├── MtnPainter.cs ├── Plugins │ ├── AudioFileFormatter.cs │ ├── FreeMount.cs │ ├── FreeMountContext.cs │ ├── IPsbAudioFormatter.cs │ ├── IPsbImageFormatter.cs │ ├── IPsbKeyProvider.cs │ ├── IPsbPluginInfo.cs │ ├── IPsbShell.cs │ ├── IPsbSpecialType.cs │ ├── ManagedTlgFormatter.cs │ └── WavFormatter.cs ├── Properties │ └── AssemblyInfo.cs ├── Psb.cs ├── PsbExtension.cs ├── PsbResHelper.cs ├── PsbValues.cs ├── Resources │ ├── ArchDatas.cs │ ├── AudioMetadata.cs │ ├── FlattenArrayMetadata.cs │ ├── IArchData.cs │ └── ImageMetadata.cs ├── SprPainter.cs ├── Textures │ ├── TextureCombiner.cs │ ├── TexturePacker.cs │ └── TextureSpliter.cs └── Types │ ├── ArchiveType.cs │ ├── BaseImageType.cs │ ├── FontType.cs │ ├── ImageType.cs │ ├── M2Types.cs │ ├── MapType.cs │ ├── MmoType.cs │ ├── MotionType.cs │ ├── PimgType.cs │ ├── ScnType.cs │ └── SoundArchiveType.cs ├── FreeMote.Purify └── readme.md ├── FreeMote.Tests ├── App.config ├── FreeMote.Tests.csproj ├── FreeMoteTest.cs ├── MmoTest.cs ├── Properties │ └── AssemblyInfo.cs ├── PsBuildTest.cs └── PsbTest.cs ├── FreeMote.Tools.EmtConvert ├── App.config ├── FreeMote.Tools.EmtConvert.csproj ├── Program.cs └── Properties │ └── AssemblyInfo.cs ├── FreeMote.Tools.EmtMake ├── App.config ├── FreeMote.Tools.EmtMake.csproj ├── Program.cs └── Properties │ └── AssemblyInfo.cs ├── FreeMote.Tools.PsBuild ├── App.config ├── FreeMote.Tools.PsBuild.csproj ├── Program.cs └── Properties │ └── AssemblyInfo.cs ├── FreeMote.Tools.PsbDecompile ├── App.config ├── FreeMote.Tools.PsbDecompile.csproj ├── Program.cs └── Properties │ └── AssemblyInfo.cs ├── FreeMote.Tools.Viewer ├── App.xaml ├── App.xaml.cs ├── FreeMote.Tools.Viewer.csproj ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── PreciseTimer.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings └── app.config ├── FreeMote.sln ├── FreeMote.sln.DotSettings ├── FreeMote ├── Adler32.cs ├── AstcDecoder.cs ├── AstcFile.cs ├── Bc7Decoder.cs ├── BitmapExtension.cs ├── BitmapHelper.cs ├── Consts.cs ├── DxtCodec.cs ├── DxtUtil.cs ├── FreeMote.LICENSE.txt ├── FreeMote.csproj ├── FreeMoteExtension.cs ├── Logger.cs ├── MPack.cs ├── Motion │ └── MmoEnums.cs ├── PathHelper.cs ├── PostProcessing.cs ├── PrefixTree.cs ├── Properties │ └── AssemblyInfo.cs ├── PsbEnums.cs ├── PsbFile.cs ├── PsbHeader.cs ├── PsbStreamContext.cs ├── RL.cs ├── RleCompress.cs ├── TlgImageConverter.cs └── ZlibCompress.cs ├── LICENSE └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CS1591: 缺少对公共可见类型或成员的 XML 注释 4 | dotnet_diagnostic.cs1591.severity = none 5 | 6 | # Microsoft .NET properties 7 | csharp_new_line_before_members_in_object_initializers = false 8 | csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion 9 | csharp_space_after_cast = true 10 | csharp_style_var_elsewhere = true:suggestion 11 | csharp_style_var_for_built_in_types = true:suggestion 12 | csharp_style_var_when_type_is_apparent = true:suggestion 13 | dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none 14 | dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none 15 | dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none 16 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 17 | dotnet_style_predefined_type_for_member_access = true:suggestion 18 | dotnet_style_qualification_for_event = false:suggestion 19 | dotnet_style_qualification_for_field = false:suggestion 20 | dotnet_style_qualification_for_method = false:suggestion 21 | dotnet_style_qualification_for_property = false:suggestion 22 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 23 | 24 | # ReSharper properties 25 | resharper_csharp_max_line_length = 140 26 | resharper_space_within_single_line_array_initializer_braces = false 27 | 28 | # ReSharper inspection severities 29 | resharper_arrange_redundant_parentheses_highlighting = hint 30 | resharper_arrange_this_qualifier_highlighting = hint 31 | resharper_arrange_type_member_modifiers_highlighting = hint 32 | resharper_arrange_type_modifiers_highlighting = hint 33 | resharper_built_in_type_reference_style_for_member_access_highlighting = hint 34 | resharper_built_in_type_reference_style_highlighting = hint 35 | resharper_redundant_base_qualifier_highlighting = warning 36 | resharper_suggest_var_or_type_built_in_types_highlighting = hint 37 | resharper_suggest_var_or_type_elsewhere_highlighting = hint 38 | resharper_suggest_var_or_type_simple_types_highlighting = hint 39 | 40 | [*.{appxmanifest,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,csproj,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,jsproj,lsproj,mpp,mq4,mq5,mqh,njsproj,nuspec,proj,props,proto,resw,resx,StyleCop,targets,tasks,tpp,usf,ush,vbproj,xml,xsd}] 41 | indent_style = tab 42 | indent_size = tab 43 | tab_width = 4 44 | 45 | [*.{asax,ascx,aspx,axaml,cs,cshtml,css,htm,html,js,jsx,master,paml,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}] 46 | indent_style = space 47 | indent_size = 4 48 | tab_width = 4 49 | 50 | [*.{json,resjson}] 51 | indent_style = space 52 | indent_size = 2 53 | tab_width = 2 54 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: Ulysses 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Provide samples (e.g. PSB file) or your issue will be ignored! 4 | title: "[Question]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | * Read [wiki](https://github.com/UlyssesWu/FreeMote/wiki) 10 | * Provide game name and platform (PC/Android/PSP etc.) 11 | * Provide PSB file samples 12 | * Program output, or screenshots 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: beta 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: windows-2019 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Add NuGet source 20 | run: nuget sources add -Name MonarchSolutions -Source https://www.myget.org/F/monarchsolutions/api/v3/index.json 21 | 22 | - name: Restore NuGet packages 23 | run: nuget restore FreeMote.sln 24 | 25 | - name: Build solution 26 | run: dotnet build -c Release FreeMote.sln 27 | 28 | - name: Run post-build script 29 | shell: pwsh 30 | run: | 31 | cd $env:GITHUB_WORKSPACE 32 | mkdir FreeMoteToolkit -Force 33 | Copy-Item FreeMote.Tools.EmtConvert/bin/Release/net48/* FreeMoteToolkit -Recurse -Force 34 | Copy-Item FreeMote.Tools.PsbDecompile/bin/Release/net48/* FreeMoteToolkit -Recurse -Force 35 | Copy-Item FreeMote.Tools.PsBuild/bin/Release/net48/* FreeMoteToolkit -Recurse -Force 36 | Copy-Item FreeMote.Tools.EmtMake/bin/Release/net48/* FreeMoteToolkit -Recurse -Force 37 | Copy-Item FreeMote.Tools.Viewer/bin/Release/net48/* FreeMoteToolkit -Recurse -Force 38 | del FreeMoteToolkit\*.pdb 39 | del FreeMoteToolkit\*.xml 40 | mkdir FreeMoteToolkit\lib -Force 41 | Move-Item FreeMoteToolkit/*.dll FreeMoteToolkit/lib 42 | Move-Item FreeMoteToolkit/x86 FreeMoteToolkit/lib/ 43 | Move-Item FreeMoteToolkit/x64 FreeMoteToolkit/lib/ 44 | 45 | - name: Upload artifact 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: FreeMote-${{ github.workflow }}-${{ github.run_number }} 49 | path: FreeMoteToolkit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | /FreeMote.Purify 254 | /FreeMote.Tests/Res 255 | /FreeMote.Tests/PurifyTest.cs 256 | 257 | /MigrationBackup 258 | /bak 259 | /.vscode 260 | launchSettings.json 261 | -------------------------------------------------------------------------------- /FreeMote.Plugins.x64/FreeMote.Plugins.x64.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net48 4 | Library 5 | FreeMote.Plugins 6 | false 7 | x64 8 | 9 | 10 | 11 | 1.1.0 12 | 13 | 14 | 0.2.0 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | echo https://github.com/dotnet/project-system/issues/1569#issuecomment-289939906 27 | xcopy "$(TargetDir.TrimEnd('\'))" "$(SolutionDir)FreeMote.Tools.PsbDecompile\$(OutDir)" /E /Y 28 | xcopy "$(TargetDir.TrimEnd('\'))" "$(SolutionDir)FreeMote.Tools.PsBuild\$(OutDir)" /E /Y 29 | xcopy "$(TargetDir.TrimEnd('\'))" "$(SolutionDir)FreeMote.Tools.EmtConvert\$(OutDir)" /E /Y 30 | xcopy "$(TargetDir.TrimEnd('\'))" "$(SolutionDir)FreeMote.Tools.EmtMake\$(OutDir)" /E /Y 31 | 32 | -------------------------------------------------------------------------------- /FreeMote.Plugins.x64/Images/TlgFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Composition; 4 | using System.Drawing; 5 | using System.IO; 6 | using FreeMote.Tlg; 7 | 8 | namespace FreeMote.Plugins.Images 9 | { 10 | [Export(typeof(IPsbImageFormatter))] 11 | [ExportMetadata("Name", "FreeMote.Tlg")] 12 | [ExportMetadata("Author", "Ulysses")] 13 | [ExportMetadata("Comment", "TLG support via TlgLib.")] 14 | public class TlgFormatter : IPsbImageFormatter 15 | { 16 | private const string TlgVersion = "TlgVersion"; 17 | 18 | /// 19 | /// Is TLG encode plugin enabled 20 | /// 21 | //public static bool CanSaveTlg => TlgNativePlugin.IsEnabled; 22 | 23 | private static TlgImageConverter _managedConverter = null; 24 | 25 | /// 26 | /// Load TLG 27 | /// 28 | /// 29 | /// TLG version, can be 0(unknown),5,6 30 | /// 31 | public static Bitmap LoadTlg(byte[] tlgData, out int version) 32 | { 33 | if (!Consts.PreferManaged) 34 | { 35 | try 36 | { 37 | return TlgNative.ToBitmap(tlgData, out version); 38 | } 39 | catch (Exception) 40 | { 41 | //ignored, fallback to managed decoder 42 | } 43 | } 44 | 45 | if (_managedConverter == null) 46 | { 47 | _managedConverter = new TlgImageConverter(); 48 | } 49 | 50 | using (var ms = new MemoryStream(tlgData)) 51 | { 52 | using (var br = new BinaryReader(ms)) 53 | { 54 | var bmp = _managedConverter.ReadAndGetMetaData(br, out var md); 55 | version = md.Version; 56 | return bmp; 57 | } 58 | } 59 | } 60 | 61 | /// 62 | /// Save TLG 63 | /// 64 | /// 65 | /// true: Save as TLG6; false: Save as TLG5 66 | /// 67 | public static byte[] SaveTlg(Bitmap bmp, bool tlg6 = false) 68 | { 69 | return tlg6 ? bmp.ToTlg6() : bmp.ToTlg5(); 70 | } 71 | 72 | public List Extensions { get; } = new List { ".tlg", ".tlg5", ".tlg6" }; 73 | public bool CanToBitmap(in byte[] data, Dictionary context = null) 74 | { 75 | return true; 76 | } 77 | 78 | public bool CanToBytes(Bitmap bitmap, Dictionary context = null) 79 | { 80 | var bpp = Image.GetPixelFormatSize(bitmap.PixelFormat); 81 | return bpp == 32 || bpp == 24 || bpp == 8; 82 | } 83 | 84 | public Bitmap ToBitmap(in byte[] data, int width, int height, PsbSpec platform, Dictionary context = null) 85 | { 86 | var bmp = LoadTlg(data, out var v); 87 | if (v > 5 && context != null) 88 | { 89 | context[TlgVersion] = v; 90 | } 91 | 92 | return bmp; 93 | } 94 | 95 | public byte[] ToBytes(Bitmap bitmap, PsbSpec platform, Dictionary context = null) 96 | { 97 | return SaveTlg(bitmap, 98 | context != null && context.ContainsKey(TlgVersion) && context[TlgVersion] is int v && v == 6); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /FreeMote.Plugins.x64/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("FreeMote.Plugins.x64")] 5 | [assembly: AssemblyDescription("FreeMote official plugins (x64)")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("Ulysses")] 8 | [assembly: AssemblyProduct("FreeMote")] 9 | [assembly: AssemblyCopyright("Copyright © Ulysses 2020-2025")] 10 | [assembly: AssemblyTrademark("wdwxy12345@gmail.com")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | [assembly: ComVisible(false)] 14 | 15 | [assembly: Guid("7b5bae81-6779-4404-9e6e-512be4471ab9")] 16 | 17 | // [assembly: AssemblyVersion("1.0.*")] 18 | [assembly: AssemblyVersion("4.0.0.0")] 19 | [assembly: AssemblyFileVersion("4.0.0.0")] 20 | -------------------------------------------------------------------------------- /FreeMote.Plugins.x64/Shells/MflShell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Composition; 4 | using System.IO; 5 | using System.Linq; 6 | using FreeMote.FastLz; 7 | using FreeMote.Psb; 8 | using static FreeMote.Consts; 9 | 10 | namespace FreeMote.Plugins.Shells 11 | { 12 | [Export(typeof(IPsbShell))] 13 | [ExportMetadata("Name", "FreeMote.Mfl")] 14 | [ExportMetadata("Author", "Ulysses")] 15 | [ExportMetadata("Comment", "MFL (FastLZ) support.")] 16 | class MflShell : IPsbShell 17 | { 18 | public string Name => "MFL"; 19 | 20 | public byte[] Signature => new byte[] {(byte) 'm', (byte) 'f', (byte) 'l', 0}; 21 | 22 | public bool IsInShell(Stream stream, Dictionary context = null) 23 | { 24 | var header = new byte[4]; 25 | var pos = stream.Position; 26 | stream.Read(header, 0, 4); 27 | stream.Position = pos; 28 | if (header.SequenceEqual(Signature)) 29 | { 30 | if (context != null) 31 | { 32 | context[Context_PsbShellType] = Name; 33 | } 34 | 35 | return true; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public MemoryStream ToPsb(Stream stream, Dictionary context = null) 42 | { 43 | if (context != null) 44 | { 45 | if (context.ContainsKey(Context_MdfKey)) 46 | { 47 | int? keyLength = context.ContainsKey(Context_MdfKeyLength) 48 | ? Convert.ToInt32(context[Context_MdfKeyLength]) 49 | : (int?) null; 50 | 51 | stream = PsbExtension.EncodeMdf(stream, (string) context[Context_MdfKey], keyLength, true); 52 | stream.Position = 0; //A new MemoryStream 53 | } 54 | } 55 | 56 | stream.Seek(4, SeekOrigin.Current); 57 | var bytes = new byte[4]; 58 | stream.Read(bytes, 0, 4); 59 | var unzippedSize = BitConverter.ToInt32(bytes, 0); 60 | 61 | byte[] input = new byte[stream.Length - 8]; 62 | stream.Read(input, 0, input.Length); 63 | 64 | var output = FastLzNative.Decompress(input, unzippedSize); 65 | if (output == null) 66 | { 67 | throw new InvalidDataException("Fast LZ decompress failed"); 68 | } 69 | 70 | return new MemoryStream(output); 71 | } 72 | 73 | 74 | public MemoryStream ToShell(Stream stream, Dictionary context = null) 75 | { 76 | byte[] input; 77 | if (stream is MemoryStream inputMs) 78 | { 79 | input = inputMs.ToArray(); 80 | } 81 | else 82 | { 83 | input = new byte[stream.Length]; 84 | stream.Read(input, 0, input.Length); 85 | } 86 | 87 | var unzipLength = input.Length; 88 | 89 | var output = FastLzNative.Compress(input); 90 | var ms = new MemoryStream(output); 91 | 92 | if (context != null && context.ContainsKey(Context_MdfKey)) 93 | { 94 | int? keyLength; 95 | if (context.ContainsKey(Context_MdfKeyLength)) 96 | { 97 | keyLength = Convert.ToInt32(context[Context_MdfKeyLength]); 98 | } 99 | else 100 | { 101 | keyLength = (int?) null; 102 | } 103 | 104 | var mms = PsbExtension.EncodeMdf(ms, (string) context[Context_MdfKey], keyLength, false); 105 | ms?.Dispose(); //ms disposed 106 | ms = mms; 107 | } 108 | 109 | ms.Seek(0, SeekOrigin.Begin); 110 | var shellMs = new MemoryStream((int) (ms.Length + 8)); 111 | shellMs.Write(Signature, 0, 4); 112 | shellMs.Write(BitConverter.GetBytes(unzipLength), 0, 4); 113 | ms.CopyTo(shellMs); 114 | ms.Dispose(); 115 | shellMs.Seek(0, SeekOrigin.Begin); 116 | return shellMs; 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /FreeMote.Plugins/Audio/NxAdpcmFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Composition; 4 | using System.IO; 5 | using System.Linq; 6 | using FreeMote.Psb; 7 | using VGAudio.Containers.Dsp; 8 | using VGAudio.Containers.Wave; 9 | using VGAudio.Utilities; 10 | 11 | //REF: https://www.metroid2002.com/retromodding/wiki/DSP_(File_Format) 12 | 13 | namespace FreeMote.Plugins.Audio 14 | { 15 | [Export(typeof(IPsbAudioFormatter))] 16 | [ExportMetadata("Name", "FreeMote.NxAdpcm")] 17 | [ExportMetadata("Author", "Ulysses")] 18 | [ExportMetadata("Comment", "NX ADPCM support via VGAudio.")] 19 | class NxAdpcmFormatter : IPsbAudioFormatter 20 | { 21 | public List Extensions { get; } = new List { ".adpcm" }; 22 | public bool CanToWave(IArchData archData, Dictionary context = null) 23 | { 24 | return archData is AdpcmArchData; 25 | } 26 | 27 | public bool CanToArchData(byte[] wave, Dictionary context = null) 28 | { 29 | return wave != null; 30 | } 31 | 32 | public byte[] ToWave(AudioMetadata md, IArchData archData, string fileName = null, Dictionary context = null) 33 | { 34 | DspReader reader = new DspReader(); 35 | var data = reader.Read(archData.Data.Data); 36 | using MemoryStream oms = new MemoryStream(); 37 | WaveWriter writer = new WaveWriter(); 38 | writer.WriteToStream(data, oms, new WaveConfiguration { Codec = WaveCodec.Pcm16Bit }); //only 16Bit supported 39 | return oms.ToArray(); 40 | } 41 | 42 | public bool ToArchData(AudioMetadata md, IArchData archData, in byte[] wave, string fileName, string waveExt, Dictionary context = null) 43 | { 44 | if (archData is not AdpcmArchData data) 45 | { 46 | return false; 47 | } 48 | WaveReader reader = new WaveReader(); 49 | var rawData = reader.Read(wave); 50 | using MemoryStream oms = new MemoryStream(); 51 | DspWriter writer = new DspWriter(); 52 | writer.WriteToStream(rawData, oms, new DspConfiguration {Endianness = Endianness.LittleEndian}); 53 | data.Data = new PsbResource {Data = oms.ToArray()}; 54 | var format = rawData.GetAllFormats().FirstOrDefault(); 55 | if (format != null) 56 | { 57 | data.SampRate = format.SampleRate; 58 | } 59 | 60 | return true; 61 | } 62 | 63 | public bool TryGetArchData(AudioMetadata md, PsbDictionary channel, out IArchData data, Dictionary context = null) 64 | { 65 | data = null; 66 | //if (psb.Platform != PsbSpec.nx) 67 | //{ 68 | // return false; 69 | //} 70 | if (!(channel["archData"] is PsbDictionary archDic)) 71 | { 72 | return false; 73 | } 74 | 75 | if (!archDic.ContainsKey("body") && 76 | archDic["data"] is PsbResource aData 77 | && archDic["ext"] is PsbString ext && ext.Value == Extensions[0] && 78 | archDic["samprate"] is PsbNumber sampRate) 79 | { 80 | var newData = new AdpcmArchData 81 | { 82 | Data = aData, 83 | SampRate = sampRate.AsInt, 84 | Format = PsbAudioFormat.ADPCM 85 | }; 86 | 87 | ExtractPan(channel, newData); 88 | 89 | data = newData; 90 | return true; 91 | } 92 | 93 | return false; 94 | } 95 | 96 | private static void ExtractPan(PsbDictionary channel, AdpcmArchData newData) 97 | { 98 | if (channel["pan"] is PsbList panList) 99 | { 100 | newData.Pan = panList; 101 | 102 | if (panList.Count == 2) 103 | { 104 | var left = panList[0].GetFloat(); 105 | var right = panList[1].GetFloat(); 106 | if (left == 1.0f && right == 0.0f) 107 | { 108 | newData.ChannelPan = PsbAudioPan.Left; 109 | } 110 | else if (left == 0.0f && right == 1.0f) 111 | { 112 | newData.ChannelPan = PsbAudioPan.Right; 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /FreeMote.Plugins/Audio/VagFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Composition; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Reflection; 7 | using FreeMote.Psb; 8 | 9 | namespace FreeMote.Plugins.Audio 10 | { 11 | [Export(typeof(IPsbAudioFormatter))] 12 | [ExportMetadata("Name", "FreeMote.Vag")] 13 | [ExportMetadata("Author", "Ulysses")] 14 | [ExportMetadata("Comment", "VAG support.")] 15 | class VagFormatter : IPsbAudioFormatter 16 | { 17 | public List Extensions { get; } = new List { ".vag" }; 18 | 19 | private const string EncoderTool = "vagconv2.exe"; 20 | private const string DecoderTool = "vgmstream.exe"; 21 | 22 | public string ToolPath { get; set; } = null; 23 | 24 | public VagFormatter() 25 | { 26 | var toolPath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) ?? "", "Tools", EncoderTool); 27 | if (File.Exists(toolPath)) 28 | { 29 | ToolPath = toolPath; 30 | } 31 | } 32 | public bool CanToWave(IArchData archData, Dictionary context = null) 33 | { 34 | if (archData is PsArchData psArch) 35 | { 36 | if (psArch.Data?.Data != null && psArch.Data.Data.Length > 4) 37 | { 38 | if (psArch.Data.Data.AsciiEqual("VAGp")) 39 | { 40 | return true; 41 | } 42 | } 43 | } 44 | 45 | return false; 46 | } 47 | 48 | public bool CanToArchData(byte[] wave, Dictionary context = null) 49 | { 50 | if (!File.Exists(ToolPath)) 51 | { 52 | Logger.LogWarn($"[WARN] External tool missing: {EncoderTool}"); 53 | return false; 54 | } 55 | 56 | return true; 57 | } 58 | 59 | public byte[] ToWave(AudioMetadata md, IArchData archData, string fileName = null, Dictionary context = null) 60 | { 61 | //bool hasTool = File.Exists(ToolPath); 62 | 63 | VagFile vag = new VagFile(); 64 | if (vag.LoadFromStream(new MemoryStream(archData.Data.Data))) 65 | { 66 | return vag.ToWave().ToArray(); 67 | } 68 | 69 | return null; 70 | } 71 | 72 | public bool ToArchData(AudioMetadata md, IArchData archData, in byte[] wave, string fileName, string waveExt, Dictionary context = null) 73 | { 74 | if (!File.Exists(ToolPath)) 75 | { 76 | return false; 77 | } 78 | 79 | if (archData is not PsArchData data) 80 | { 81 | return false; 82 | } 83 | 84 | var tempPath = Path.GetTempPath(); 85 | var tempFile = Path.Combine(tempPath, fileName); 86 | if (tempFile.EndsWith(".vag")) 87 | { 88 | tempFile = Path.ChangeExtension(tempFile, ""); 89 | } 90 | File.WriteAllBytes(tempFile, wave); 91 | var tempOutFile = Path.ChangeExtension(tempFile, ".vag"); 92 | 93 | byte[] outBytes = null; 94 | try 95 | { 96 | ProcessStartInfo info = new ProcessStartInfo(ToolPath, $"\"{tempFile}\"") 97 | { 98 | UseShellExecute = false, 99 | WindowStyle = ProcessWindowStyle.Hidden, 100 | CreateNoWindow = true 101 | }; 102 | Process process = Process.Start(info); 103 | process?.WaitForExit(); 104 | if (!File.Exists(tempOutFile) || process?.ExitCode != 0) 105 | { 106 | Logger.LogError("[ERROR] VAG convert failed."); 107 | return false; 108 | } 109 | 110 | outBytes = File.ReadAllBytes(tempOutFile); 111 | File.Delete(tempFile); 112 | File.Delete(tempOutFile); 113 | } 114 | catch (Exception e) 115 | { 116 | Logger.Log(e); 117 | } 118 | 119 | if (data.Data == null) 120 | { 121 | data.Data = new PsbResource { Data = outBytes }; 122 | } 123 | else 124 | { 125 | data.Data.Data = outBytes; 126 | } 127 | data.Format = PsbAudioFormat.VAG; 128 | 129 | return true; 130 | } 131 | 132 | public bool TryGetArchData(AudioMetadata md, PsbDictionary channel, out IArchData data, Dictionary context = null) 133 | { 134 | data = null; 135 | if (md.Spec == PsbSpec.ps4 || md.Spec == PsbSpec.vita) 136 | { 137 | if (channel.Count == 1 && channel["archData"] is PsbResource res) 138 | { 139 | if (res.Data != null && res.Data.Length > 0) //res data exists 140 | { 141 | if (res.Data.AsciiEqual("VAGp")) 142 | { 143 | data = new PsArchData 144 | { 145 | Data = res, 146 | Format = PsbAudioFormat.VAG 147 | }; 148 | return true; 149 | } 150 | 151 | //...but the data is other format (at9) 152 | return false; 153 | } 154 | 155 | //res data is null, maybe linking 156 | data = new PsArchData 157 | { 158 | Data = res 159 | }; 160 | return true; 161 | } 162 | 163 | return false; 164 | } 165 | 166 | return false; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /FreeMote.Plugins/Audio/XwmaFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Composition; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Reflection; 7 | using FreeMote.Psb; 8 | 9 | //REF: https://wiki.multimedia.cx/index.php/Microsoft_xWMA 10 | 11 | namespace FreeMote.Plugins.Audio 12 | { 13 | [Export(typeof(IPsbAudioFormatter))] 14 | [ExportMetadata("Name", "FreeMote.Xwma")] 15 | [ExportMetadata("Author", "Ulysses")] 16 | [ExportMetadata("Comment", "XWMA support.")] 17 | class XwmaFormatter : IPsbAudioFormatter 18 | { 19 | public List Extensions { get; } = new List {".xwma", ".xwm"}; 20 | 21 | private const string EncoderTool = "xWMAEncode.exe"; 22 | 23 | public string ToolPath { get; set; } = null; 24 | 25 | public XwmaFormatter() 26 | { 27 | var toolPath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) ?? "", "Tools", EncoderTool); 28 | if (File.Exists(toolPath)) 29 | { 30 | ToolPath = toolPath; 31 | } 32 | } 33 | 34 | public bool CanToWave(IArchData archData, Dictionary context = null) 35 | { 36 | return archData is XwmaArchData; 37 | } 38 | 39 | public bool CanToArchData(byte[] wave, Dictionary context = null) 40 | { 41 | if (!File.Exists(ToolPath)) 42 | { 43 | Logger.LogWarn($"[WARN] External tool missing: {EncoderTool}"); 44 | return false; 45 | } 46 | 47 | return wave != null; 48 | } 49 | 50 | public byte[] ToWave(AudioMetadata md, IArchData archData, string fileName = null, Dictionary context = null) 51 | { 52 | if (string.IsNullOrEmpty(ToolPath)) 53 | { 54 | archData.WaveExtension = Extensions[0]; 55 | return ((XwmaArchData)archData).ToXwma(); 56 | } 57 | 58 | archData.WaveExtension = ".wav"; 59 | var xwmaBytes = ((XwmaArchData) archData).ToXwma(); 60 | var tempFile = Path.GetTempFileName(); 61 | File.WriteAllBytes(tempFile, xwmaBytes); 62 | var tempOutFile = Path.GetTempFileName(); 63 | 64 | byte[] outBytes = null; 65 | try 66 | { 67 | ProcessStartInfo info = new ProcessStartInfo(ToolPath, $"\"{tempFile}\" \"{tempOutFile}\"") 68 | { 69 | WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true 70 | }; 71 | Process process = Process.Start(info); 72 | process?.WaitForExit(); 73 | 74 | outBytes = File.ReadAllBytes(tempOutFile); 75 | File.Delete(tempFile); 76 | File.Delete(tempOutFile); 77 | } 78 | catch (Exception e) 79 | { 80 | Logger.Log(e); 81 | } 82 | 83 | 84 | return outBytes; 85 | } 86 | 87 | public bool ToArchData(AudioMetadata md, IArchData archData, in byte[] wave, string fileName, string waveExt, Dictionary context = null) 88 | { 89 | if (!File.Exists(ToolPath)) 90 | { 91 | return false; 92 | } 93 | 94 | if (archData is not XwmaArchData xwma) 95 | { 96 | return false; 97 | } 98 | 99 | var tempFile = Path.GetTempFileName(); 100 | File.WriteAllBytes(tempFile, wave); 101 | var tempOutFile = Path.GetTempFileName(); 102 | MemoryStream oms = null; 103 | try 104 | { 105 | ProcessStartInfo info = new ProcessStartInfo(ToolPath, $"\"{tempFile}\" \"{tempOutFile}\"") 106 | { 107 | WindowStyle = ProcessWindowStyle.Hidden, 108 | CreateNoWindow = true 109 | }; 110 | Process process = Process.Start(info); 111 | process?.WaitForExit(); 112 | 113 | var fs = File.OpenRead(tempOutFile); 114 | oms = new MemoryStream((int)fs.Length); 115 | fs.CopyTo(oms); 116 | oms.Position = 0; 117 | 118 | File.Delete(tempFile); 119 | File.Delete(tempOutFile); 120 | } 121 | catch (Exception e) 122 | { 123 | Logger.Log(e); 124 | } 125 | 126 | if (oms == null) 127 | { 128 | return false; 129 | } 130 | 131 | xwma.ReadFromXwma(oms); 132 | oms.Dispose(); 133 | 134 | return true; 135 | } 136 | 137 | public bool TryGetArchData(AudioMetadata md, PsbDictionary channel, out IArchData data, Dictionary context = null) 138 | { 139 | data = null; 140 | if (md.Spec == PsbSpec.win) 141 | { 142 | if (channel.Count == 1 && channel["archData"] is PsbDictionary archDic && archDic["data"] is PsbResource aData && archDic["dpds"] is PsbResource aDpds && archDic["fmt"] is PsbResource aFmt && archDic["wav"] is PsbString aWav) 143 | { 144 | data = new XwmaArchData() 145 | { 146 | Data = aData, 147 | Fmt = aFmt, 148 | Dpds = aDpds, 149 | Wav = aWav.Value 150 | }; 151 | 152 | return true; 153 | } 154 | 155 | return false; 156 | } 157 | 158 | return false; 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /FreeMote.Plugins/FreeMote.Plugins.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net48 4 | Library 5 | false 6 | default 7 | 8 | 9 | 10 | True 11 | True 12 | Resources.resx 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 2.1.0-CI00002 22 | 23 | 24 | 2.5.1.16 25 | 26 | 27 | 1.0.0.6 28 | 29 | 30 | 1.3.8 31 | 32 | 33 | 34 | 35 | 36 | 2.2.1-CI00002 37 | 38 | 39 | 1.4.5 40 | 41 | 42 | 43 | 44 | ResXFileCodeGenerator 45 | Resources.Designer.cs 46 | Designer 47 | 48 | 49 | 50 | 51 | echo There is a bug that some nuget package dll may not have edit date so it won't be copied. If it happens, remove /D and try again. 52 | xcopy "$(TargetDir.TrimEnd('\'))" "$(SolutionDir)FreeMote.Tools.PsbDecompile\$(OutDir)" /E /Y 53 | xcopy "$(TargetDir.TrimEnd('\'))" "$(SolutionDir)FreeMote.Tools.PsBuild\$(OutDir)" /E /Y 54 | xcopy "$(TargetDir.TrimEnd('\'))" "$(SolutionDir)FreeMote.Tools.EmtConvert\$(OutDir)" /E /Y 55 | xcopy "$(TargetDir.TrimEnd('\'))" "$(SolutionDir)FreeMote.Tools.EmtMake\$(OutDir)" /E /Y 56 | xcopy "$(TargetDir.TrimEnd('\'))" "$(SolutionDir)FreeMote.Tools.Viewer\$(OutDir)" /E /Y 57 | xcopy "$(TargetDir.TrimEnd('\'))" "$(SolutionDir)FreeMote.Tests\$(OutDir)" /E /Y 58 | 59 | -------------------------------------------------------------------------------- /FreeMote.Plugins/FreeMote.Plugins.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /FreeMote.Plugins/Images/AstcFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Composition; 4 | using System.Diagnostics; 5 | using System.Drawing; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | 10 | namespace FreeMote.Plugins.Images 11 | { 12 | [Export(typeof(IPsbImageFormatter))] 13 | [ExportMetadata("Name", "FreeMote.Astc")] 14 | [ExportMetadata("Author", "Ulysses")] 15 | [ExportMetadata("Comment", "ASTC support.")] 16 | class AstcFormatter : IPsbImageFormatter 17 | { 18 | public const string AstcKeepHeader = "AstcKeepHeader"; 19 | 20 | public AstcFormatter() 21 | { 22 | var toolPath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) ?? "", "Tools"); 23 | foreach (var encoderTool in EncoderTools) 24 | { 25 | var tool = Path.Combine(toolPath, encoderTool); 26 | if (File.Exists(tool)) 27 | { 28 | ToolPath = tool; 29 | return; 30 | } 31 | } 32 | } 33 | 34 | public string ToolPath { get; set; } = null; 35 | 36 | public List Extensions { get; } = new() {".astc"}; 37 | 38 | public static List EncoderTools { get; } = new() 39 | {"astcenc.exe", "astcenc-sse2.exe", "astcenc-sse4.1.exe", "astcenc-avx2.exe"}; 40 | 41 | public bool CanToBitmap(in byte[] data, Dictionary context = null) 42 | { 43 | if (data.Take(4).SequenceEqual(AstcHeader.Magic)) 44 | { 45 | return true; 46 | } 47 | 48 | return false; 49 | } 50 | 51 | public bool CanToBytes(Bitmap bitmap, Dictionary context = null) 52 | { 53 | if (string.IsNullOrEmpty(ToolPath)) 54 | { 55 | return false; 56 | } 57 | 58 | return true; 59 | } 60 | 61 | public Bitmap ToBitmap(in byte[] data, int width, int height, PsbSpec platform, Dictionary context = null) 62 | { 63 | if (AstcFile.IsAstcHeader(data)) 64 | { 65 | var header = AstcFile.ParseAstcHeader(data); 66 | return RL.ConvertToImage(data.AsSpan(AstcHeader.Length).ToArray(), header.Width, header.Height, PsbPixelFormat.ASTC_8BPP); 67 | } 68 | 69 | return null; 70 | } 71 | 72 | public byte[] ToBytes(Bitmap bitmap, PsbSpec platform, Dictionary context = null) 73 | { 74 | var tempFile = Path.GetTempFileName(); 75 | bitmap.Save(tempFile); 76 | var tempOutFile = Path.ChangeExtension(tempFile, ".astc"); 77 | 78 | byte[] outBytes = null; 79 | try 80 | { 81 | ProcessStartInfo info = new ProcessStartInfo(ToolPath, $"-cl \"{tempFile}\" \"{tempOutFile}\" 4x4 -thorough") 82 | { 83 | UseShellExecute = false, 84 | WindowStyle = ProcessWindowStyle.Hidden, 85 | CreateNoWindow = true 86 | }; 87 | Process process = Process.Start(info); 88 | process?.WaitForExit(); 89 | 90 | if (!File.Exists(tempOutFile)) 91 | { 92 | Logger.LogError("[ERROR] ASTC convert failed."); 93 | File.Delete(tempFile); 94 | return null; 95 | } 96 | 97 | outBytes = File.ReadAllBytes(tempOutFile); 98 | if (outBytes.Length == 0) 99 | { 100 | Logger.LogWarn("[WARN] ASTC encoder output length is 0"); 101 | } 102 | File.Delete(tempFile); 103 | File.Delete(tempOutFile); 104 | } 105 | catch (Exception e) 106 | { 107 | Logger.Log(e); 108 | } 109 | 110 | if (context != null) 111 | { 112 | if (context.ContainsKey(AstcKeepHeader) && context[AstcKeepHeader] is true) 113 | { 114 | return outBytes; 115 | } 116 | } 117 | 118 | return AstcFile.CutHeader(outBytes); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /FreeMote.Plugins/Images/Bc7Formatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Composition; 4 | using System.Diagnostics; 5 | using System.Drawing; 6 | using System.Runtime.InteropServices; 7 | using BCnEncoder.Decoder; 8 | using BCnEncoder.Encoder; 9 | using BCnEncoder.Shared; 10 | using PixelFormat = System.Drawing.Imaging.PixelFormat; 11 | 12 | namespace FreeMote.Plugins.Images 13 | { 14 | [Export(typeof(IPsbImageFormatter))] 15 | [ExportMetadata("Name", "FreeMote.Bc7")] 16 | [ExportMetadata("Author", "Ulysses")] 17 | [ExportMetadata("Comment", "BC7 support via BCnEncoder.NET.")] 18 | class Bc7Formatter : IPsbImageFormatter 19 | { 20 | public List Extensions { get; } = new() {".bc7", ".dds"}; 21 | 22 | public bool CanToBitmap(in byte[] data, Dictionary context = null) 23 | { 24 | return true; 25 | } 26 | 27 | public bool CanToBytes(Bitmap bitmap, Dictionary context = null) 28 | { 29 | if (bitmap != null && bitmap.PixelFormat == PixelFormat.Format32bppArgb) 30 | return true; 31 | return false; 32 | } 33 | 34 | public Bitmap ToBitmap(in byte[] data, int width, int height, PsbSpec platform, Dictionary context = null) 35 | { 36 | var decoder = new BcDecoder(); 37 | //var bufferSize = decoder.GetBlockSize(CompressionFormat.Bc7) * width * height; 38 | //Debug.WriteLine($"size: expect: {bufferSize} ; actual: {buffer.Length}"); 39 | 40 | var pixels = decoder.DecodeRaw(data, width, height, CompressionFormat.Bc7); 41 | var pixelBytes = MemoryMarshal.Cast(pixels); 42 | return RL.ConvertToImage(pixelBytes.ToArray(), width, height, PsbPixelFormat.BeRGBA8); 43 | } 44 | 45 | public byte[] ToBytes(Bitmap bitmap, PsbSpec platform, Dictionary context = null) 46 | { 47 | var encoder = new BcEncoder(CompressionFormat.Bc7) 48 | {OutputOptions = {GenerateMipMaps = false, Quality = CompressionQuality.Fast}}; 49 | var pixelBytes = RL.GetPixelBytesFromImage(bitmap, PsbPixelFormat.BeRGBA8); 50 | var pixels = encoder.EncodeToRawBytes(pixelBytes, bitmap.Width, bitmap.Height, BCnEncoder.Encoder.PixelFormat.Rgba32); 51 | return pixels[0]; //other = mipmap 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /FreeMote.Plugins/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("FreeMote.Plugins")] 6 | [assembly: AssemblyDescription("FreeMote official plugins.")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("Ulysses")] 9 | [assembly: AssemblyProduct("FreeMote")] 10 | [assembly: AssemblyCopyright("Copyright © Ulysses 2018-2025")] 11 | [assembly: AssemblyTrademark("wdwxy12345@gmail.com")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | [assembly: InternalsVisibleTo("FreeMote.Tests")] 16 | 17 | [assembly: Guid("f37472b9-6501-440e-8898-7774304f7fec")] 18 | 19 | // [assembly: AssemblyVersion("1.0.*")] 20 | [assembly: AssemblyVersion("4.0.0.0")] 21 | [assembly: AssemblyFileVersion("4.0.0.0")] 22 | -------------------------------------------------------------------------------- /FreeMote.Plugins/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace FreeMote.Plugins.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// 一个强类型的资源类,用于查找本地化的字符串等。 17 | /// 18 | // 此类是由 StronglyTypedResourceBuilder 19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen 21 | // (以 /str 作为命令选项),或重新生成 VS 项目。 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// 返回此类使用的缓存的 ResourceManager 实例。 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FreeMote.Plugins.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// 重写当前线程的 CurrentUICulture 属性,对 51 | /// 使用此强类型资源类的所有资源查找执行重写。 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// 查找类似 <?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> 65 | ///<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 6.1.10"> 66 | /// <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> 67 | /// <rdf:Description rdf:about="" 68 | /// xmlns:xmp="http://ns.adobe.com/xap/1.0/" 69 | /// xmlns:dc="http://purl.org/dc/elements/1.1/" 70 | /// xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/" 71 | /// xmlns:xmpRights="http://ns.adobe.com/xap/1.0/rights/" 72 | /// xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" 73 | /// [字符串的其余部分被截断]"; 的本地化字符串。 74 | /// 75 | internal static string Xmp { 76 | get { 77 | return ResourceManager.GetString("Xmp", resourceCulture); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /FreeMote.Plugins/Shells/Lz4Shell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Composition; 4 | using System.IO; 5 | using K4os.Compression.LZ4.Streams; 6 | 7 | namespace FreeMote.Plugins.Shells 8 | { 9 | [Export(typeof(IPsbShell))] 10 | [ExportMetadata("Name", "FreeMote.Lz4")] 11 | [ExportMetadata("Author", "Ulysses")] 12 | [ExportMetadata("Comment", "LZ4 support.")] 13 | class Lz4Shell : IPsbShell 14 | { 15 | /// 16 | /// LZ4 Frame Header Signature 17 | /// 18 | public const int MAGIC = 0x184D2204; 19 | 20 | public string Name => "LZ4"; 21 | public bool IsInShell(Stream stream, Dictionary context = null) 22 | { 23 | var header = new byte[4]; 24 | var pos = stream.Position; 25 | stream.Read(header, 0, 4); 26 | stream.Position = pos; 27 | if (BitConverter.ToInt32(header, 0) == MAGIC) 28 | { 29 | if (context != null) 30 | { 31 | context[Consts.Context_PsbShellType] = Name; 32 | } 33 | return true; 34 | } 35 | 36 | return false; 37 | } 38 | 39 | public MemoryStream ToPsb(Stream stream, Dictionary context = null) 40 | { 41 | var ms = new MemoryStream(); 42 | using (var decode = LZ4Stream.Decode(stream, leaveOpen:true)) 43 | { 44 | decode.CopyTo(ms); 45 | } 46 | 47 | ms.Position = 0; 48 | return ms; 49 | } 50 | 51 | public MemoryStream ToShell(Stream stream, Dictionary context = null) 52 | { 53 | var ms = new MemoryStream(); 54 | using (var encode = LZ4Stream.Encode(ms, leaveOpen:true)) 55 | { 56 | stream.CopyTo(encode); 57 | } 58 | ms.Position = 0; 59 | return ms; 60 | } 61 | 62 | public byte[] Signature => BitConverter.GetBytes(MAGIC); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /FreeMote.Plugins/Shells/MdfShell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Composition; 4 | using System.IO; 5 | using System.Linq; 6 | using FreeMote.Psb; 7 | using static FreeMote.Consts; 8 | 9 | namespace FreeMote.Plugins.Shells 10 | { 11 | [Export(typeof(IPsbShell))] 12 | [ExportMetadata("Name", "FreeMote.Mdf")] 13 | [ExportMetadata("Author", "Ulysses")] 14 | [ExportMetadata("Comment", "MDF (ZLIB) support.")] 15 | class MdfShell : IPsbShell 16 | { 17 | public string Name => "MDF"; 18 | 19 | public byte[] Signature => new byte[] {(byte) 'm', (byte) 'd', (byte) 'f', 0}; 20 | 21 | public bool IsInShell(Stream stream, Dictionary context = null) 22 | { 23 | var header = new byte[4]; 24 | var pos = stream.Position; 25 | _ = stream.Read(header, 0, 4); 26 | stream.Position = pos; 27 | if (header.SequenceEqual(Signature)) 28 | { 29 | if (context != null) 30 | { 31 | context[Context_PsbShellType] = Name; 32 | } 33 | 34 | return true; 35 | } 36 | 37 | return false; 38 | } 39 | 40 | public MemoryStream ToPsb(Stream stream, Dictionary context = null) 41 | { 42 | int size = 0; 43 | if (context != null) 44 | { 45 | if (context.TryGetValue(Context_MdfKey, out var mdfKey)) 46 | { 47 | int? keyLength = context.TryGetValue(Context_MdfKeyLength, out var kl) 48 | ? Convert.ToInt32(kl) 49 | : (int?) null; 50 | 51 | stream = PsbExtension.EncodeMdf(stream, (string) mdfKey, keyLength, true); 52 | stream.Position = 0; //A new MemoryStream 53 | //File.WriteAllBytes("test.mdf", ((MemoryStream) stream).ToArray()); 54 | //stream.Position = 0; 55 | } 56 | 57 | var pos = stream.Position; 58 | stream.Seek(4, SeekOrigin.Current); 59 | var bytes = new byte[4]; 60 | stream.Read(bytes, 0, 4); 61 | if (FastMode) 62 | { 63 | size = BitConverter.ToInt32(bytes, 0); 64 | } 65 | stream.Seek(1, SeekOrigin.Current); 66 | context[Context_PsbZlibFastCompress] = stream.ReadByte() == (byte) 0x9C; 67 | stream.Position = pos; 68 | } 69 | 70 | return MPack.MdfDecompressToStream(stream, size) as MemoryStream; 71 | } 72 | 73 | 74 | public MemoryStream ToShell(Stream stream, Dictionary context = null) 75 | { 76 | bool fast = true; //mdf use fast mode by default 77 | if (context != null && context.TryGetValue(Context_PsbZlibFastCompress, out var fastCompress)) 78 | { 79 | fast = (bool) fastCompress; 80 | } 81 | 82 | var ms = MPack.CompressPsbToMdfStream(stream, fast); //this will prepend MDF header 83 | 84 | if (context != null && context.TryGetValue(Context_MdfKey, out var mdfKey)) 85 | { 86 | int? keyLength; 87 | if (context.TryGetValue(Context_MdfKeyLength, out var kl)) 88 | { 89 | keyLength = Convert.ToInt32(kl); 90 | } 91 | else 92 | { 93 | keyLength = (int?) null; 94 | } 95 | 96 | var mms = PsbExtension.EncodeMdf(ms, (string)mdfKey, keyLength, true); 97 | ms?.Dispose(); //ms disposed 98 | ms = mms; 99 | } 100 | 101 | return ms; 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /FreeMote.Plugins/Shells/MxbShell.cs: -------------------------------------------------------------------------------- 1 | using FreeMote.Psb; 2 | using System; 3 | using System.Buffers.Binary; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.Composition; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Runtime.InteropServices; 9 | using XMemCompress; 10 | using static FreeMote.Consts; 11 | 12 | namespace FreeMote.Plugins.Shells 13 | { 14 | [Export(typeof(IPsbShell))] 15 | [ExportMetadata("Name", "FreeMote.Mxb")] 16 | [ExportMetadata("Author", "Ulysses")] 17 | [ExportMetadata("Comment", "MXB (XMemCompress) support.")] 18 | internal class MxbShell : IPsbShell 19 | { 20 | public string Name => "MXB"; 21 | public byte[] Signature { get; } = { (byte) 'm', (byte) 'x', (byte) 'b', 0 }; 22 | 23 | public bool IsInShell(Stream stream, Dictionary context = null) 24 | { 25 | var header = new byte[4]; 26 | var pos = stream.Position; 27 | _ = stream.Read(header, 0, 4); 28 | stream.Position = pos; 29 | if (header.SequenceEqual(Signature)) 30 | { 31 | if (context != null) 32 | { 33 | context[Context_PsbShellType] = Name; 34 | } 35 | 36 | return true; 37 | } 38 | 39 | return false; 40 | } 41 | 42 | public MemoryStream ToPsb(Stream stream, Dictionary context = null) 43 | { 44 | if (context != null) 45 | { 46 | if (context.TryGetValue(Context_MdfKey, out var mdfKey)) 47 | { 48 | int? keyLength = context.TryGetValue(Context_MdfKeyLength, out var kl) 49 | ? Convert.ToInt32(kl) 50 | : (int?) null; 51 | 52 | stream = PsbExtension.EncodeMdf(stream, (string) mdfKey, keyLength, true); 53 | stream.Position = 0; //A new MemoryStream 54 | } 55 | } 56 | 57 | stream.Seek(4, SeekOrigin.Current); 58 | var bytes = new byte[4]; 59 | _ = stream.Read(bytes, 0, 4); 60 | var unzippedSize = BitConverter.ToInt32(bytes, 0); 61 | 62 | var ms = new MemoryStream(unzippedSize); 63 | XCompressFile.DecompressStream(stream, ms); 64 | ms.Seek(0, SeekOrigin.Begin); 65 | return ms; 66 | } 67 | 68 | public MemoryStream ToShell(Stream stream, Dictionary context = null) 69 | { 70 | var unzipLength = (int)stream.Length; 71 | var ms = XCompressFile.CompressStream(stream); 72 | if (context != null && context.TryGetValue(Context_MdfKey, out var mdfKey)) 73 | { 74 | int? keyLength; 75 | if (context.TryGetValue(Context_MdfKeyLength, out var kl)) 76 | { 77 | keyLength = Convert.ToInt32(kl); 78 | } 79 | else 80 | { 81 | keyLength = (int?) null; 82 | } 83 | 84 | var mms = PsbExtension.EncodeMdf(ms, (string) mdfKey, keyLength, false); 85 | ms?.Dispose(); //ms disposed 86 | ms = mms; 87 | } 88 | 89 | ms.Seek(0, SeekOrigin.Begin); 90 | var shellMs = new MemoryStream((int) (ms.Length + 8)); 91 | shellMs.Write(Signature, 0, 4); 92 | shellMs.Write(BitConverter.GetBytes(unzipLength), 0, 4); 93 | ms.CopyTo(shellMs); 94 | ms.Dispose(); 95 | shellMs.Seek(0, SeekOrigin.Begin); 96 | return shellMs; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /FreeMote.Plugins/Shells/MzsShell.cs: -------------------------------------------------------------------------------- 1 | //Actually I don't even have a sample of mzs. This is inspired by https://gitlab.com/modmyclassic/sega-mega-drive-mini/marchive-batch-tool/-/blob/master/MArchiveBatchTool/MArchive/ZStandardCodec.cs 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.Composition; 6 | using System.IO; 7 | using System.Linq; 8 | using FreeMote.Psb; 9 | using ZstdNet; 10 | using static FreeMote.Consts; 11 | 12 | namespace FreeMote.Plugins.Shells 13 | { 14 | [Export(typeof(IPsbShell))] 15 | [ExportMetadata("Name", "FreeMote.Mzs")] 16 | [ExportMetadata("Author", "Ulysses")] 17 | [ExportMetadata("Comment", "MZS (ZStandard) support.")] 18 | class MzsShell : IPsbShell 19 | { 20 | public string Name => "MZS"; 21 | 22 | public byte[] Signature => new byte[] { (byte)'m', (byte)'z', (byte)'s', 0 }; 23 | 24 | public bool IsInShell(Stream stream, Dictionary context = null) 25 | { 26 | var header = new byte[4]; 27 | var pos = stream.Position; 28 | _ = stream.Read(header, 0, 4); 29 | stream.Position = pos; 30 | if (header.SequenceEqual(Signature)) 31 | { 32 | if (context != null) 33 | { 34 | context[Context_PsbShellType] = Name; 35 | } 36 | 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | 43 | public MemoryStream ToPsb(Stream stream, Dictionary context = null) 44 | { 45 | if (context != null) 46 | { 47 | if (context.TryGetValue(Context_MdfKey, out var mdfKey)) 48 | { 49 | int? keyLength = context.TryGetValue(Context_MdfKeyLength, out var kl) 50 | ? Convert.ToInt32(kl) 51 | : (int?)null; 52 | 53 | stream = PsbExtension.EncodeMdf(stream, (string)mdfKey, keyLength, true); 54 | stream.Position = 0; //A new MemoryStream 55 | } 56 | } 57 | 58 | stream.Seek(4, SeekOrigin.Current); 59 | var bytes = new byte[4]; 60 | stream.Read(bytes, 0, 4); 61 | var unzippedSize = BitConverter.ToInt32(bytes, 0); 62 | 63 | byte[] input = new byte[stream.Length - 8]; 64 | stream.Read(input, 0, input.Length); 65 | 66 | using var decompress = new Decompressor(); 67 | var output = decompress.Unwrap(input, unzippedSize); 68 | 69 | return new MemoryStream(output); 70 | } 71 | 72 | 73 | public MemoryStream ToShell(Stream stream, Dictionary context = null) 74 | { 75 | var unzipLength = (int) stream.Length; 76 | int? compressLevel = null; 77 | if (context != null && context.TryGetValue(Context_PsbZStdCompressLevel, out var cl)) 78 | { 79 | compressLevel = (int) cl; 80 | if (compressLevel > CompressionOptions.MaxCompressionLevel) 81 | { 82 | compressLevel = CompressionOptions.MaxCompressionLevel; 83 | } 84 | else if (compressLevel < CompressionOptions.MinCompressionLevel) 85 | { 86 | compressLevel = CompressionOptions.MinCompressionLevel; 87 | } 88 | } 89 | 90 | byte[] input; 91 | if (stream is MemoryStream inputMs) 92 | { 93 | input = inputMs.ToArray(); 94 | } 95 | else 96 | { 97 | input = new byte[stream.Length]; 98 | stream.Read(input, 0, input.Length); 99 | } 100 | 101 | using var compress = compressLevel == null ? new Compressor() : new Compressor(new CompressionOptions(compressLevel.Value)); 102 | var output = compress.Wrap(input); 103 | var ms = new MemoryStream(output); 104 | 105 | if (context != null && context.TryGetValue(Context_MdfKey, out var mdfKey)) 106 | { 107 | int? keyLength; 108 | if (context.TryGetValue(Context_MdfKeyLength, out var kl)) 109 | { 110 | keyLength = Convert.ToInt32(kl); 111 | } 112 | else 113 | { 114 | keyLength = (int?)null; 115 | } 116 | 117 | var mms = PsbExtension.EncodeMdf(ms, (string)mdfKey, keyLength, false); 118 | ms?.Dispose(); //ms disposed 119 | ms = mms; 120 | } 121 | 122 | var shellMs = new MemoryStream((int) (ms.Length + 8)); 123 | shellMs.Write(Signature, 0, 4); 124 | shellMs.Write(BitConverter.GetBytes(unzipLength), 0, 4); 125 | ms.CopyTo(shellMs); 126 | ms.Dispose(); 127 | shellMs.Seek(0, SeekOrigin.Begin); 128 | return shellMs; 129 | } 130 | 131 | } 132 | } -------------------------------------------------------------------------------- /FreeMote.Plugins/Shells/PspShell.cs: -------------------------------------------------------------------------------- 1 | // LZSS decompression by morkt (https://github.com/morkt/GARbro) LICENSE: MIT 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.Composition; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | namespace FreeMote.Plugins.Shells 11 | { 12 | [Export(typeof(IPsbShell))] 13 | [ExportMetadata("Name", "FreeMote.Psp")] 14 | [ExportMetadata("Author", "Ulysses & morkt")] 15 | [ExportMetadata("Comment", "PSP (LZSS) unpack support.")] 16 | class PspShell : IPsbShell 17 | { 18 | public string Name => "PSP"; 19 | 20 | //40 C0 A6 01 FF | 50 53 42 (P S B) 21 | public static byte[] MAGIC => new[] {(byte) 'P', (byte) 'S', (byte) 'B'}; 22 | 23 | public bool IsInShell(Stream stream, Dictionary context = null) 24 | { 25 | var header = new byte[3]; 26 | var pos = stream.Position; 27 | stream.Seek(5, SeekOrigin.Current); 28 | _ = stream.Read(header, 0, 3); 29 | stream.Position = pos; 30 | if (header.SequenceEqual(MAGIC)) 31 | { 32 | return true; 33 | } 34 | 35 | return false; 36 | } 37 | 38 | public MemoryStream ToPsb(Stream stream, Dictionary context = null) 39 | { 40 | MemoryStream ms = null; 41 | using (BinaryReader br = new BinaryReader(stream)) 42 | { 43 | int unpackedSize = br.ReadInt32(); 44 | ms = new MemoryStream(unpackedSize); 45 | using (BinaryWriter bw = new BinaryWriter(ms, Encoding.UTF8, true)) 46 | { 47 | //var output = new byte[unpackedSize]; 48 | //int dst = 0; 49 | var frame = new byte[0x1000]; 50 | int framePos = 1; 51 | while (ms.Length < unpackedSize) 52 | { 53 | int ctl = br.ReadByte(); 54 | for (int bit = 1; ms.Length < unpackedSize && bit != 0x100; bit <<= 1) 55 | { 56 | if (0 != (ctl & bit)) 57 | { 58 | byte b = br.ReadByte(); 59 | bw.Write(frame[framePos++ & 0xFFF] = b); 60 | } 61 | else 62 | { 63 | int hi = br.ReadByte(); 64 | int lo = br.ReadByte(); 65 | int offset = hi << 4 | lo >> 4; 66 | for (int count = 2 + (lo & 0xF); count != 0; --count) 67 | { 68 | byte v = frame[offset++ & 0xFFF]; 69 | bw.Write(frame[framePos++ & 0xFFF] = v); 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | ms.Position = 0; 78 | return ms; 79 | } 80 | 81 | public MemoryStream ToShell(Stream stream, Dictionary context = null) 82 | { 83 | //TODO: 84 | Logger.Log("PSP compression is not supported."); 85 | return null; 86 | //throw new NotImplementedException("PSP compression is not supported."); 87 | } 88 | 89 | public byte[] Signature => null; 90 | } 91 | } -------------------------------------------------------------------------------- /FreeMote.Plugins/Shells/PszShell.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.Composition; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace FreeMote.Plugins.Shells 7 | { 8 | [Export(typeof(IPsbShell))] 9 | [ExportMetadata("Name", "FreeMote.Psz")] 10 | [ExportMetadata("Author", "Ulysses")] 11 | [ExportMetadata("Comment", "PSZ (ZLIB) support.")] 12 | class PszShell : IPsbShell 13 | { 14 | public string Name => "PSZ"; 15 | 16 | public bool IsInShell(Stream stream, Dictionary context = null) 17 | { 18 | var header = new byte[4]; 19 | var pos = stream.Position; 20 | _ = stream.Read(header, 0, 4); 21 | stream.Position = pos; 22 | if (header[0] == Name[0] && header[1] == Name[1] && header[2] == Name[2] && header[3] == 0) 23 | { 24 | if (context != null) 25 | { 26 | context[Consts.Context_PsbShellType] = Name; 27 | } 28 | return true; 29 | } 30 | 31 | return false; 32 | } 33 | 34 | public MemoryStream ToPsb(Stream stream, Dictionary context = null) 35 | { 36 | using (var br = new BinaryReader(stream)) 37 | { 38 | br.ReadBytes(4); //PSZ 39 | var zippedLen = br.ReadInt32(); 40 | var oriLen = br.ReadInt32(); 41 | br.ReadInt32(); //0 42 | br.ReadByte(); //0x78 43 | var config = br.ReadByte(); //0x9C: fast; 0xDA: compact 44 | if (context != null) 45 | { 46 | context[Consts.Context_PsbZlibFastCompress] = config == (byte)0x9C; 47 | } 48 | 49 | return ZlibCompress.DecompressToStream(stream) as MemoryStream; 50 | } 51 | } 52 | 53 | public MemoryStream ToShell(Stream stream, Dictionary context = null) 54 | { 55 | bool fast = false; 56 | if (context != null && context.TryGetValue(Consts.Context_PsbZlibFastCompress, out var fastCompress)) 57 | { 58 | fast = (bool)fastCompress; 59 | } 60 | 61 | var oriLen = (int)stream.Length; 62 | var pos = stream.Position; 63 | var compressedStream = ZlibCompress.CompressToStream(stream, fast); 64 | MemoryStream ms = new MemoryStream(16 + (int)compressedStream.Length); 65 | using (var bw = new BinaryWriter(ms, Encoding.UTF8, true)) 66 | { 67 | stream.Position = pos; 68 | var adler32 = new Adler32(); 69 | adler32.Update(stream); 70 | var checksum = (uint)adler32.Checksum; 71 | 72 | bw.Write(Signature); 73 | bw.Write((int)compressedStream.Length + 4); 74 | bw.Write(oriLen); 75 | bw.Write((int)0); 76 | compressedStream.CopyTo(ms); 77 | bw.WriteBE(checksum); 78 | compressedStream.Dispose(); 79 | } 80 | 81 | ms.Position = 0; 82 | return ms; 83 | } 84 | 85 | public byte[] Signature { get; } = { (byte)'P', (byte)'S', (byte)'Z', 0 }; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /FreeMote.PsBuild/Converters/CommonWinConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FreeMote.Psb; 5 | 6 | namespace FreeMote.PsBuild.Converters 7 | { 8 | /// 9 | /// Common/Ems-Win Converter 10 | /// 11 | class CommonWinConverter : ISpecConverter 12 | { 13 | /// 14 | /// Won't be used in this conversion 15 | /// 16 | public SpecConvertOption ConvertOption { get; set; } 17 | /// 18 | /// Won't be used in this conversion 19 | /// 20 | public PsbPixelFormat TargetPixelFormat { get; set; } 21 | public bool UseRL { get; set; } = false; 22 | /// 23 | /// If true, it is an EmsWinConverter 24 | /// 25 | public bool EmsAsCommon { get; set; } = false; 26 | public IList FromSpec { get; } = new List { PsbSpec.win, PsbSpec.common, PsbSpec.ems }; 27 | public IList ToSpec { get; } = new List { PsbSpec.common, PsbSpec.win, PsbSpec.ems }; 28 | public void Convert(PSB psb) 29 | { 30 | if (!FromSpec.Contains(psb.Platform)) 31 | { 32 | throw new FormatException("Can not convert Spec for this PSB"); 33 | } 34 | 35 | var asSpec = EmsAsCommon ? PsbSpec.ems : PsbSpec.common; 36 | var toSpec = psb.Platform == PsbSpec.win ? asSpec : PsbSpec.win; 37 | var toPixelFormat = toSpec == asSpec ? PsbPixelFormat.BeRGBA8 : PsbPixelFormat.LeRGBA8; 38 | var resList = psb.CollectResources(false); 39 | foreach (var resMd in resList) 40 | { 41 | var resourceData = resMd.Resource.Data; 42 | if (resourceData == null) 43 | { 44 | continue; 45 | } 46 | if (resMd.Compress == PsbCompressType.RL) 47 | { 48 | resourceData = RL.Decompress(resourceData); 49 | } 50 | else if (resMd.PixelFormat == PsbPixelFormat.DXT5) 51 | { 52 | resourceData = RL.GetPixelBytesFromImage( 53 | DxtUtil.Dxt5Decode(resourceData, resMd.Width, resMd.Height), toPixelFormat); 54 | resMd.TypeString.Value = toPixelFormat.ToStringForPsb(); 55 | } 56 | else if (resMd.PixelFormat == PsbPixelFormat.DXT1) 57 | { 58 | resourceData = RL.GetPixelBytesFromImage( 59 | DxtUtil.Dxt1Decode(resourceData, resMd.Width, resMd.Height), toPixelFormat); 60 | resMd.TypeString.Value = toPixelFormat.ToStringForPsb(); 61 | } 62 | else 63 | { 64 | RL.Switch_0_2(ref resourceData); 65 | if (UseRL) 66 | { 67 | resourceData = RL.Compress(resourceData); 68 | } 69 | } 70 | resMd.Resource.Data = resourceData; 71 | } 72 | psb.Platform = toSpec; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /FreeMote.PsBuild/Converters/ISpecConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using FreeMote.Psb; 3 | 4 | namespace FreeMote.PsBuild.Converters 5 | { 6 | /// 7 | /// Spec convert strategy 8 | /// 9 | public enum SpecConvertOption 10 | { 11 | /// 12 | /// Best success rate 13 | /// 14 | Default, 15 | /// 16 | /// Remove unnecessary info 17 | /// 18 | Minimum, 19 | /// 20 | /// Keep unnecessary info 21 | /// 22 | Maximum, 23 | } 24 | 25 | /// 26 | /// Convert among 27 | /// 28 | public interface ISpecConverter 29 | { 30 | /// 31 | /// Convert a PSB to target 32 | /// 33 | /// 34 | void Convert(PSB psb); 35 | 36 | SpecConvertOption ConvertOption { get; set; } 37 | 38 | /// 39 | /// Select if supported 40 | /// 41 | PsbPixelFormat TargetPixelFormat { get; set; } 42 | 43 | /// 44 | /// Use RL Compress 45 | /// 46 | bool UseRL { get; set; } 47 | 48 | IList FromSpec { get; } 49 | IList ToSpec { get; } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /FreeMote.PsBuild/FreeMote.PsBuild.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net48 4 | Library 5 | false 6 | 5118e00b-0de7-4935-9013-b6ebe83c8397 7 | 8 | 9 | default 10 | 11 | 12 | default 13 | bin\Release\FreeMote.PsBuild.xml 14 | 15 | 16 | 17 | True 18 | True 19 | Resources.resx 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 13.0.3 29 | 30 | 31 | 4.5.0 32 | 33 | 34 | 35 | 36 | PublicResXFileCodeGenerator 37 | Resources.Designer.cs 38 | 39 | 40 | -------------------------------------------------------------------------------- /FreeMote.PsBuild/MmoCompiler.cs: -------------------------------------------------------------------------------- 1 | namespace FreeMote.PsBuild 2 | { 3 | /// 4 | /// TODO: Managed Open-Source MMO->PSB Compiler 5 | /// When implemented, you can use furi Editor for editing and use this to get PSB 6 | /// 7 | class MmoCompiler 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /FreeMote.PsBuild/MmoTypes.cs: -------------------------------------------------------------------------------- 1 | //This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. 2 | 3 | using System; 4 | using System.Diagnostics; 5 | using FreeMote.Psb; 6 | 7 | // ReSharper disable InconsistentNaming 8 | 9 | namespace FreeMote.PsBuild 10 | { 11 | internal static class MmoExtensions 12 | { 13 | /// 14 | /// Convert number to shape 15 | /// 16 | /// 17 | /// 18 | public static string ToShapeString(this PsbNumber shape) 19 | { 20 | if (Enum.IsDefined(typeof(MmoShape), shape.IntValue)) 21 | { 22 | return ((MmoShape) shape.IntValue).ToString(); 23 | } 24 | 25 | Debug.WriteLine($"{shape.IntValue} is not a valid {nameof(MmoShape)}"); 26 | return MmoShape.point.ToString(); 27 | } 28 | } 29 | 30 | /*particle in frameList/content 31 | * "prt": { 32 | "amax": 5, 33 | "amin": 4, 34 | "fmax": 10.0, 35 | "fmin": 1.0, 36 | "mask": 63, 37 | "range": 8, 38 | "trigger": 1, 39 | "vmax": 0.05, 40 | "vmin": 0.0333333351, 41 | "zmax": 7, 42 | "zmin": 6 43 | }, 44 | */ 45 | 46 | internal enum MmoShape 47 | { 48 | point = 0, 49 | circle = 1, 50 | rect = 2, 51 | quad = 3, 52 | } 53 | 54 | [Flags] 55 | internal enum MmoFrameMaskEx 56 | { 57 | None = 0, 58 | CoordXY = 0b1, 59 | CoordZ = 0b10, 60 | SrcSrc = 0b100, 61 | SrcMotion = 0b1000, 62 | SrcShape = 0b10000, 63 | SrcParticle = 0b100000, 64 | } 65 | 66 | public enum MmoMarkerColor 67 | { 68 | /// 69 | /// なし 70 | /// 71 | None = 0, 72 | 73 | /// 74 | /// 赤 75 | /// 76 | Red = 1, 77 | 78 | /// 79 | /// 绿 80 | /// 81 | Green = 2, 82 | 83 | /// 84 | /// 青 85 | /// 86 | Blue = 3, 87 | 88 | /// 89 | /// 橙 90 | /// 91 | Orange = 4, 92 | 93 | /// 94 | /// 紫 95 | /// 96 | Purple = 5, 97 | 98 | /// 99 | /// 桃 100 | /// 101 | Pink = 6, 102 | 103 | /// 104 | /// 灰 105 | /// 106 | Gray = 7, 107 | } 108 | 109 | public enum MmoItemClass 110 | { 111 | ObjLayerItem = 0, //CharaItem, MotionItem 112 | 113 | //"objClipping": 0, 114 | //"objMaskThresholdOpacity": 64, 115 | //"objTriPriority": 2, 116 | //TextLayer is always hold by a ObjLayer with "#text00000" label and "src/#font00000/#text00000" frameList/content/src 117 | ShapeLayerItem = 1, 118 | 119 | //"shape": "point" (psb: 0) | "circle" (psb: 1) | "rect" (psb: 2) | "quad" (psb: 3) 120 | LayoutLayerItem = 2, 121 | MotionLayerItem = 3, 122 | 123 | /* 124 | "motionClipping": 0, 125 | "motionIndependentLayerInherit": 0, 126 | "motionMaskThresholdOpacity": 64, 127 | */ 128 | ParticleLayerItem = 4, //global.LAYER_TYPE_PARTICLE = (int)4 129 | //"particle": "point" (psb: 0) | "ellipse" (psb: 1) | "quad" (psb: 2) 130 | /* 131 | "particleAccelRatio": 1.0, 132 | "particleApplyZoomToVelocity": 0, 133 | "particleDeleteOutsideScreen": 0, 134 | "particleFlyDirection": 0, 135 | "particleInheritAngle": 0, 136 | "particleInheritOpacity": 1, 137 | "particleInheritVelocity": 0, 138 | "particleMaxNum": 20, 139 | "particleMotionList": [], 140 | "particleTriVolume": 0, 141 | */ 142 | 143 | CameraLayerItem = 5, 144 | ModelLayerItem = 6, 145 | ClipLayerItem = 7, //nothing special 146 | TextLayerItem = 8, 147 | AnchorLayerItem = 9, 148 | FeedbackLayerItem = 10, 149 | /* 150 | "fontParams": { 151 | "antiAlias": 1, 152 | "bold": 0, 153 | "brushColor1": -16777216, 154 | "brushColor2": -16777216, 155 | "depth": 1, 156 | "name": "MS ゴシック", 157 | "penColor": -16777216, 158 | "penSize": 0, 159 | "rev": 1, 160 | "size": 16 161 | }, 162 | "textParams": { 163 | "alignment": 0, 164 | "colSpace": 0, 165 | "defaultVertexColor": -1, 166 | "originAlignment": 1, 167 | "rasterlize": 2, 168 | "rowSpace": 0, 169 | "text": "Built by FreeMote" 170 | }, 171 | */ 172 | MeshLayerItem = 11, //nothing special, take care of meshXXX 173 | StencilLayerItem = 12, 174 | /* 175 | "stencilCompositeMaskLayerList": [], 176 | "stencilMaskThresholdOpacity": 64, 177 | "stencilType": 1, 178 | */ 179 | } 180 | 181 | /// 182 | /// MMO Meta Format for PSD 183 | /// 184 | public class MmoPsdMetadata 185 | { 186 | public string SourceLabel { get; set; } 187 | public string PsdComment { get; set; } 188 | public string PsdFrameLabel { get; set; } 189 | public string PsdGroup { get; set; } 190 | 191 | /// 192 | /// Category 193 | /// WARNING: Category can not be set as `Expression` 194 | /// 195 | public string Category { get; set; } 196 | 197 | public string Label { get; set; } 198 | } 199 | 200 | public partial class MmoBuilder 201 | { 202 | } 203 | } -------------------------------------------------------------------------------- /FreeMote.PsBuild/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("FreeMote.PsBuild")] 6 | [assembly: AssemblyDescription("Managed PSB Compiler & Decompiler")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("Ulysses")] 9 | [assembly: AssemblyProduct("FreeMote")] 10 | [assembly: AssemblyCopyright("Copyright © Ulysses 2017-2025")] 11 | [assembly: AssemblyTrademark("wdwxy12345@gmail.com")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: Guid("717a63de-e599-4134-92ab-40a17c326da3")] 17 | [assembly: InternalsVisibleTo("FreeMote.Tests")] 18 | [assembly: InternalsVisibleTo("FreeMote.Editor")] 19 | 20 | // [assembly: AssemblyVersion("1.0.*")] 21 | [assembly: AssemblyVersion("4.0.1.0")] 22 | [assembly: AssemblyFileVersion("4.0.1.0")] -------------------------------------------------------------------------------- /FreeMote.PsBuild/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace FreeMote.PsBuild.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// 一个强类型的资源类,用于查找本地化的字符串等。 17 | /// 18 | // 此类是由 StronglyTypedResourceBuilder 19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen 21 | // (以 /str 作为命令选项),或重新生成 VS 项目。 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | public class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// 返回此类使用的缓存的 ResourceManager 实例。 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | public static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FreeMote.PsBuild.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// 重写当前线程的 CurrentUICulture 属性,对 51 | /// 使用此强类型资源类的所有资源查找执行重写。 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | public static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// 查找类似 { 65 | /// "bustControlParameterDefinitionList": [ 66 | /// { 67 | /// "comment": "", 68 | /// "friction": 0.125, 69 | /// "gravity": 0.3, 70 | /// "label": "ふらり", 71 | /// "scale_x": 1.0, 72 | /// "scale_y": 1.0, 73 | /// "spring": 0.015625 74 | /// }, 75 | /// { 76 | /// "comment": "", 77 | /// "friction": 0.125, 78 | /// "gravity": 0.3, 79 | /// "label": "標準", 80 | /// "scale_x": 1.0, 81 | /// "scale_y": 2.0, 82 | /// "spring": 0.03125 83 | /// }, 84 | /// { 85 | /// "comment" [字符串的其余部分被截断]"; 的本地化字符串。 86 | /// 87 | public static string Mmo { 88 | get { 89 | return ResourceManager.GetString("Mmo", resourceCulture); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /FreeMote.PsBuild/PsBuildHelper.cs: -------------------------------------------------------------------------------- 1 | using FreeMote.Psb; 2 | 3 | namespace FreeMote.PsBuild 4 | { 5 | public static class PsBuildHelper 6 | { 7 | public static PsbNumber ToPsbNumber(this MmoMarkerColor color) => ((int)color).ToPsbNumber(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /FreeMote.PsBuild/PsbResourceJson.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using FreeMote.Psb; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Converters; 8 | 9 | namespace FreeMote.PsBuild 10 | { 11 | /// 12 | /// Advanced resource json (.resx.json) 13 | /// 14 | /// JsonConvert.SerializeObject(MyObject, new Newtonsoft.Json.Converters.StringEnumConverter()); 15 | [Serializable] 16 | public class PsbResourceJson 17 | { 18 | /// 19 | /// PSB version 20 | /// 21 | public ushort? PsbVersion { get; set; } = 3; 22 | 23 | /// 24 | /// PSB Type 25 | /// 26 | [JsonConverter(typeof(StringEnumConverter))] 27 | public PsbType? PsbType { get; set; } = null; 28 | 29 | /// 30 | /// PSB Spec (only for EMT) 31 | /// 32 | [JsonConverter(typeof(StringEnumConverter))] 33 | public PsbSpec? Platform { get; set; } = null; 34 | 35 | /// 36 | /// Key 37 | /// 38 | public uint? CryptKey { get; set; } = null; 39 | 40 | /// 41 | /// Whether to use external textures 42 | /// 43 | public bool ExternalTextures { get; set; } = false; 44 | 45 | /// 46 | /// Setting Context (mainly for plugins) 47 | /// 48 | [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] 49 | public Dictionary Context { get; set; } = new(); 50 | 51 | /// 52 | /// Resources 53 | /// 54 | public Dictionary Resources { get; set; } 55 | 56 | /// 57 | /// ExtraResources 58 | /// 59 | [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] 60 | public Dictionary ExtraResources { get; set; } 61 | 62 | /// 63 | /// ExtraResources 64 | /// 65 | [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] 66 | public Dictionary ExtraFlattenArrays { get; set; } 67 | 68 | [JsonIgnore] 69 | public bool HasExtraResources => ExtraFlattenArrays is {Count: > 0} || 70 | ExtraResources is {Count: > 0}; 71 | 72 | [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] 73 | public int? Encoding { get; set; } 74 | 75 | public PsbResourceJson() 76 | { 77 | } 78 | 79 | public PsbResourceJson(PSB psb, Dictionary context = null) 80 | { 81 | PsbVersion = psb.Header.Version; 82 | PsbType = psb.Type; 83 | Platform = psb.Platform; 84 | //ExternalTextures = psb.Resources.Count <= 0; 85 | 86 | if (context != null) 87 | { 88 | CryptKey = context.ContainsKey(Consts.Context_CryptKey) 89 | ? (uint?) context[Consts.Context_CryptKey] 90 | : null; 91 | Context = context; 92 | } 93 | } 94 | 95 | public void AppliedToPsb(PSB psb) 96 | { 97 | if (PsbType != null) 98 | { 99 | psb.Type = PsbType.Value; 100 | } 101 | 102 | if (PsbVersion != null) 103 | { 104 | psb.Header.Version = PsbVersion.Value; 105 | } 106 | 107 | if (Platform != null && psb.Platform != PsbSpec.none) 108 | { 109 | psb.Platform = Platform.Value; 110 | } 111 | } 112 | 113 | public string SerializeToJson() 114 | { 115 | return JsonConvert.SerializeObject(this, Formatting.Indented); 116 | } 117 | 118 | /// 119 | /// Load resx.json using psb.json path 120 | /// 121 | /// psb.json path 122 | /// 123 | public static PsbResourceJson LoadByPsbJsonPath(string jsonPath) 124 | { 125 | var inputResPath = Path.ChangeExtension(jsonPath, ".resx.json"); 126 | if (inputResPath != null && File.Exists(inputResPath)) 127 | { 128 | return JsonConvert.DeserializeObject(File.ReadAllText(inputResPath)); 129 | } 130 | 131 | throw new FileNotFoundException($"Can not find {inputResPath}"); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /FreeMote.PsBuild/PsbSpecConverter.cs: -------------------------------------------------------------------------------- 1 | using FreeMote.Psb; 2 | using FreeMote.PsBuild.Converters; 3 | 4 | namespace FreeMote.PsBuild 5 | { 6 | public static class PsbSpecConverter 7 | { 8 | /// 9 | /// Enable resolution support (may scaling images, quality is not guaranteed) 10 | /// 11 | public static bool EnableResolution = false; 12 | 13 | /// 14 | /// Try to switch Spec 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static void SwitchSpec(this PSB psb, PsbSpec targetSpec, 20 | PsbPixelFormat pixelFormat = PsbPixelFormat.None) 21 | { 22 | if (targetSpec is PsbSpec.other or PsbSpec.none) 23 | { 24 | return; 25 | } 26 | 27 | //Alternative //TODO: Alternative table? 28 | bool isAlternative = false; 29 | var realTargetSpec = PsbSpec.common; 30 | 31 | var original = psb.Platform; 32 | if (original == PsbSpec.ems) 33 | { 34 | original = PsbSpec.common; 35 | } 36 | 37 | if (targetSpec == PsbSpec.ems) 38 | { 39 | isAlternative = true; 40 | realTargetSpec = targetSpec; 41 | targetSpec = PsbSpec.common; 42 | } 43 | 44 | if (targetSpec == PsbSpec.krkr) //krkr can not select pixel format 45 | { 46 | switch (original) 47 | { 48 | case PsbSpec.win: 49 | { 50 | Common2KrkrConverter winKrkr = new Common2KrkrConverter(); 51 | winKrkr.Convert(psb); 52 | break; 53 | } 54 | 55 | case PsbSpec.common: 56 | { 57 | Common2KrkrConverter commonKrkr = new Common2KrkrConverter(); 58 | commonKrkr.Convert(psb); 59 | break; 60 | } 61 | 62 | default: 63 | psb.Platform = targetSpec; 64 | break; 65 | } 66 | } 67 | 68 | else if (targetSpec == PsbSpec.win) 69 | { 70 | switch (original) 71 | { 72 | case PsbSpec.krkr: 73 | Krkr2CommonConverter krkr2Win = new Krkr2CommonConverter(true) {EnableResolution = EnableResolution}; 74 | krkr2Win.Convert(psb); 75 | break; 76 | case PsbSpec.common: 77 | CommonWinConverter winCommon = new CommonWinConverter(); 78 | winCommon.Convert(psb); 79 | break; 80 | default: 81 | psb.Platform = targetSpec; 82 | break; 83 | } 84 | } 85 | 86 | else if (targetSpec is PsbSpec.common or PsbSpec.ems) 87 | { 88 | switch (original) 89 | { 90 | case PsbSpec.krkr: 91 | Krkr2CommonConverter krkr2Common = new Krkr2CommonConverter() {EnableResolution = EnableResolution}; 92 | krkr2Common.Convert(psb); 93 | break; 94 | case PsbSpec.win: 95 | CommonWinConverter commonWin = new CommonWinConverter(); 96 | commonWin.Convert(psb); 97 | break; 98 | case PsbSpec.common: 99 | case PsbSpec.ems: 100 | psb.Platform = targetSpec; 101 | break; 102 | default: 103 | psb.Platform = targetSpec; 104 | break; 105 | } 106 | } 107 | 108 | else 109 | { 110 | psb.Platform = targetSpec; 111 | } 112 | 113 | if (isAlternative) 114 | { 115 | psb.Platform = realTargetSpec; 116 | } 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /FreeMote.Psb/FreeMote.Psb.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net48 4 | Library 5 | false 6 | 7 | 8 | default 9 | true 10 | 11 | 12 | default 13 | true 14 | bin\Release\FreeMote.Psb.xml 15 | 16 | 17 | 18 | 19 | 20 | 21 | 2.1.0 22 | 23 | 24 | 2024.1.21.12 25 | 26 | 27 | 4.5.1 28 | 29 | 30 | 31 | 4.5.5 32 | 33 | 34 | 6.0.0 35 | 36 | 37 | 4.5.0 38 | 39 | 40 | 41 | 42 | .editorconfig 43 | 44 | 45 | -------------------------------------------------------------------------------- /FreeMote.Psb/FreeMote.Psb.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /FreeMote.Psb/IPsbType.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | 3 | using System.Collections.Generic; 4 | using FreeMote.Plugins; 5 | using FreeMote.Psb.Types; 6 | 7 | namespace FreeMote.Psb 8 | { 9 | public interface IPsbType 10 | { 11 | /// 12 | /// PSB type 13 | /// 14 | PsbType PsbType { get; } 15 | 16 | /// 17 | /// Check if is this type 18 | /// 19 | /// 20 | /// 21 | bool IsThisType(PSB psb); 22 | 23 | /// 24 | /// Collect Resources 25 | /// 26 | /// 27 | /// 28 | /// 29 | List CollectResources(PSB psb, bool deDuplication = true) where T: class, IResourceMetadata; 30 | 31 | /// 32 | /// Link 33 | /// 34 | /// 35 | /// 36 | /// (legacy) res.json style resource list 37 | /// 38 | /// 39 | void Link(PSB psb, FreeMountContext context, IList resPaths, string baseDir = null, PsbLinkOrderBy order = PsbLinkOrderBy.Convention); 40 | 41 | /// 42 | /// Link 43 | /// 44 | /// 45 | /// 46 | /// 47 | /// 48 | void Link(PSB psb, FreeMountContext context, IDictionary resPaths, string baseDir = null); 49 | 50 | /// 51 | /// Unlink to file 52 | /// 53 | /// 54 | /// 55 | /// resource folder name, could be PSB name itself, or with `-resource` suffix 56 | /// resource folder path 57 | /// whether to save the PSB without texture 58 | /// 59 | /// unlinked PSB path 60 | void UnlinkToFile(PSB psb, FreeMountContext context, string name, string dirPath, bool outputUnlinkedPsb = true, PsbLinkOrderBy order = PsbLinkOrderBy.Name); 61 | 62 | /// 63 | /// Output resources 64 | /// 65 | /// 66 | /// 67 | /// resource folder name, could be PSB name itself, or with `-resource` suffix 68 | /// resource folder path 69 | /// 70 | /// (FileName, RelativePath) 71 | Dictionary OutputResources(PSB psb, FreeMountContext context, string name, string dirPath, PsbExtractOption extractOption = PsbExtractOption.Original); 72 | } 73 | 74 | public partial class PSB 75 | { 76 | internal static readonly Dictionary TypeHandlers = new() 77 | { 78 | {PsbType.Motion, new MotionType()}, 79 | {PsbType.Scn, new ScnType()}, 80 | {PsbType.Tachie, new ImageType()}, 81 | {PsbType.Pimg, new PimgType()}, 82 | {PsbType.Mmo, new MmoType()}, 83 | {PsbType.ArchiveInfo, new ArchiveType()}, 84 | {PsbType.SoundArchive, new SoundArchiveType()}, 85 | {PsbType.BmpFont, new FontType()}, 86 | {PsbType.Map, new MapType()}, 87 | {PsbType.ClutImg, new ClutType()}, 88 | {PsbType.SprBlock, new SprBlockType()}, 89 | {PsbType.SprData, new SprDataType()}, 90 | {PsbType.Chip, new ChipType()}, 91 | {PsbType.PSB, new MotionType()}, //assume as motion type by default, must put this after Motion 92 | }; 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /FreeMote.Psb/IResourceMetadata.cs: -------------------------------------------------------------------------------- 1 | using FreeMote.Plugins; 2 | 3 | namespace FreeMote.Psb 4 | { 5 | public interface IResourceMetadata 6 | { 7 | string Name { get; set; } 8 | public uint Index { get; } 9 | PsbSpec Spec { get; set; } 10 | public PsbType PsbType { get; set; } 11 | void Link(string fullPath, FreeMountContext context); 12 | } 13 | 14 | /// 15 | /// Texture Link Order 16 | /// 17 | public enum PsbLinkOrderBy 18 | { 19 | /// 20 | /// The image name should be FreeMote style: {part}-{name}.{ext} 21 | /// 22 | Convention = 0, 23 | 24 | /// 25 | /// The image name should be EMT Editor style: {name}_tex#{no:D3}.{ext} 26 | /// 27 | Name = 1, 28 | 29 | /// 30 | /// The order in list matters 31 | /// 32 | Order = 2, 33 | } 34 | 35 | /// 36 | /// Compression in PSB 37 | /// 38 | public enum PsbCompressType 39 | { 40 | /// 41 | /// Normal 42 | /// 43 | None, 44 | 45 | /// 46 | /// RLE 47 | /// 48 | RL, 49 | 50 | /// 51 | /// Raw Bitmap 52 | /// 53 | Bmp, 54 | 55 | /// 56 | /// KRKR TLG 57 | /// 58 | Tlg, 59 | 60 | ///// 61 | ///// ASTC 62 | ///// 63 | //Astc, 64 | 65 | ///// 66 | ///// BC7 67 | ///// 68 | //Bc7, 69 | 70 | /// 71 | /// By extension 72 | /// 73 | ByName, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /FreeMote.Psb/Plugins/AudioFileFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.Composition; 3 | using FreeMote.Psb; 4 | 5 | namespace FreeMote.Plugins 6 | { 7 | [Export(typeof(IPsbAudioFormatter))] 8 | [ExportMetadata("Name", "FreeMote.Audio.File")] 9 | [ExportMetadata("Author", "Ulysses")] 10 | [ExportMetadata("Comment", "Export/Import audio file.")] 11 | internal class AudioFileFormatter : IPsbAudioFormatter 12 | { 13 | public List Extensions { get; } = new() { ".wav", ".ogg" }; 14 | public bool CanToWave(IArchData archData, Dictionary context = null) 15 | { 16 | return archData is AudioFileArchData; 17 | } 18 | 19 | public bool CanToArchData(byte[] wave, Dictionary context = null) 20 | { 21 | if (wave == null || wave.Length < 4) 22 | { 23 | return false; 24 | } 25 | 26 | if (wave[0] == 'O' && wave[1] == 'g' && wave[2] == 'g') return true; 27 | if (wave[0] == 'R' && wave[1] == 'I' && wave[2] == 'F' && wave[3] == 'F') return true; 28 | 29 | return false; 30 | } 31 | 32 | public byte[] ToWave(AudioMetadata md, IArchData archData, string fileName = null, Dictionary context = null) 33 | { 34 | var arch = archData as AudioFileArchData; 35 | return arch?.Data?.Data; 36 | } 37 | 38 | public bool ToArchData(AudioMetadata md, IArchData archData, in byte[] wave, string fileName, string waveExt, Dictionary context = null) 39 | { 40 | if (archData is not AudioFileArchData fileArchData) 41 | { 42 | return false; 43 | } 44 | 45 | if (wave[0] == 'O' && wave[1] == 'g' && wave[2] == 'g') 46 | { 47 | fileArchData.Format = PsbAudioFormat.OGG; 48 | } 49 | else //if (wave[0] == 'R' && wave[1] == 'I' && wave[2] == 'F' && wave[3] == 'F') 50 | { 51 | fileArchData.Format = PsbAudioFormat.WAV; 52 | } 53 | 54 | if (fileArchData.Data == null) 55 | { 56 | fileArchData.Data = new PsbResource { Data = wave }; 57 | } 58 | else 59 | { 60 | fileArchData.Data.Data = wave; 61 | } 62 | return true; 63 | } 64 | 65 | public bool TryGetArchData(AudioMetadata md, PsbDictionary channel, out IArchData data, Dictionary context = null) 66 | { 67 | data = null; 68 | if (channel.Count == 1 && channel["archData"] is PsbDictionary archDic && archDic.TryGetPsbValue("background", out var background) && archDic.TryGetPsbValue("filetype", out var fileType) && archDic.TryGetPsbValue("ext", out var ext) && archDic.TryGetPsbValue("data", out var aData)) 69 | { 70 | var newData = new AudioFileArchData 71 | { 72 | Data = aData, 73 | Background = background, 74 | Format = ext == ".ogg" ? PsbAudioFormat.OGG : PsbAudioFormat.WAV 75 | }; 76 | 77 | if (archDic["loop"] is PsbList aLoop) 78 | { 79 | newData.Loop = aLoop; 80 | } 81 | 82 | data = newData; 83 | return true; 84 | } 85 | 86 | return false; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /FreeMote.Psb/Plugins/IPsbAudioFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using FreeMote.Psb; 3 | 4 | namespace FreeMote.Plugins 5 | { 6 | /// 7 | /// Handle audio conversion 8 | /// 9 | public interface IPsbAudioFormatter : IPsbPlugin 10 | { 11 | /// 12 | /// Target Extension (if have) e.g. ".wav" 13 | /// 14 | List Extensions { get; } 15 | 16 | /// 17 | /// Check if is available 18 | /// 19 | /// 20 | /// 21 | /// 22 | bool CanToWave(IArchData archData, Dictionary context = null); 23 | 24 | /// 25 | /// Check if is available 26 | /// 27 | /// 28 | /// 29 | /// 30 | bool CanToArchData(byte[] wave, Dictionary context = null); 31 | 32 | /// 33 | /// Convert to wave bytes 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// 39 | /// 40 | byte[] ToWave(AudioMetadata md, IArchData archData, string fileName = null, Dictionary context = null); 41 | 42 | /// 43 | /// Convert wave bytes to 44 | /// 45 | /// 46 | /// the archData to be filled 47 | /// 48 | /// 49 | /// 50 | /// 51 | /// true if filled. 52 | bool ToArchData(AudioMetadata md, IArchData archData, in byte[] wave, string fileName, string waveExt, Dictionary context = null); 53 | 54 | /// 55 | /// Used when collecting audio resource, the data could be null when compiling 56 | /// 57 | /// is still null at this time 58 | /// an object in [channelList] 59 | /// 60 | /// 61 | /// 62 | bool TryGetArchData(AudioMetadata md, PsbDictionary channel, out IArchData data, Dictionary context = null); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /FreeMote.Psb/Plugins/IPsbImageFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Drawing; 3 | 4 | namespace FreeMote.Plugins 5 | { 6 | public interface IPsbImageFormatter : IPsbPlugin 7 | { 8 | /// 9 | /// Target Extension (if have) e.g. ".png" 10 | /// 11 | List Extensions { get; } 12 | 13 | /// 14 | /// Check if is available 15 | /// 16 | /// 17 | /// 18 | /// 19 | bool CanToBitmap(in byte[] data, Dictionary context = null); 20 | 21 | /// 22 | /// Check if is available 23 | /// 24 | /// 25 | /// 26 | /// 27 | bool CanToBytes(Bitmap bitmap, Dictionary context = null); 28 | 29 | /// 30 | /// Convert image bytes to 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// 38 | Bitmap ToBitmap(in byte[] data, int width, int height, PsbSpec platform, Dictionary context = null); 39 | 40 | /// 41 | /// Convert to image bytes 42 | /// 43 | /// 44 | /// 45 | /// 46 | /// 47 | byte[] ToBytes(Bitmap bitmap, PsbSpec platform, Dictionary context = null); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /FreeMote.Psb/Plugins/IPsbKeyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace FreeMote.Plugins 5 | { 6 | /// 7 | /// Get PSB CryptKey 8 | /// 9 | public interface IPsbKeyProvider : IPsbPlugin 10 | { 11 | /// 12 | /// Try to get PSB key, do not lift the stream position 13 | /// 14 | /// 15 | /// 16 | /// null if no key detected; otherwise give the key 17 | uint? GetKey(Stream stream, Dictionary context = null); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /FreeMote.Psb/Plugins/IPsbPluginInfo.cs: -------------------------------------------------------------------------------- 1 | namespace FreeMote.Plugins 2 | { 3 | public interface IPsbPlugin 4 | { 5 | } 6 | public interface IPsbPluginInfo 7 | { 8 | /// 9 | /// Plugin Name 10 | /// 11 | string Name { get; } 12 | /// 13 | /// Plugin Author 14 | /// 15 | string Author { get; } 16 | /// 17 | /// Comment 18 | /// 19 | string Comment { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /FreeMote.Psb/Plugins/IPsbShell.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace FreeMote.Plugins 5 | { 6 | public interface IPsbShell : IPsbPlugin 7 | { 8 | string Name { get; } 9 | bool IsInShell(Stream stream, Dictionary context = null); 10 | MemoryStream ToPsb(Stream stream, Dictionary context = null); 11 | MemoryStream ToShell(Stream stream, Dictionary context = null); 12 | /// 13 | /// If no signature, should set it to null, use instead 14 | /// 15 | byte[] Signature { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /FreeMote.Psb/Plugins/IPsbSpecialType.cs: -------------------------------------------------------------------------------- 1 | using FreeMote.Psb; 2 | 3 | namespace FreeMote.Plugins 4 | { 5 | public interface IPsbSpecialType : IPsbType, IPsbPlugin 6 | { 7 | string TypeId { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /FreeMote.Psb/Plugins/ManagedTlgFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Drawing; 3 | using System.IO; 4 | 5 | namespace FreeMote.Plugins 6 | { 7 | class ManagedTlgFormatter : IPsbImageFormatter 8 | { 9 | private const string TlgVersion = "TlgVersion"; 10 | 11 | private static TlgImageConverter _managedConverter = null; 12 | 13 | /// 14 | /// Load TLG 15 | /// 16 | /// 17 | /// TLG version, can be 0(unknown),5,6 18 | /// 19 | public static Bitmap LoadTlg(byte[] tlgData, out int version) 20 | { 21 | if (_managedConverter == null) 22 | { 23 | _managedConverter = new TlgImageConverter(); 24 | } 25 | 26 | using (var ms = new MemoryStream(tlgData)) 27 | { 28 | using (var br = new BinaryReader(ms)) 29 | { 30 | var bmp = _managedConverter.ReadAndGetMetaData(br, out var md); 31 | version = md.Version; 32 | return bmp; 33 | } 34 | } 35 | } 36 | 37 | /// 38 | /// Save TLG 39 | /// 40 | /// 41 | /// true: Save as TLG6; false: Save as TLG5 42 | /// 43 | public static byte[] SaveTlg(Bitmap bmp, bool tlg6 = false) 44 | { 45 | return null; 46 | } 47 | 48 | public List Extensions { get; } = new List { ".tlg", ".tlg5", ".tlg6" }; 49 | public bool CanToBitmap(in byte[] data, Dictionary context = null) 50 | { 51 | return true; 52 | } 53 | 54 | public bool CanToBytes(Bitmap bitmap, Dictionary context = null) 55 | { 56 | return false; 57 | } 58 | 59 | public Bitmap ToBitmap(in byte[] data, int width, int height, PsbSpec platform, Dictionary context = null) 60 | { 61 | var bmp = LoadTlg(data, out var v); 62 | if (v > 5 && context != null) 63 | { 64 | context[TlgVersion] = v; 65 | } 66 | 67 | return bmp; 68 | } 69 | 70 | public byte[] ToBytes(Bitmap bitmap, PsbSpec platform, Dictionary context = null) 71 | { 72 | return null; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /FreeMote.Psb/Plugins/WavFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.Composition; 3 | using System.IO; 4 | using FreeMote.Psb; 5 | // ReSharper disable CheckNamespace 6 | 7 | namespace FreeMote.Plugins 8 | { 9 | [Export(typeof(IPsbAudioFormatter))] 10 | [ExportMetadata("Name", "FreeMote.Wav")] 11 | [ExportMetadata("Author", "Ulysses")] 12 | [ExportMetadata("Comment", "Wav/P16 support.")] 13 | class WavFormatter : IPsbAudioFormatter 14 | { 15 | public List Extensions { get; } = new List { ".wav", ".p16" }; 16 | public bool CanToWave(IArchData archData, Dictionary context = null) 17 | { 18 | return archData is WavArchData || archData is P16ArchData; 19 | } 20 | 21 | public bool CanToArchData(byte[] wave, Dictionary context = null) 22 | { 23 | return wave != null; 24 | } 25 | 26 | public byte[] ToWave(AudioMetadata md, IArchData archData, string fileName = null, Dictionary context = null) 27 | { 28 | if (archData is WavArchData arch) 29 | { 30 | return arch.ToWav(); 31 | } 32 | 33 | if (archData is P16ArchData p16) 34 | { 35 | return p16.ToWav(); 36 | } 37 | 38 | return null; 39 | } 40 | 41 | public bool ToArchData(AudioMetadata md, IArchData archData, in byte[] wave, string fileName, string waveExt, Dictionary context = null) 42 | { 43 | if (archData is WavArchData data) 44 | { 45 | WavArchData arch = data; 46 | 47 | using var oms = new MemoryStream(wave); 48 | arch.ReadFromWav(oms); 49 | if (md != null && md.LoopStr != null) 50 | { 51 | arch.Loop = PsbResHelper.ParseLoopStr(md.LoopStr.Value); 52 | } 53 | 54 | return true; 55 | } 56 | 57 | if (archData is P16ArchData p16) 58 | { 59 | P16ArchData arch = p16; 60 | 61 | using var oms = new MemoryStream(wave); 62 | arch.ReadFromWav(oms); 63 | 64 | return true; 65 | 66 | } 67 | 68 | return false; 69 | } 70 | 71 | public bool TryGetArchData(AudioMetadata md, PsbDictionary channel, out IArchData data, Dictionary context = null) 72 | { 73 | data = null; 74 | if (md.Spec == PsbSpec.win) 75 | { 76 | if (channel.Count == 1 && channel["archData"] is PsbDictionary archDic && !archDic.ContainsKey("dpds") && archDic["data"] is PsbResource aData && archDic["fmt"] is PsbResource aFmt && archDic["wav"] is PsbString aWav) 77 | { 78 | var newData = new WavArchData 79 | { 80 | Data = aData, 81 | Fmt = aFmt, 82 | Wav = aWav.Value 83 | }; 84 | 85 | if (archDic["loop"] is PsbList aLoop) 86 | { 87 | newData.Loop = aLoop; 88 | } 89 | 90 | data = newData; 91 | return true; 92 | } 93 | } 94 | 95 | { 96 | if (channel.Count == 1 && channel["archData"] is PsbDictionary p16Arch && p16Arch["filetype"] is PsbString fileType && fileType == "p16" && p16Arch["data"] is PsbResource p16Data && p16Arch["samprate"] is PsbNumber sampRate) 97 | { 98 | var newData = new P16ArchData 99 | { 100 | Data = p16Data, 101 | SampleRate = sampRate.AsInt 102 | }; 103 | data = newData; 104 | return true; 105 | } 106 | } 107 | 108 | // android 2ch P16 109 | { 110 | if (channel["archData"] is PsbDictionary p16Arch && p16Arch["ext"] is PsbString ext && ext == ".p16" && p16Arch["data"] is PsbResource p16Data && p16Arch["samprate"] is PsbNumber sampRate) 111 | { 112 | var newData = new P16ArchData 113 | { 114 | Data = p16Data, 115 | SampleRate = sampRate.AsInt 116 | }; 117 | 118 | ExtractPan(channel, newData); 119 | data = newData; 120 | return true; 121 | } 122 | } 123 | 124 | return false; 125 | } 126 | 127 | private static void ExtractPan(PsbDictionary channel, P16ArchData newData) 128 | { 129 | if (channel["pan"] is PsbList panList) 130 | { 131 | newData.Pan = panList; 132 | 133 | if (panList.Count == 2) 134 | { 135 | var left = panList[0].GetFloat(); 136 | var right = panList[1].GetFloat(); 137 | if (left == 1.0f && right == 0.0f) 138 | { 139 | newData.ChannelPan = PsbAudioPan.Left; 140 | } 141 | else if (left == 0.0f && right == 1.0f) 142 | { 143 | newData.ChannelPan = PsbAudioPan.Right; 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /FreeMote.Psb/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("FreeMote.Psb")] 6 | [assembly: AssemblyDescription("PSB Parser & Painter")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("Ulysses")] 9 | [assembly: AssemblyProduct("FreeMote")] 10 | [assembly: AssemblyCopyright("Copyright © Ulysses 2017-2025")] 11 | [assembly: AssemblyTrademark("wdwxy12345@gmail.com")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: Guid("c0b2c2ff-d8f4-497e-8312-c2af1bb6e7f7")] 17 | [assembly: InternalsVisibleTo("FreeMote.PsBuild")] 18 | [assembly: InternalsVisibleTo("FreeMote.Purify")] 19 | [assembly: InternalsVisibleTo("FreeMote.Plugins")] 20 | [assembly: InternalsVisibleTo("FreeMote.Tests")] 21 | [assembly: InternalsVisibleTo("FreeMote.Editor")] 22 | 23 | // [assembly: AssemblyVersion("1.0.*")] 24 | [assembly: AssemblyVersion("4.0.0.0")] 25 | [assembly: AssemblyFileVersion("4.0.0.0")] 26 | -------------------------------------------------------------------------------- /FreeMote.Psb/Resources/FlattenArrayMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | using FreeMote.Plugins; 5 | 6 | namespace FreeMote.Psb 7 | { 8 | /// 9 | /// Information about FlattenArray such as rawMeshList 10 | /// 11 | public class FlattenArrayMetadata : IResourceMetadata 12 | { 13 | public string Name { get; set; } 14 | public PsbResource Resource { get; set; } 15 | 16 | public uint Index 17 | { 18 | get => Resource.Index ?? uint.MaxValue; 19 | set 20 | { 21 | if (Resource != null) 22 | { 23 | Resource.Index = value; 24 | } 25 | } 26 | } 27 | 28 | public byte[] Data 29 | { 30 | get => Resource?.Data; 31 | 32 | internal set 33 | { 34 | if (Resource == null) 35 | { 36 | throw new NullReferenceException("Resource is null"); 37 | } 38 | 39 | Resource.Data = value; 40 | } 41 | } 42 | 43 | public Span FloatValues => MemoryMarshal.Cast(Data.AsSpan()); 44 | 45 | public PsbSpec Spec { get; set; } 46 | public PsbType PsbType { get; set; } 47 | 48 | public void Link(string fullPath, FreeMountContext context) 49 | { 50 | Data = File.ReadAllBytes(fullPath); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /FreeMote.Psb/Resources/IArchData.cs: -------------------------------------------------------------------------------- 1 | // ArchData in PSB is a typical bad design. It does not describe the true audio format, 2 | // and different formats (such as AT9 and VAG) may share exactly same ArchData structure. 3 | // We have to keep this info in resx.json for re-compile. 4 | // An ArchData may have multiple channels, a channel may have multiple resources, 5 | // making it difficult to re-compile. 6 | 7 | using System.Collections.Generic; 8 | 9 | namespace FreeMote.Psb 10 | { 11 | /// 12 | /// Audio Arch Data (usually a Channel) 13 | /// 14 | public interface IArchData 15 | { 16 | uint Index { get; } 17 | /// 18 | /// Unusual audio extension, such as .vag (must start with dot) 19 | /// 20 | string Extension { get; } 21 | /// 22 | /// Common wave type after decode, usually .wav (must start with dot) 23 | /// 24 | string WaveExtension { get; set; } 25 | PsbAudioFormat Format { get; } 26 | PsbAudioPan ChannelPan { get; } 27 | /// 28 | /// key "data" in PSB 29 | /// 30 | PsbResource Data { get; } 31 | IList DataList { get; } 32 | 33 | /// 34 | /// PSB object "archData" value 35 | /// 36 | PsbDictionary PsbArchData { get; set; } 37 | 38 | /// 39 | /// Generate PSB object "archData" value 40 | /// 41 | /// 42 | IPsbValue ToPsbArchData(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /FreeMote.Psb/Types/ArchiveType.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using FreeMote.Plugins; 3 | 4 | namespace FreeMote.Psb.Types 5 | { 6 | class ArchiveType : IPsbType 7 | { 8 | public PsbType PsbType => PsbType.ArchiveInfo; 9 | public bool IsThisType(PSB psb) 10 | { 11 | return psb.TypeId == "archive" || (psb.TypeId == "scenario" && psb.Objects.ContainsKey("file_info")); //&& psb.Objects.ContainsKey("file_info") 12 | } 13 | 14 | public List CollectResources(PSB psb, bool deDuplication = true) where T : class, IResourceMetadata 15 | { 16 | return new List(); 17 | } 18 | 19 | public void Link(PSB psb, FreeMountContext context, IList resPaths, string baseDir = null, PsbLinkOrderBy order = PsbLinkOrderBy.Convention) 20 | { 21 | } 22 | 23 | public void Link(PSB psb, FreeMountContext context, IDictionary resPaths, string baseDir = null) 24 | { 25 | } 26 | 27 | public void UnlinkToFile(PSB psb, FreeMountContext context, string name, string dirPath, bool outputUnlinkedPsb = true, 28 | PsbLinkOrderBy order = PsbLinkOrderBy.Name) 29 | { 30 | } 31 | 32 | public Dictionary OutputResources(PSB psb, FreeMountContext context, string name, string dirPath, 33 | PsbExtractOption extractOption = PsbExtractOption.Original) 34 | { 35 | return null; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /FreeMote.Psb/Types/BaseImageType.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Drawing; 3 | using FreeMote.Plugins; 4 | 5 | namespace FreeMote.Psb.Types 6 | { 7 | public abstract class BaseImageType 8 | { 9 | public virtual void Link(PSB psb, FreeMountContext context, IList resPaths, string baseDir = null, PsbLinkOrderBy order = PsbLinkOrderBy.Convention) 10 | { 11 | PsbResHelper.LinkImages(psb, context, resPaths, baseDir, order); 12 | } 13 | 14 | public virtual void Link(PSB psb, FreeMountContext context, IDictionary resPaths, string baseDir = null) 15 | { 16 | PsbResHelper.LinkImages(psb, context, resPaths, baseDir); 17 | } 18 | 19 | public virtual List Unlink(PSB psb, PsbLinkOrderBy order = PsbLinkOrderBy.Name, bool disposeResInPsb = true) 20 | { 21 | return PsbResHelper.UnlinkImages(psb, order, disposeResInPsb); 22 | } 23 | 24 | public virtual void UnlinkToFile(PSB psb, FreeMountContext context, string name, string dirPath, bool outputUnlinkedPsb = true, 25 | PsbLinkOrderBy order = PsbLinkOrderBy.Name) 26 | { 27 | PsbResHelper.UnlinkImagesToFile(psb, context, name, dirPath, outputUnlinkedPsb, order); 28 | } 29 | 30 | public virtual Dictionary OutputResources(PSB psb, FreeMountContext context, string name, string dirPath, 31 | PsbExtractOption extractOption = PsbExtractOption.Original) 32 | { 33 | return PsbResHelper.OutputImageResources(psb, context, name, dirPath, extractOption); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /FreeMote.Psb/Types/FontType.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace FreeMote.Psb.Types 5 | { 6 | class FontType : BaseImageType, IPsbType 7 | { 8 | public const string Source = "source"; 9 | public PsbType PsbType => PsbType.BmpFont; 10 | public bool IsThisType(PSB psb) 11 | { 12 | return psb.TypeId == "font"; //&& psb.Objects.ContainsKey("code"); 13 | } 14 | 15 | public List CollectResources(PSB psb, bool deDuplication = true) where T : class, IResourceMetadata 16 | { 17 | //List resourceList = psb.Resources == null 18 | // ? new List() 19 | // : new List(psb.Resources.Count); 20 | 21 | //if (psb.Resources != null) 22 | // resourceList.AddRange(psb.Resources.Select(r => new ImageMetadata { Resource = r }).Cast()); 23 | 24 | var resourceList = FindFontResources(psb, deDuplication).Cast().ToList(); 25 | 26 | return resourceList; 27 | } 28 | 29 | public List FindFontResources(PSB psb, bool deDuplication = true) 30 | { 31 | List resList = new List(psb.Resources.Count); 32 | 33 | if (psb.Objects == null || !psb.Objects.ContainsKey(Source) || !(psb.Objects[Source] is PsbList list)) 34 | { 35 | return resList; 36 | } 37 | 38 | foreach (var item in list) 39 | { 40 | if (item is not PsbDictionary obj) 41 | { 42 | continue; 43 | } 44 | 45 | //TODO: deDuplication for resource (besides pal) 46 | var md = PsbResHelper.GenerateImageMetadata(obj, null); 47 | md.PsbType = PsbType.BmpFont; 48 | md.Spec = psb.Platform; 49 | resList.Add(md); 50 | } 51 | 52 | return resList; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /FreeMote.Psb/Types/MapType.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace FreeMote.Psb.Types 5 | { 6 | /// 7 | /// Tile Map 8 | /// 9 | class MapType : BaseImageType, IPsbType 10 | { 11 | public const string Source = "layer"; 12 | public PsbType PsbType => PsbType.Map; 13 | public bool IsThisType(PSB psb) 14 | { 15 | return psb.TypeId == "map"; 16 | } 17 | 18 | public List CollectResources(PSB psb, bool deDuplication = true) where T : class, IResourceMetadata 19 | { 20 | var resourceList = FindTileResources(psb, deDuplication).Cast().ToList(); 21 | 22 | return resourceList; 23 | } 24 | 25 | private List FindTileResources(PSB psb, bool deDuplication) 26 | { 27 | List resList = new List(psb.Resources.Count); 28 | 29 | if (psb.Objects == null || !psb.Objects.ContainsKey(Source) || psb.Objects[Source] is not PsbList list) 30 | { 31 | return resList; 32 | } 33 | 34 | foreach (var item in list) 35 | { 36 | if (item is not PsbDictionary obj || !obj.ContainsKey("image") || obj["image"] is not PsbDictionary image) 37 | { 38 | continue; 39 | } 40 | 41 | var md = PsbResHelper.GenerateImageMetadata(image, null); 42 | if (obj.ContainsKey("label") && (string.IsNullOrEmpty(md.Name) || md.Name == "image")) 43 | { 44 | md.Name = obj["label"].ToString(); 45 | } 46 | md.PsbType = PsbType.Map; 47 | md.Spec = psb.Platform; 48 | resList.Add(md); 49 | } 50 | 51 | return resList; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /FreeMote.Psb/Types/MmoType.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace FreeMote.Psb.Types 5 | { 6 | class MmoType : BaseImageType, IPsbType 7 | { 8 | public const string MmoSourceKey = "sourceChildren"; 9 | public const string MmoBgSourceKey = "bgChildren"; 10 | 11 | public PsbType PsbType => PsbType.Mmo; 12 | public bool IsThisType(PSB psb) 13 | { 14 | if (psb.Objects == null) 15 | { 16 | return false; 17 | } 18 | return psb.Objects.ContainsKey("objectChildren") && psb.Objects.ContainsKey("sourceChildren"); 19 | } 20 | 21 | public List CollectResources(PSB psb, bool deDuplication = true) where T : class, IResourceMetadata 22 | { 23 | List resourceList = psb.Resources == null 24 | ? new List() 25 | : new List(psb.Resources.Count); 26 | 27 | FindMmoResources(resourceList, psb.Objects[MmoBgSourceKey], MmoBgSourceKey, deDuplication); 28 | FindMmoResources(resourceList, psb.Objects[MmoSourceKey], MmoSourceKey, deDuplication); 29 | 30 | resourceList.ForEach(r => 31 | { 32 | r.PsbType = PsbType.Mmo; 33 | r.Spec = psb.Platform; 34 | }); 35 | return resourceList; 36 | } 37 | 38 | private static void FindMmoResources(List list, IPsbValue obj, in string defaultPartname = "", 39 | bool deDuplication = true) where T: IResourceMetadata 40 | { 41 | switch (obj) 42 | { 43 | case PsbList c: 44 | foreach (var o in c) FindMmoResources(list, o, defaultPartname, deDuplication); 45 | break; 46 | case PsbDictionary d: 47 | if (d[Consts.ResourceKey] is PsbResource r) 48 | { 49 | if (!deDuplication) 50 | { 51 | list.Add((T)(IResourceMetadata)GenerateMmoResMetadata(d, defaultPartname, r)); 52 | } 53 | else if (r.Index == null || list.FirstOrDefault(md => md.Index == r.Index.Value) == null) 54 | { 55 | list.Add((T)(IResourceMetadata)GenerateMmoResMetadata(d, defaultPartname, r)); 56 | } 57 | } 58 | 59 | foreach (var o in d.Values) 60 | { 61 | FindMmoResources(list, o, defaultPartname, deDuplication); 62 | } 63 | 64 | break; 65 | } 66 | } 67 | 68 | private static ImageMetadata GenerateMmoResMetadata(PsbDictionary d, string defaultPartName = "", 69 | PsbResource r = null) 70 | { 71 | if (r == null) 72 | { 73 | r = d.Values.FirstOrDefault(v => v is PsbResource) as PsbResource; 74 | } 75 | 76 | var dd = d.Parent.Parent as PsbDictionary ?? d; 77 | 78 | string name = ""; 79 | string part = defaultPartName; 80 | if ((dd["label"]) is PsbString lbl) 81 | { 82 | name = lbl.Value; 83 | } 84 | 85 | //if (dd.Parent.Parent["className"] is PsbString className) 86 | //{ 87 | // part = className; 88 | //} 89 | 90 | bool is2D = false; 91 | var compress = PsbCompressType.None; 92 | if (d["compress"] is PsbString sc) 93 | { 94 | is2D = true; 95 | if (sc.Value.ToUpperInvariant() == "RL") 96 | { 97 | compress = PsbCompressType.RL; 98 | } 99 | } 100 | 101 | int width = 1, height = 1; 102 | float originX = 0, originY = 0; 103 | if ((d["width"] ?? dd["width"]) is PsbNumber nw) 104 | { 105 | is2D = true; 106 | width = (int)nw; 107 | } 108 | 109 | if ((d["height"] ?? dd["height"]) is PsbNumber nh) 110 | { 111 | is2D = true; 112 | height = (int)nh; 113 | } 114 | 115 | if ((dd["originX"] ?? d["originX"]) is PsbNumber nx) 116 | { 117 | is2D = true; 118 | originX = (float)nx; 119 | } 120 | 121 | if ((dd["originY"] ?? d["originY"]) is PsbNumber ny) 122 | { 123 | is2D = true; 124 | originY = (float)ny; 125 | } 126 | 127 | var md = new ImageMetadata() 128 | { 129 | Is2D = is2D, 130 | Compress = compress, 131 | OriginX = originX, 132 | OriginY = originY, 133 | Width = width, 134 | Height = height, 135 | Name = name, 136 | Part = part, 137 | Resource = r, 138 | }; 139 | return md; 140 | } 141 | 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /FreeMote.Psb/Types/MotionType.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using FreeMote.Plugins; 4 | using static FreeMote.Psb.PsbResHelper; 5 | 6 | namespace FreeMote.Psb.Types 7 | { 8 | class MotionType : BaseImageType, IPsbType 9 | { 10 | public const string MotionSourceKey = "source"; 11 | 12 | public PsbType PsbType => PsbType.Motion; 13 | public bool IsThisType(PSB psb) 14 | { 15 | return psb.TypeId == "motion"; 16 | } 17 | 18 | public List CollectResources(PSB psb, bool deDuplication = true) where T : class, IResourceMetadata 19 | { 20 | List resourceList = psb.Resources == null 21 | ? new List() 22 | : new List(psb.Resources.Count); 23 | 24 | if (psb.Objects != null && psb.Objects.ContainsKey(MotionSourceKey)) 25 | { 26 | FindMotionResources(resourceList, psb.Objects[MotionSourceKey], deDuplication); 27 | } 28 | 29 | resourceList.ForEach(r => 30 | { 31 | r.PsbType = psb.Type; 32 | r.Spec = psb.Platform; 33 | }); 34 | 35 | return resourceList; 36 | } 37 | 38 | public override void Link(PSB psb, FreeMountContext context, IList resPaths, string baseDir = null, 39 | PsbLinkOrderBy order = PsbLinkOrderBy.Convention) 40 | { 41 | LinkImages(psb, context, resPaths, baseDir, order, true); 42 | } 43 | 44 | private static void FindMotionResources(List list, IPsbValue obj, bool deDuplication = true) where T: IResourceMetadata 45 | { 46 | switch (obj) 47 | { 48 | case PsbList c: 49 | c.ForEach(o => FindMotionResources(list, o, deDuplication)); 50 | break; 51 | case PsbDictionary d: 52 | if (d[Consts.ResourceKey] is PsbResource r) 53 | { 54 | if (!deDuplication) 55 | { 56 | list.Add((T)(IResourceMetadata)GenerateImageMetadata(d, r)); 57 | } 58 | else if (r.Index == null) 59 | { 60 | list.Add((T)(IResourceMetadata)GenerateImageMetadata(d, r)); 61 | } 62 | else 63 | { 64 | var imd = GenerateImageMetadata(d, r); 65 | if (imd.Palette == null) 66 | { 67 | if (list.FirstOrDefault(md => md.Index == r.Index.Value) == null) 68 | { 69 | list.Add((T) (IResourceMetadata) imd); 70 | } 71 | } 72 | else 73 | { 74 | if (list.FirstOrDefault(md => md is ImageMetadata i && md.Index == r.Index.Value && i.Palette == imd.Palette) == null) 75 | { 76 | list.Add((T) (IResourceMetadata) imd); 77 | } 78 | } 79 | } 80 | } 81 | 82 | foreach (var o in d.Values) 83 | { 84 | FindMotionResources(list, o, deDuplication); 85 | } 86 | 87 | break; 88 | } 89 | } 90 | 91 | /// 92 | /// Add stub to this PSB 93 | /// 94 | /// 95 | internal static List MotionResourceInstrument(PSB psb) 96 | { 97 | if (!psb.Objects.ContainsKey(MotionSourceKey)) 98 | { 99 | return null; 100 | } 101 | 102 | var resources = new List(); 103 | GenerateMotionResourceStubs(resources, psb.Objects[MotionSourceKey]); 104 | return resources; 105 | } 106 | 107 | /// 108 | /// Add stubs ( with null Data) into a Motion PSB. A stub must be linked with a texture, or it will be null after 109 | /// 110 | /// 111 | /// 112 | private static void GenerateMotionResourceStubs(List resources, IPsbValue obj) 113 | { 114 | switch (obj) 115 | { 116 | case PsbList c: 117 | c.ForEach(o => GenerateMotionResourceStubs(resources, o)); 118 | break; 119 | case PsbDictionary d: 120 | if (d.ContainsKey(Consts.ResourceKey) && (d[Consts.ResourceKey] == null || d[Consts.ResourceKey] is PsbNull)) 121 | { 122 | if (d.ContainsKey("width") && d.ContainsKey("height")) 123 | { 124 | //confirmed, add stub 125 | PsbResource res = new PsbResource(); 126 | resources.Add(res); 127 | res.Index = (uint)resources.IndexOf(res); 128 | d[Consts.ResourceKey] = res; 129 | } 130 | } 131 | 132 | foreach (var o in d.Values) 133 | { 134 | GenerateMotionResourceStubs(resources, o); 135 | } 136 | 137 | break; 138 | } 139 | } 140 | 141 | public override Dictionary OutputResources(PSB psb, FreeMountContext context, string name, string dirPath, 142 | PsbExtractOption extractOption = PsbExtractOption.Original) 143 | { 144 | return base.OutputResources(psb, context, name, dirPath, extractOption); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /FreeMote.Psb/Types/PimgType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace FreeMote.Psb.Types 6 | { 7 | class PimgType : BaseImageType, IPsbType 8 | { 9 | public const string PimgSourceKey = "layers"; 10 | 11 | public PsbType PsbType => PsbType.Pimg; 12 | public bool IsThisType(PSB psb) 13 | { 14 | if (psb.Objects == null) 15 | { 16 | return false; 17 | } 18 | 19 | if (psb.Objects.ContainsKey("layers") && psb.Objects.ContainsKey("height") && psb.Objects.ContainsKey("width")) 20 | { 21 | return true; 22 | } 23 | 24 | if (psb.Objects.Any(k => k.Key.Contains(".") && k.Value is PsbResource)) 25 | { 26 | return true; 27 | } 28 | 29 | return false; 30 | } 31 | 32 | public List CollectResources(PSB psb, bool deDuplication = true) where T : class, IResourceMetadata 33 | { 34 | List resourceList = psb.Resources == null 35 | ? new List() 36 | : new List(psb.Resources.Count); 37 | 38 | resourceList.AddRange(psb.Objects.Where(k => k.Value is PsbResource).Select(k => 39 | new ImageMetadata() 40 | { 41 | Name = k.Key, 42 | Resource = k.Value as PsbResource, 43 | Compress = k.Key.EndsWith(".tlg", true, null) ? PsbCompressType.Tlg : PsbCompressType.ByName, 44 | PsbType = PsbType.Pimg, 45 | Spec = psb.Platform 46 | }).Cast()); 47 | FindPimgResources(resourceList, psb.Objects[PimgSourceKey], deDuplication); 48 | 49 | return resourceList; 50 | } 51 | 52 | private static void FindPimgResources(List list, IPsbValue obj, bool deDuplication = true) where T: IResourceMetadata 53 | { 54 | if (obj is PsbList c) 55 | { 56 | foreach (var o in c) 57 | { 58 | if (!(o is PsbDictionary dic)) continue; 59 | if (dic.ContainsKey("layer_id")) 60 | { 61 | int layerId = 0; 62 | if (dic["layer_id"] is PsbString sLayerId && int.TryParse(sLayerId.Value, out var id)) 63 | { 64 | layerId = id; 65 | } 66 | else if (dic["layer_id"] is PsbNumber nLayerId) 67 | { 68 | layerId = nLayerId.IntValue; 69 | } 70 | else 71 | { 72 | Logger.LogWarn($"[WARN] layer_id {dic["layer_id"]} is wrong."); 73 | continue; 74 | } 75 | 76 | var res = 77 | (ImageMetadata) (IResourceMetadata) (list.FirstOrDefault(k => k.Name.StartsWith($"{layerId}.", true, null)) ?? 78 | list.FirstOrDefault(k => k.Name.StartsWith($"{layerId}", true, null))); 79 | if (res == null) 80 | { 81 | continue; 82 | } 83 | 84 | res.Index = (uint)layerId; 85 | //res.Part = layerId.ToString(); 86 | 87 | if (dic["width"] is PsbNumber nw) 88 | { 89 | res.Width = GetIntValue(nw, res.Width); 90 | } 91 | 92 | if (dic["height"] is PsbNumber nh) 93 | { 94 | res.Height = GetIntValue(nh, res.Height); 95 | } 96 | 97 | if (dic["top"] is PsbNumber nt) 98 | { 99 | res.Top = GetIntValue(nt, res.Top); 100 | } 101 | 102 | if (dic["left"] is PsbNumber nl) 103 | { 104 | res.Left = GetIntValue(nl, res.Left); 105 | } 106 | 107 | if (dic["opacity"] is PsbNumber no) 108 | { 109 | res.Opacity = GetIntValue(no, res.Opacity); 110 | } 111 | 112 | if (dic["group_layer_id"] is PsbNumber gLayerId) 113 | { 114 | res.Part = gLayerId.IntValue.ToString(); 115 | } 116 | 117 | if (dic["visible"] is PsbNumber nv) 118 | { 119 | res.Visible = nv.IntValue != 0; 120 | } 121 | 122 | if (dic["name"] is PsbString nn) 123 | { 124 | res.Label = nn.Value; 125 | } 126 | 127 | if (dic["layer_type"] is PsbNumber lt) 128 | { 129 | res.LayerType = lt.IntValue; 130 | } 131 | } 132 | } 133 | } 134 | 135 | int GetIntValue(PsbNumber num, int ori) => deDuplication ? Math.Max((int)num, ori) : (int)num; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /FreeMote.Psb/Types/ScnType.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace FreeMote.Psb.Types 5 | { 6 | class ScnType : BaseImageType, IPsbType 7 | { 8 | public PsbType PsbType => PsbType.Scn; 9 | public bool IsThisType(PSB psb) 10 | { 11 | if (psb.Objects == null) 12 | { 13 | return false; 14 | } 15 | 16 | if (psb.Objects.ContainsKey("scenes") && psb.Objects.ContainsKey("name")) 17 | { 18 | return true; 19 | } 20 | 21 | if (psb.Objects.ContainsKey("list") && psb.Objects.ContainsKey("map") && psb.Resources?.Count == 0) 22 | { 23 | return true; 24 | } 25 | 26 | return false; 27 | } 28 | 29 | public List CollectResources(PSB psb, bool deDuplication = true) where T : class, IResourceMetadata 30 | { 31 | List resourceList = psb.Resources == null 32 | ? new List() 33 | : new List(psb.Resources.Count); 34 | 35 | resourceList.AddRange(psb.Objects.Where(k => k.Value is PsbResource).Select(k => 36 | new ImageMetadata() 37 | { 38 | Name = k.Key, 39 | Resource = k.Value as PsbResource, 40 | Compress = k.Key.EndsWith(".tlg", true, null) ? PsbCompressType.Tlg : PsbCompressType.ByName, 41 | Spec = psb.Platform, 42 | PsbType = PsbType.Scn 43 | }).Cast()); 44 | 45 | return resourceList; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /FreeMote.Purify/readme.md: -------------------------------------------------------------------------------- 1 | # FreeMote.Purify 2 | 3 | A tool lib to *infer* the key of an EMT PSB file. It doesn't need any other file(e.g. DLL), and can do inference without brute force. It can get the key very fast and is universal for most EMT PSB files. It's NOT based on already known keys. 4 | 5 | # Feature 6 | * Support EMT PSB v2-v4 (only works with *EMT* PSB, not for normal PSB) 7 | * Very fast because it's all from inference rather than brute force 8 | * Can handle both header & body(string) encryption 9 | 10 | It's unreleased (in a standalone repo) for now. It implements `IPsbKeyProvider` and can be used as a FreeMote plugin. 11 | 12 | --- 13 | by Ulysses, wdwxy12345@gmail.com 14 | 15 | 16 | -------------------------------------------------------------------------------- /FreeMote.Tests/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /FreeMote.Tests/FreeMote.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net48 4 | Library 5 | false 6 | default 7 | 8 | 9 | bin\x86\Debug\ 10 | default 11 | MinimumRecommendedRules.ruleset 12 | 13 | 14 | bin\x86\Release\ 15 | default 16 | MinimumRecommendedRules.ruleset 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 2.1.0-CI00002 27 | 28 | 29 | 1.1.0 30 | 31 | 32 | 33 | 3.6.0 34 | 35 | 36 | 3.6.0 37 | 38 | 39 | 13.0.3 40 | 41 | 42 | 6.0.0 43 | 44 | 45 | 2.2.1-CI00002 46 | 47 | 48 | -------------------------------------------------------------------------------- /FreeMote.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("FreeMote.Tests")] 5 | [assembly: AssemblyDescription("Tests for FreeMote.")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("Ulysses")] 8 | [assembly: AssemblyProduct("FreeMote")] 9 | [assembly: AssemblyCopyright("Copyright © Ulysses 2017-2024")] 10 | [assembly: AssemblyTrademark("wdwxy12345@gmail.com")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | [assembly: ComVisible(false)] 14 | 15 | [assembly: Guid("c4077584-00a5-44ce-88a0-1eaf216031ee")] 16 | 17 | // [assembly: AssemblyVersion("1.0.*")] 18 | [assembly: AssemblyVersion("1.0.0.0")] 19 | [assembly: AssemblyFileVersion("1.0.0.0")] 20 | -------------------------------------------------------------------------------- /FreeMote.Tools.EmtConvert/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /FreeMote.Tools.EmtConvert/FreeMote.Tools.EmtConvert.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net48 4 | Exe 5 | EmtConvert 6 | false 7 | default 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 3.0.0 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /FreeMote.Tools.EmtConvert/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("FreeMote.Tools.EmtConvert")] 5 | [assembly: AssemblyDescription("Convert/Compress/Decompress EMT PSBs and images")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("Ulysses")] 8 | [assembly: AssemblyProduct("FreeMote")] 9 | [assembly: AssemblyCopyright("Copyright © Ulysses 2017-2025")] 10 | [assembly: AssemblyTrademark("wdwxy12345@gmail.com")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | [assembly: ComVisible(false)] 14 | 15 | [assembly: Guid("44270712-4acb-447d-835e-9982bd515dbb")] 16 | 17 | [assembly: AssemblyVersion("4.0.0.0")] 18 | [assembly: AssemblyFileVersion("4.0.0.0")] 19 | -------------------------------------------------------------------------------- /FreeMote.Tools.EmtMake/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /FreeMote.Tools.EmtMake/FreeMote.Tools.EmtMake.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net48 4 | Exe 5 | EmtMake 6 | false 7 | default 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 2.3.2 18 | 19 | 20 | 21 | 6.0.0 22 | 23 | 24 | -------------------------------------------------------------------------------- /FreeMote.Tools.EmtMake/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using FreeMote.Plugins; 4 | using FreeMote.Psb; 5 | using FreeMote.PsBuild; 6 | 7 | namespace FreeMote.Tools.EmtMake 8 | { 9 | class Program 10 | { 11 | static void Main(string[] args) 12 | { 13 | Console.WriteLine("FreeMote MMO Decompiler (Preview)"); 14 | Console.WriteLine("by Ulysses, wdwxy12345@gmail.com"); 15 | Logger.InitConsole(); 16 | FreeMount.Init(); 17 | Console.WriteLine(); 18 | Console.WriteLine("This is a preview version. If it crashes, send the PSB to me."); 19 | Console.WriteLine("All output files from this tool should follow [CC BY-NC-SA 4.0] license. Agree this license by pressing Enter:"); 20 | Console.ReadLine(); 21 | if (args.Length < 1 || !File.Exists(args[0])) 22 | { 23 | PrintHelp(); 24 | return; 25 | } 26 | 27 | PSB psb = null; 28 | try 29 | { 30 | psb = new PSB(args[0]); 31 | } 32 | catch (Exception e) 33 | { 34 | Logger.LogError($"Input PSB is invalid: {e.Message}"); 35 | } 36 | 37 | if (psb != null) 38 | { 39 | psb.FixMotionMetadata(); //Fix for partial exported PSB 40 | if (psb.Platform != PsbSpec.krkr) 41 | { 42 | if (psb.Platform == PsbSpec.common || psb.Platform == PsbSpec.win) 43 | { 44 | psb.SwitchSpec(PsbSpec.krkr); 45 | psb.Merge(); 46 | } 47 | else 48 | { 49 | Console.WriteLine( 50 | $"EmtMake do not support {psb.Platform} PSB. Please use pure krkr PSB."); 51 | goto END; 52 | } 53 | } 54 | #if !DEBUG 55 | try 56 | #endif 57 | { 58 | MmoBuilder builder = new MmoBuilder(); 59 | var output = builder.Build(psb); 60 | output.Merge(); 61 | File.WriteAllBytes(Path.ChangeExtension(args[0], ".FreeMote.mmo"), output.Build()); 62 | } 63 | #if !DEBUG 64 | catch (Exception e) 65 | { 66 | Console.WriteLine(e); 67 | } 68 | #endif 69 | } 70 | 71 | END: 72 | Console.WriteLine("Done."); 73 | Console.ReadLine(); 74 | } 75 | 76 | private static void PrintHelp() 77 | { 78 | Console.WriteLine("Usage: .exe "); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /FreeMote.Tools.EmtMake/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("FreeMote.Tools.EmtMake")] 5 | [assembly: AssemblyDescription("MMO/Emtproj Maker")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("Ulysses")] 8 | [assembly: AssemblyProduct("FreeMote")] 9 | [assembly: AssemblyCopyright("Copyright © Ulysses 2018-2025")] 10 | [assembly: AssemblyTrademark("wdwxy12345@gmail.com")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | [assembly: ComVisible(false)] 14 | 15 | [assembly: Guid("bd8855cf-0b99-43b2-8b92-4013e9a0c41c")] 16 | 17 | // [assembly: AssemblyVersion("1.0.*")] 18 | [assembly: AssemblyVersion("4.0.0.0")] 19 | [assembly: AssemblyFileVersion("4.0.0.0")] -------------------------------------------------------------------------------- /FreeMote.Tools.PsBuild/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /FreeMote.Tools.PsBuild/FreeMote.Tools.PsBuild.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net48 4 | Exe 5 | PsBuild 6 | false 7 | default 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 3.0.0 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /FreeMote.Tools.PsBuild/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("FreeMote.Tools.PsBuild")] 5 | [assembly: AssemblyDescription("PSB Compiler")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("Ulysses")] 8 | [assembly: AssemblyProduct("FreeMote")] 9 | [assembly: AssemblyCopyright("Copyright © Ulysses 2017-2025")] 10 | [assembly: AssemblyTrademark("wdwxy12345@gmail.com")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | [assembly: ComVisible(false)] 14 | 15 | [assembly: Guid("d877dbdb-41d7-4c6f-9a46-957cecdaefa8")] 16 | 17 | [assembly: AssemblyVersion("4.0.2.0")] 18 | [assembly: AssemblyFileVersion("4.0.2.0")] 19 | -------------------------------------------------------------------------------- /FreeMote.Tools.PsbDecompile/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /FreeMote.Tools.PsbDecompile/FreeMote.Tools.PsbDecompile.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net48 4 | Exe 5 | PsbDecompile 6 | false 7 | default 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 3.0.0 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /FreeMote.Tools.PsbDecompile/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("FreeMote.Tools.PsbDecompile")] 5 | [assembly: AssemblyDescription("PSB Decompiler")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("Ulysses")] 8 | [assembly: AssemblyProduct("FreeMote")] 9 | [assembly: AssemblyCopyright("Copyright © Ulysses 2017-2025")] 10 | [assembly: AssemblyTrademark("wdwxy12345@gmail.com")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | [assembly: ComVisible(false)] 14 | 15 | [assembly: Guid("943fe440-32bb-4caf-a1da-5f38de7f9b92")] 16 | 17 | [assembly: AssemblyVersion("4.0.2.0")] 18 | [assembly: AssemblyFileVersion("4.0.2.0")] 19 | -------------------------------------------------------------------------------- /FreeMote.Tools.Viewer/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FreeMote.Tools.Viewer/FreeMote.Tools.Viewer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net48 4 | WinExe 5 | FreeMoteViewer 6 | false 7 | true 8 | true 9 | default 10 | true 11 | 12 | 13 | true 14 | 15 | 16 | 17 | 18 | FreeMote.Tools.Viewer.Program 19 | x64 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 3.0.0 28 | 29 | 30 | 31 | 6.0.0 32 | 33 | 34 | 4.5.0 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /FreeMote.Tools.Viewer/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | 14 | 15 | 16 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /FreeMote.Tools.Viewer/PreciseTimer.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace FreeMote.Tools.Viewer 4 | { 5 | /// 6 | /// 精确计时器类 7 | /// 8 | public class PreciseTimer 9 | { 10 | [System.Security.SuppressUnmanagedCodeSecurity] 11 | [DllImport("kernel32")] 12 | private static extern bool QueryPerformanceFrequency(ref long PerformanceFrequency); 13 | [System.Security.SuppressUnmanagedCodeSecurity] 14 | [DllImport("kernel32")] 15 | private static extern bool QueryPerformanceCounter(ref long PerformanceCount); 16 | long _ticksPerSecond = 0; 17 | long _previousElapsedTime = 0; 18 | public PreciseTimer() 19 | { 20 | QueryPerformanceFrequency(ref _ticksPerSecond); 21 | GetElaspedTime();//Get rid of first rubbish result 22 | } 23 | 24 | public double GetElaspedTime() 25 | { 26 | long time = 0; 27 | QueryPerformanceCounter(ref time); 28 | double elapsedTime = (double)(time - _previousElapsedTime) / (double)_ticksPerSecond; 29 | _previousElapsedTime = time; 30 | return elapsedTime; 31 | } 32 | //QueryPerformanceFrequency用于获取高分辨率性能计时器的频率。 33 | //QueryPerformanceCounter用于获取计时器的当前值。 34 | //结合起来确定最后一帧用了多长时间。 35 | //GetElaspedTime应该每帧调用一次,它会记录帧之间经过的时间。 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /FreeMote.Tools.Viewer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | using System.Windows; 4 | 5 | [assembly: AssemblyTitle("FreeMote Viewer")] 6 | [assembly: AssemblyDescription("EMT PSB models Viewer")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("Ulysses")] 9 | [assembly: AssemblyProduct("FreeMote")] 10 | [assembly: AssemblyCopyright("Copyright © Ulysses 2015-2025")] 11 | [assembly: AssemblyTrademark("wdwxy12345@gmail.com")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | // 将 ComVisible 设置为 false 使此程序集中的类型 15 | // 对 COM 组件不可见。 如果需要从 COM 访问此程序集中的类型, 16 | // 则将该类型上的 ComVisible 特性设置为 true。 17 | [assembly: ComVisible(false)] 18 | 19 | //若要开始生成可本地化的应用程序,请在 20 | // 中的 .csproj 文件中 21 | //设置 CultureYouAreCodingWith。 例如,如果您在源文件中 22 | //使用的是美国英语,请将 设置为 en-US。 然后取消 23 | //对以下 NeutralResourceLanguage 特性的注释。 更新 24 | //以下行中的“en-US”以匹配项目文件中的 UICulture 设置。 25 | 26 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 27 | 28 | 29 | [assembly: ThemeInfo( 30 | ResourceDictionaryLocation.None, //主题特定资源词典所处位置 31 | //(在页面或应用程序资源词典中 32 | // 未找到某个资源的情况下使用) 33 | ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置 34 | //(在页面、应用程序或任何主题特定资源词典中 35 | // 未找到某个资源的情况下使用) 36 | )] 37 | 38 | 39 | // 程序集的版本信息由下面四个值组成: 40 | // 41 | // 主版本 42 | // 次版本 43 | // 生成号 44 | // 修订号 45 | // 46 | // 可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值, 47 | // 方法是按如下所示使用“*”: 48 | // [assembly: AssemblyVersion("1.0.*")] 49 | [assembly: AssemblyVersion("4.0.0.0")] 50 | [assembly: AssemblyFileVersion("4.0.0.0")] 51 | -------------------------------------------------------------------------------- /FreeMote.Tools.Viewer/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace FreeMote.Tools.Viewer.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// 一个强类型的资源类,用于查找本地化的字符串等。 17 | /// 18 | // 此类是由 StronglyTypedResourceBuilder 19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen 21 | // (以 /str 作为命令选项),或重新生成 VS 项目。 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// 返回此类使用的缓存的 ResourceManager 实例。 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FreeMote.Tools.Viewer.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// 重写当前线程的 CurrentUICulture 属性,对 51 | /// 使用此强类型资源类的所有资源查找执行重写。 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /FreeMote.Tools.Viewer/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /FreeMote.Tools.Viewer/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace FreeMote.Tools.Viewer.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.2.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /FreeMote.Tools.Viewer/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /FreeMote.Tools.Viewer/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /FreeMote.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | OK 3 | RL 4 | UTF 5 | True 6 | True 7 | True 8 | True 9 | True 10 | True 11 | True 12 | True 13 | True 14 | True 15 | True 16 | True 17 | True 18 | True 19 | True 20 | True 21 | True 22 | True 23 | True 24 | True 25 | True 26 | True 27 | True 28 | True 29 | True 30 | True 31 | True 32 | True 33 | True 34 | True 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /FreeMote/AstcFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace FreeMote 6 | { 7 | /// 8 | /// AstcHeader - 16 bytes 9 | /// 10 | public class AstcHeader 11 | { 12 | public static readonly byte[] Magic = {0x13, 0xAB, 0xA1, 0x5C}; 13 | public const int Length = 16; 14 | 15 | public byte[] Data { get; } 16 | 17 | public AstcHeader() 18 | { 19 | Data = new byte[16]; 20 | } 21 | 22 | public AstcHeader(byte[] data) 23 | { 24 | Data = data; 25 | } 26 | 27 | public Span Signature 28 | { 29 | get => Data.AsSpan(0, 4); 30 | set => value.CopyTo(Data); 31 | } 32 | 33 | public byte BlockX 34 | { 35 | get => Data[4]; 36 | set => Data[4] = value; 37 | } 38 | 39 | public byte BlockY 40 | { 41 | get => Data[5]; 42 | set => Data[5] = value; 43 | } 44 | 45 | public byte BlockZ 46 | { 47 | get => Data[6]; 48 | set => Data[6] = value; 49 | } 50 | 51 | public Span DimX 52 | { 53 | get => Data.AsSpan().Slice(7, 3); 54 | set => value.CopyTo(Data.AsSpan(7, 3)); 55 | } 56 | 57 | public Span DimY { 58 | get => Data.AsSpan().Slice(10, 3); 59 | set => value.CopyTo(Data.AsSpan(10, 3)); 60 | } 61 | public Span DimZ { 62 | get => Data.AsSpan().Slice(13, 3); 63 | set => value.CopyTo(Data.AsSpan(13, 3)); 64 | } 65 | 66 | public int Width 67 | { 68 | get => DimX[0] << 16 | DimX[1] << 8 | DimX[2]; 69 | set => BitConverter.GetBytes(value).AsSpan(0, 3).CopyTo(Data.AsSpan(7, 3)); 70 | } 71 | 72 | public int Height 73 | { 74 | get => DimY[0] << 16 | DimY[1] << 8 | DimY[2]; 75 | set => BitConverter.GetBytes(value).AsSpan(0, 3).CopyTo(Data.AsSpan(10, 3)); 76 | } 77 | 78 | public int Depth 79 | { 80 | get => DimZ[0] << 16 | DimZ[1] << 8 | DimZ[2]; 81 | set => BitConverter.GetBytes(value).AsSpan(0, 3).CopyTo(Data.AsSpan(13, 3)); 82 | } 83 | } 84 | 85 | public static class AstcFile 86 | { 87 | public static AstcHeader ParseAstcHeader(Stream stream) 88 | { 89 | var pos = stream.Position; 90 | if (stream.Length - stream.Position < 16) 91 | { 92 | return null; 93 | } 94 | 95 | byte[] bytes = new byte[16]; 96 | stream.Read(bytes, 0, 16); 97 | stream.Position = pos; 98 | 99 | if (bytes[0] == AstcHeader.Magic[0] && bytes[1] == AstcHeader.Magic[1] && bytes[2] == AstcHeader.Magic[2] && bytes[3] == AstcHeader.Magic[3]) 100 | { 101 | return new AstcHeader(bytes); 102 | } 103 | 104 | return null; 105 | } 106 | 107 | public static AstcHeader ParseAstcHeader(BinaryReader br) 108 | { 109 | var stream = br.BaseStream; 110 | var pos = stream.Position; 111 | if (stream.Length - stream.Position < 16) 112 | { 113 | return null; 114 | } 115 | 116 | var bytes = br.ReadBytes(16); 117 | stream.Position = pos; 118 | 119 | if (bytes[0] == AstcHeader.Magic[0] && bytes[1] == AstcHeader.Magic[1] && bytes[2] == AstcHeader.Magic[2] && bytes[3] == AstcHeader.Magic[3]) 120 | { 121 | return new AstcHeader(bytes); 122 | } 123 | 124 | return null; 125 | } 126 | 127 | public static AstcHeader ParseAstcHeader(byte[] bytes) 128 | { 129 | if (IsAstcHeader(bytes)) 130 | { 131 | return new AstcHeader(bytes.Take(16).ToArray()); 132 | } 133 | 134 | return null; 135 | } 136 | 137 | public static bool IsAstcHeader(in byte[] bytes) 138 | { 139 | if (bytes.Length > 16 && bytes.Take(4).SequenceEqual(AstcHeader.Magic)) 140 | { 141 | return true; 142 | } 143 | 144 | return false; 145 | } 146 | 147 | public static bool IsAstcHeader(BinaryReader br) 148 | { 149 | var stream = br.BaseStream; 150 | var pos = stream.Position; 151 | if (stream.Length - stream.Position < 16) 152 | { 153 | return false; 154 | } 155 | 156 | var bytes = br.ReadBytes(4); 157 | stream.Position = pos; 158 | 159 | return bytes.SequenceEqual(AstcHeader.Magic); 160 | } 161 | 162 | public static bool IsAstcHeader(Stream stream) 163 | { 164 | var pos = stream.Position; 165 | if (stream.Length - stream.Position < 16) 166 | { 167 | return false; 168 | } 169 | 170 | byte[] bytes = new byte[4]; 171 | stream.Read(bytes, 0, 4); 172 | stream.Position = pos; 173 | 174 | return bytes.SequenceEqual(AstcHeader.Magic); 175 | } 176 | 177 | public static byte[] CutHeader(byte[] bts) 178 | { 179 | return IsAstcHeader(bts) ? bts.AsSpan(AstcHeader.Length).ToArray() : bts; 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /FreeMote/BitmapExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Drawing.Drawing2D; 4 | using System.Drawing.Imaging; 5 | 6 | namespace FreeMote 7 | { 8 | public static class BitmapExtension 9 | { 10 | /// 11 | /// Copies a region of the source bitmap into this fast bitmap 12 | /// 13 | /// The image which applies to 14 | /// The source image to copy 15 | /// The region on the source bitmap that will be copied over 16 | /// The region on this fast bitmap that will be changed 17 | /// The provided source bitmap is the same bitmap locked in this FastBitmap 18 | public static void CopyRegion(this Bitmap target, Bitmap source, Rectangle srcRect, Rectangle destRect) 19 | { 20 | if (srcRect.Width != destRect.Width || srcRect.Height != destRect.Height) 21 | { 22 | throw new ArgumentException("Source and destination rectangles must have the same dimensions."); 23 | } 24 | 25 | var srcData = source.LockBits(srcRect, ImageLockMode.ReadOnly, source.PixelFormat); 26 | var destData = target.LockBits(destRect, ImageLockMode.WriteOnly, target.PixelFormat); 27 | 28 | try 29 | { 30 | unsafe 31 | { 32 | byte* srcPtr = (byte*)srcData.Scan0; 33 | byte* destPtr = (byte*)destData.Scan0; 34 | 35 | int bytesPerPixel = Image.GetPixelFormatSize(source.PixelFormat) / 8; 36 | int stride = srcData.Stride; 37 | int height = srcRect.Height; 38 | int widthInBytes = srcRect.Width * bytesPerPixel; 39 | 40 | for (int y = 0; y < height; y++) 41 | { 42 | byte* srcRow = srcPtr + (y * stride); 43 | byte* destRow = destPtr + (y * destData.Stride); 44 | 45 | for (int x = 0; x < widthInBytes; x++) 46 | { 47 | destRow[x] = srcRow[x]; 48 | } 49 | } 50 | } 51 | } 52 | finally 53 | { 54 | source.UnlockBits(srcData); 55 | target.UnlockBits(destData); 56 | } 57 | } 58 | 59 | /// 60 | /// Resize the image to the specified width and height. 61 | /// 62 | /// The image to resize. 63 | /// The width to resize to. 64 | /// The height to resize to. 65 | /// The resized image. 66 | /// https://stackoverflow.com/a/24199315/4374462 67 | public static Bitmap ResizeImage(this Image image, int width, int height) 68 | { 69 | var destRect = new Rectangle(0, 0, width, height); 70 | var destImage = new Bitmap(width, height); 71 | 72 | destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); 73 | 74 | using (var graphics = Graphics.FromImage(destImage)) 75 | { 76 | graphics.CompositingMode = CompositingMode.SourceCopy; 77 | graphics.CompositingQuality = CompositingQuality.HighQuality; 78 | graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; 79 | graphics.SmoothingMode = SmoothingMode.HighQuality; // = SmoothingMode.AntiAlias 80 | graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; 81 | 82 | using (var wrapMode = new ImageAttributes()) 83 | { 84 | wrapMode.SetWrapMode(WrapMode.TileFlipXY); 85 | graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode); 86 | } 87 | } 88 | 89 | return destImage; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /FreeMote/FreeMote.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net48 4 | Library 5 | false 6 | 7 | 8 | true 9 | default 10 | 11 | 12 | 13 | true 14 | default 15 | bin\Release\FreeMote.xml 16 | 1591 17 | 18 | 19 | 20 | 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | 27 | 28 | 2.3.2 29 | 30 | 31 | 4.5.5 32 | 33 | 34 | 6.0.0 35 | 36 | 37 | -------------------------------------------------------------------------------- /FreeMote/Motion/MmoEnums.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FreeMote.Motion 4 | { 5 | [Flags] 6 | public enum TexAttribute: ushort 7 | { 8 | None = 0, 9 | Translucent = 1, 10 | Opaque = 2, 11 | NoDiffuse = 4 12 | } 13 | 14 | [Flags] 15 | public enum MeshSyncChild : ushort 16 | { 17 | None = 0, 18 | Coord = 1, 19 | Angle = 2, 20 | Zoom = 4, 21 | Shape = 8 22 | } 23 | 24 | public enum MeshTransform : short 25 | { 26 | None = 0, 27 | BezierPatch = 1, 28 | Composite = 2 29 | } 30 | 31 | //it's a god-damn krkr enum 32 | internal enum TimelineFrameType: short 33 | { 34 | Null = 0, 35 | Single = 1, 36 | Continuous = 2, 37 | Tween = 3 38 | } 39 | 40 | 41 | // "///^(透過表示|ビュー|レイアウト|レイアウト角度|角度|XY座標|XY座標角度|Z座標|メッシュ|パーティクル|削除|ブレンドモード)\\((.+)\\)$" 42 | // "///^(Transparent|View|Layout|Layout角度|角度|XY座標|XY座標角度|Z座標|Mesh|Particle|Remove|BlendMode)\\((.+)\\)$" 43 | // Some parameters are compile-time constants (BlendMode, Remove etc), it's impossible to recover them if they were default value when compiling 44 | [Flags] 45 | public enum MmoFrameMask 46 | { 47 | //fx,fy, (flip) sx, sy, scc (slant), ccc (coord) 48 | //Low to High: 49 | //0: ox,oy 50 | //1: coord 51 | //2,3: fx, fy (must set both) 52 | //4: angle 53 | //5,6: zx,zy 54 | //7,8: sx,xy 55 | //9: color 56 | //10: opa 57 | //11: ccc 58 | //12: acc (angle) 59 | //13: zcc 60 | //14: scc 61 | //15: occ 62 | //16: ??? 63 | //17: bm 64 | //19: motion/timeOffset? 65 | //20: particle 66 | //25: mesh 67 | None = 0, 68 | 69 | /// 70 | /// ox, oy 71 | /// 72 | Origin = 1, 73 | 74 | /// 75 | /// coord 76 | /// 77 | Coord = 0b10, 78 | 79 | /// 80 | /// angle 81 | /// 82 | Angle = 0b10000, 83 | 84 | /// 85 | /// zx 86 | /// 87 | ZoomX = 0b100000, 88 | 89 | /// 90 | /// zy 91 | /// 92 | ZoomY = 0b1000000, 93 | 94 | /// 95 | /// color 96 | /// 97 | Color = 0b1000000000, 98 | 99 | /// 100 | /// opa 101 | /// 102 | Opacity = 0b10000000000, 103 | 104 | /// 105 | /// ccc 106 | /// 107 | CoordCurve = 0b10_0000000000, 108 | 109 | /// 110 | /// acc 111 | /// 112 | AngleCurve = 0b100_0000000000, 113 | 114 | /// 115 | /// zcc 116 | /// 117 | ZoomCurve = 0b1000_0000000000, 118 | 119 | /// 120 | /// scc 121 | /// 122 | SlantCurve = 0b10000_0000000000, 123 | 124 | /// 125 | /// occ (color cc) 126 | /// 127 | OpacityCurve = 0b100000_0000000000, 128 | 129 | /// 130 | /// bm 131 | /// 132 | BlendMode = 0b10000000_0000000000, 133 | 134 | /// 135 | /// motion 136 | /// 137 | Motion = 0b1000000000_0000000000, 138 | 139 | /// 140 | /// prt 141 | /// 142 | Particle = 0b1_0000000000_0000000000, 143 | 144 | /// 145 | /// mesh 146 | /// 147 | Mesh = 0b100000_0000000000_0000000000, 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /FreeMote/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("FreeMote")] 6 | [assembly: AssemblyDescription("Managed EMT/PSB Tool Library")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("Ulysses")] 9 | [assembly: AssemblyProduct("FreeMote")] 10 | [assembly: AssemblyCopyright("Copyright © Ulysses 2017-2025")] 11 | [assembly: AssemblyTrademark("wdwxy12345@gmail.com")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: Guid("d43ca425-6476-4ae3-a3d8-bbcac0f0383c")] 17 | [assembly: InternalsVisibleTo("FreeMote.PsBuild")] 18 | [assembly: InternalsVisibleTo("FreeMote.Purify")] 19 | [assembly: InternalsVisibleTo("FreeMote.Psb")] 20 | [assembly: InternalsVisibleTo("FreeMote.Plugins")] 21 | [assembly: InternalsVisibleTo("FreeMote.Tests")] 22 | [assembly: InternalsVisibleTo("PsbDecompile")] 23 | [assembly: InternalsVisibleTo("PsBuild")] 24 | [assembly: InternalsVisibleTo("EmtConvert")] 25 | [assembly: InternalsVisibleTo("EmtMake")] 26 | 27 | // [assembly: AssemblyVersion("1.0.*")] 28 | [assembly: AssemblyVersion("4.0.0.0")] 29 | [assembly: AssemblyFileVersion("4.0.0.0")] 30 | -------------------------------------------------------------------------------- /FreeMote/PsbStreamContext.cs: -------------------------------------------------------------------------------- 1 | namespace FreeMote 2 | { 3 | /// 4 | /// PSB Stream Cipher Context (XorShift128) 5 | /// 6 | public class PsbStreamContext 7 | { 8 | /// 9 | /// Key 1 10 | /// Usually you shouldn't modify it. 11 | /// 12 | public uint Key1 { get; set; } 13 | /// 14 | /// Key 2 15 | /// Usually you shouldn't modify it. 16 | /// 17 | public uint Key2 { get; set; } 18 | /// 19 | /// Key 3 20 | /// Usually you shouldn't modify it. 21 | /// 22 | public uint Key3 { get; set; } 23 | /// 24 | /// Key 4 25 | /// This is the key which differs among versions. 26 | /// 27 | public uint Key4 { get; set; } 28 | 29 | /// 30 | /// Current Key 31 | /// 32 | public uint CurrentKey { get; internal set; } = 0; 33 | /// 34 | /// Stream Round 35 | /// 36 | public uint Round { get; internal set; } 37 | /// 38 | /// Round Count in byte 39 | /// 40 | public ulong ByteCount { get; internal set; } 41 | 42 | public PsbStreamContext() 43 | { 44 | Init(); 45 | } 46 | 47 | public PsbStreamContext(uint key) 48 | { 49 | Init(); 50 | Key4 = key; 51 | } 52 | 53 | public void Init() 54 | { 55 | Round = 0; 56 | ByteCount = 0; 57 | Key1 = Consts.Key1; 58 | Key2 = Consts.Key2; 59 | Key3 = Consts.Key3; 60 | CurrentKey = 0; 61 | } 62 | 63 | /// 64 | /// Encode bytes using stream cipher. 65 | /// Every time called, the key might be modified. 66 | /// 67 | /// 68 | /// 69 | public ref byte[] Encode(ref byte[] input) 70 | { 71 | for (int i = 0; i < input.Length; i++) 72 | { 73 | if (CurrentKey == 0) 74 | { 75 | var a = Key1 ^ (Key1 << 11); 76 | var b = Key4; 77 | var c = a ^ b ^ ((a ^ (b >> 11)) >> 8); 78 | Key1 = Key2; 79 | Key2 = Key3; 80 | Key3 = b; 81 | Key4 = c; 82 | CurrentKey = c; 83 | Round++; 84 | } 85 | input[i] ^= (byte) CurrentKey; 86 | CurrentKey = CurrentKey >> 8; 87 | ByteCount++; 88 | } 89 | return ref input; 90 | } 91 | 92 | /// 93 | /// Encode bytes using stream cipher. 94 | /// Every time called, the key might be modified. 95 | /// 96 | /// 97 | /// 98 | public byte[] Encode(byte[] input) 99 | { 100 | for (int i = 0; i < input.Length; i++) 101 | { 102 | if (CurrentKey == 0) 103 | { 104 | var a = Key1 ^ (Key1 << 11); 105 | var b = Key4; 106 | var c = a ^ b ^ ((a ^ (b >> 11)) >> 8); 107 | Key1 = Key2; 108 | Key2 = Key3; 109 | Key3 = b; 110 | Key4 = c; 111 | CurrentKey = c; 112 | Round++; 113 | } 114 | input[i] ^= (byte)CurrentKey; 115 | CurrentKey = CurrentKey >> 8; 116 | ByteCount++; 117 | } 118 | return input; 119 | } 120 | 121 | /// 122 | /// Skip some bytes 123 | /// 124 | /// 125 | public void FastForward(uint byteLength) 126 | { 127 | for (int i = 0; i < byteLength; i++) 128 | { 129 | if (CurrentKey == 0) 130 | { 131 | var a = Key1 ^ (Key1 << 11); 132 | var b = Key4; 133 | var c = a ^ b ^ ((a ^ (b >> 11)) >> 8); 134 | Key1 = Key2; 135 | Key2 = Key3; 136 | Key3 = b; 137 | Key4 = c; 138 | CurrentKey = c; 139 | Round++; 140 | } 141 | CurrentKey = CurrentKey >> 1; 142 | ByteCount++; 143 | } 144 | } 145 | 146 | /// 147 | /// Skip to next round 148 | /// 149 | public uint NextRound() 150 | { 151 | while (CurrentKey != 0) 152 | { 153 | CurrentKey = CurrentKey >> 1; 154 | ByteCount++; 155 | } 156 | var a = Key1 ^ (Key1 << 11); 157 | var b = Key4; 158 | var c = a ^ b ^ ((a ^ (b >> 11)) >> 8); 159 | Key1 = Key2; 160 | Key2 = Key3; 161 | Key3 = b; 162 | Key4 = c; 163 | CurrentKey = c; 164 | Round++; 165 | return CurrentKey; 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /FreeMote/ZlibCompress.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | 4 | namespace FreeMote 5 | { 6 | public static class ZlibCompress 7 | { 8 | public static byte[] Decompress(Stream input) 9 | { 10 | using (var ms = Consts.MsManager.GetStream()) 11 | { 12 | using (DeflateStream deflateStream = new DeflateStream(input, CompressionMode.Decompress)) 13 | { 14 | deflateStream.CopyTo(ms); 15 | } 16 | 17 | //input.Dispose(); 18 | return ms.ToArray(); 19 | } 20 | } 21 | 22 | /// 23 | /// [RequireUsing] 24 | /// 25 | /// 26 | /// 27 | /// 28 | public static Stream DecompressToStream(Stream input, int size = 0) 29 | { 30 | MemoryStream ms = size <= 0 31 | ? Consts.MsManager.GetStream() 32 | : Consts.MsManager.GetStream("DecompressToStream", size); 33 | //var ms = Consts.MsManager.GetStream(); 34 | using (DeflateStream deflateStream = new DeflateStream(input, CompressionMode.Decompress)) 35 | { 36 | deflateStream.CopyTo(ms); 37 | } 38 | 39 | //input.Dispose(); 40 | ms.Seek(0, SeekOrigin.Begin); 41 | return ms; 42 | } 43 | 44 | public static byte[] Compress(Stream input, bool fast = false) 45 | { 46 | using var ms = new MemoryStream(); 47 | ms.WriteByte(0x78); 48 | ms.WriteByte((byte) (fast ? 0x9C : 0xDA)); 49 | using (DeflateStream deflateStream = 50 | new DeflateStream(ms, Consts.ForceCompressionLevel != null ? Consts.ForceCompressionLevel.Value : 51 | fast ? CompressionLevel.Fastest : CompressionLevel.Optimal)) 52 | { 53 | input.CopyTo(deflateStream); 54 | } 55 | 56 | //input.Dispose(); 57 | return ms.GetBuffer(); 58 | } 59 | 60 | public static void CompressToBinaryWriter(BinaryWriter bw, Stream input, bool fast = false) 61 | { 62 | using var output = CompressToStream(input, fast); 63 | output.CopyTo(bw.BaseStream); 64 | } 65 | 66 | /// 67 | /// Will NOT dispose input stream 68 | /// 69 | /// 70 | /// 71 | /// 72 | public static MemoryStream CompressToStream(Stream input, bool fast = false) 73 | { 74 | MemoryStream ms = new MemoryStream(); 75 | ms.WriteByte(0x78); 76 | ms.WriteByte((byte) (fast ? 0x9C : 0xDA)); 77 | using (DeflateStream deflateStream = 78 | new DeflateStream(ms, 79 | Consts.ForceCompressionLevel != null ? Consts.ForceCompressionLevel.Value : 80 | fast ? CompressionLevel.Fastest : CompressionLevel.Optimal, true)) 81 | { 82 | input.CopyTo(deflateStream); 83 | } 84 | 85 | //input.Dispose(); //DO NOT dispose 86 | ms.Position = 0; 87 | return ms; 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. --------------------------------------------------------------------------------