├── .gitattributes ├── .gitignore ├── ArcTool ├── ArcTool.csproj ├── GarArchive.cs ├── IarArchive.cs ├── Program.cs └── WarArchive.cs ├── SAS5Lib ├── Misc.cs ├── SAS5Lib.csproj ├── SecArcFileList.cs ├── SecCode │ ├── ExecutorCommand.cs │ ├── Expression.cs │ ├── ExpressionOperation.cs │ ├── OrphanExpression.cs │ ├── ScenarioCode.cs │ └── SecCodeProp.cs ├── SecCodePage.cs ├── SecOption │ ├── OptionManager.cs │ └── OptionType.cs ├── SecResource │ └── ResourceManager.cs ├── SecScenarioProgram.cs ├── SecSource.cs └── SecVariable │ ├── ObjectType.cs │ ├── PresetVariables.cs │ └── VariableManager.cs ├── SAS5Tool.sln ├── SecTool ├── Program.cs ├── SecTextTool.cs └── SecTool.csproj └── readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd 364 | /SecTool/Properties/launchSettings.json 365 | /ArcTool/Properties/launchSettings.json 366 | -------------------------------------------------------------------------------- /ArcTool/ArcTool.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | true 6 | net8.0 7 | enable 8 | enable 9 | $(SolutionDir)Build\bin 10 | $(SolutionDir)Build\obj\ArcTool\ 11 | False 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ArcTool/GarArchive.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace ArcTool 4 | { 5 | class GarArchive 6 | { 7 | class Block 8 | { 9 | public long Offset; 10 | public long Size; 11 | 12 | public Block(long offset, long size) 13 | { 14 | Offset = offset; 15 | Size = size; 16 | } 17 | 18 | public override string ToString() 19 | { 20 | return $"Offset: {Offset}, Size: {Size}"; 21 | } 22 | } 23 | 24 | struct Collection 25 | { 26 | //int notused 27 | public byte[] CollectionProperty; 28 | //int collectionElemSize 29 | public List CollectionElem; 30 | 31 | public Collection(BinaryReader br) 32 | { 33 | var a = br.ReadInt32(); 34 | var propertySize = br.ReadInt32(); 35 | CollectionProperty = br.ReadBytes(propertySize); 36 | var b = br.ReadInt32(); 37 | CollectionElem = []; 38 | 39 | var elemCount = br.ReadInt32(); 40 | for (int i = 0; i < elemCount; i++) 41 | { 42 | CollectionElem.Add(new CollectionElement(br)); 43 | } 44 | } 45 | 46 | public Collection(List> input) 47 | { 48 | //"\x03BID\x04NAME" 49 | CollectionProperty = [ 0x03, 0x42, 0x49, 0x44, 0x04, 0x4E, 0x41, 0x4D, 0x45 ]; 50 | CollectionElem = []; 51 | 52 | foreach(var item in input) 53 | { 54 | CollectionElem.Add(new CollectionElement(item.Item1, item.Item2)); 55 | } 56 | } 57 | 58 | public void Write(BinaryWriter bw) 59 | { 60 | bw.Write(2); 61 | bw.Write(CollectionProperty.Length); 62 | bw.Write(CollectionProperty); 63 | bw.Write(2); 64 | bw.Write(CollectionElem.Count); 65 | foreach(var elem in CollectionElem) 66 | { 67 | elem.Write(bw); 68 | } 69 | } 70 | } 71 | 72 | struct CollectionElement 73 | { 74 | //int propertyCount = 2; 75 | public List Properties;//BlockID, Name 76 | 77 | public CollectionElement(BinaryReader br) 78 | { 79 | Properties = []; 80 | var propCount = br.ReadInt32(); 81 | for (int i = 0; i < propCount; i++) 82 | { 83 | Property p = new() 84 | { 85 | PropertyIndex = br.ReadInt32(), 86 | Value = new PropertyValueType(br.ReadBytes(br.ReadInt32())) 87 | }; 88 | Properties.Add(p); 89 | } 90 | } 91 | 92 | public CollectionElement(int fileIndex, string fileName) 93 | { 94 | Properties = []; 95 | Properties.Add(new Property(0, new PropertyValueType(fileIndex))); 96 | Properties.Add(new Property(4, new PropertyValueType(fileName))); 97 | } 98 | 99 | public void Write(BinaryWriter bw) 100 | { 101 | bw.Write(2); 102 | foreach(var prop in Properties) 103 | { 104 | prop.Write(bw); 105 | } 106 | } 107 | } 108 | 109 | struct Property 110 | { 111 | public int PropertyIndex;//0 or 4 112 | //int valueSize 113 | public PropertyValueType Value; 114 | 115 | public Property(int index, PropertyValueType val) 116 | { 117 | PropertyIndex = index; 118 | Value = val; 119 | } 120 | 121 | public void Write(BinaryWriter bw) 122 | { 123 | bw.Write(PropertyIndex); 124 | if(Value.Value is int numVal) 125 | { 126 | bw.Write(5); 127 | bw.Write((byte)5); 128 | bw.Write(numVal); 129 | } 130 | else if(Value.Value is string strVal) 131 | { 132 | var b = CodepageManager.Instance.ImportGetBytes(strVal); 133 | bw.Write(b.Length + 1); 134 | bw.Write((byte)1); 135 | bw.Write(b); 136 | } 137 | } 138 | 139 | public override string ToString() 140 | { 141 | return $"{Value}"; 142 | } 143 | } 144 | 145 | struct PropertyValueType 146 | { 147 | public byte Type; 148 | public object? Value; 149 | 150 | public PropertyValueType(byte[] input) 151 | { 152 | Type = input[0]; 153 | switch(input[0]) 154 | { 155 | case 1://File 156 | case 2: 157 | case 3://BID 158 | case 4://NAME 159 | Value = CodepageManager.Instance.ImportGetString(input[1..]); 160 | break; 161 | case 5://File 162 | Value = input[1] | input[2] << 8 | input[3] << 16 | input[4] << 24; 163 | break; 164 | 165 | } 166 | } 167 | 168 | public PropertyValueType(object obj) 169 | { 170 | Value = obj; 171 | } 172 | 173 | public override string ToString() 174 | { 175 | return $"{Value}"; 176 | } 177 | } 178 | 179 | long m_offset; 180 | 181 | //Block -1:? 182 | //Block 0: Header 183 | //Block 1: コレクション 184 | //Block 2: BlockAllocationTable 185 | //Block n(n>2) : Files 186 | readonly Dictionary m_blockAllocationTable; 187 | Collection m_collection; 188 | BinaryReader m_arcReader; 189 | 190 | static readonly byte[] unkSectionData = [ 191 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 192 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 193 | 0x01, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 194 | 0x2C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]; 195 | 196 | public GarArchive(string arcName) 197 | { 198 | m_blockAllocationTable = []; 199 | 200 | m_arcReader = new BinaryReader(File.OpenRead(arcName)); 201 | 202 | var header = m_arcReader.ReadUInt32(); 203 | Trace.Assert(header == 0x20524147);//"GAR " 204 | 205 | var ver1 = m_arcReader.ReadUInt16(); 206 | var ver2 = m_arcReader.ReadUInt16(); 207 | 208 | m_offset = m_arcReader.ReadInt64(); 209 | 210 | var a = m_arcReader.ReadUInt32(); 211 | var b = m_arcReader.ReadUInt32(); 212 | var c = m_arcReader.ReadUInt32(); 213 | 214 | 215 | m_arcReader.BaseStream.Position = m_offset; 216 | var blockCount = m_arcReader.ReadInt32(); 217 | var unk = m_arcReader.ReadInt64();//reader.BaseStream.Position + 20 218 | for (int i = 0; i < blockCount; i++) 219 | { 220 | m_blockAllocationTable.TryAdd(m_arcReader.ReadInt32(), new Block(m_arcReader.ReadInt64(), m_arcReader.ReadInt64())); 221 | } 222 | 223 | if(!m_blockAllocationTable.TryGetValue(1, out var collectionBlock)) 224 | { 225 | Trace.Assert(false); 226 | return; 227 | } 228 | m_arcReader.BaseStream.Position = collectionBlock.Offset; 229 | m_collection = new Collection(m_arcReader); 230 | } 231 | 232 | public void ExtractTo(string outputPath) 233 | { 234 | if(!Path.Exists(outputPath)) 235 | { 236 | Directory.CreateDirectory(outputPath); 237 | } 238 | 239 | foreach(var elem in m_collection.CollectionElem) 240 | { 241 | if(elem.Properties[0].Value.Value is int blockIndex && elem.Properties[1].Value.Value is string fileName) 242 | { 243 | if(m_blockAllocationTable.TryGetValue(blockIndex, out var collectionBlock)) 244 | { 245 | m_arcReader.BaseStream.Position = collectionBlock.Offset; 246 | File.WriteAllBytes(Path.Combine(outputPath, fileName), m_arcReader.ReadBytes(Convert.ToInt32(collectionBlock.Size))); 247 | } 248 | } 249 | } 250 | } 251 | 252 | public static List> Create(string folder, string outputArcName) 253 | { 254 | Dictionary blocks = []; 255 | using var writer = new BinaryWriter(File.Open(Path.Combine(folder, "..", outputArcName), FileMode.Create)); 256 | writer.Write(0x20524147); 257 | writer.Write(1); 258 | long blockOffset = writer.BaseStream.Position; 259 | writer.Write(blockOffset); 260 | writer.Write(0); 261 | writer.Write(2); 262 | writer.Write(1); 263 | blocks.Add(0, new Block(0, writer.BaseStream.Position)); 264 | blocks.Add(-1, new Block(writer.BaseStream.Position, unkSectionData.Length)); 265 | writer.Write(unkSectionData); 266 | 267 | int fileIndex = 3; 268 | List> fileList = []; 269 | foreach(var file in Directory.EnumerateFiles(folder)) 270 | { 271 | byte[] input = File.ReadAllBytes(file); 272 | 273 | blocks.Add(fileIndex, new Block(writer.BaseStream.Position, input.LongLength)); 274 | fileList.Add(new (fileIndex, Path.GetFileName(file))); 275 | 276 | writer.Write(input); 277 | 278 | fileIndex++; 279 | } 280 | var curOffset = writer.BaseStream.Position; 281 | 282 | var collection = new Collection(fileList); 283 | collection.Write(writer); 284 | blocks.Add(1, new Block(curOffset, writer.BaseStream.Position - curOffset)); 285 | 286 | curOffset = writer.BaseStream.Position; 287 | writer.BaseStream.Position = blockOffset; 288 | writer.Write(curOffset); 289 | writer.BaseStream.Position = curOffset; 290 | 291 | writer.Write(blocks.Count + 1); 292 | curOffset = writer.BaseStream.Position; 293 | writer.Write(curOffset); 294 | 295 | foreach(var k in blocks.Keys) 296 | { 297 | writer.Write(k); 298 | var blk = blocks[k]; 299 | writer.Write(blk.Offset); 300 | writer.Write(blk.Size); 301 | } 302 | //last block 303 | writer.Write(2); 304 | writer.Write(curOffset - 4); 305 | var endOffset = writer.BaseStream.Position + 8; 306 | writer.Write(endOffset - curOffset + 24); 307 | 308 | writer.BaseStream.Position = curOffset; 309 | writer.Write(endOffset + 20); 310 | 311 | return fileList; 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /ArcTool/IarArchive.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO.Compression; 3 | using System.Numerics; 4 | using System.Runtime.Intrinsics; 5 | using System.Runtime.Intrinsics.X86; 6 | using System.Text.RegularExpressions; 7 | using ICSharpCode.SharpZipLib.Zip.Compression; 8 | using ICSharpCode.SharpZipLib.Zip.Compression.Streams; 9 | using SixLabors.ImageSharp; 10 | using SixLabors.ImageSharp.PixelFormats; 11 | 12 | namespace ArcTool 13 | { 14 | class IarArchive 15 | { 16 | public List m_arcFileOffset; 17 | public bool m_isArcLongOffset; 18 | public int m_arcVersion; 19 | public BinaryReader m_reader; 20 | public string m_arcName; 21 | 22 | public class IarImage 23 | { 24 | public static byte[] Deflate(byte[] buffer) 25 | { 26 | Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); 27 | using (MemoryStream memoryStream = new MemoryStream()) 28 | using (DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(memoryStream, deflater)) 29 | { 30 | deflaterOutputStream.Write(buffer, 0, buffer.Length); 31 | deflaterOutputStream.Flush(); 32 | deflaterOutputStream.Finish(); 33 | 34 | return memoryStream.ToArray(); 35 | } 36 | } 37 | 38 | public static byte[] Inflate(byte[] buffer) 39 | { 40 | byte[] block = new byte[256]; 41 | MemoryStream outputStream = new MemoryStream(); 42 | 43 | Inflater inflater = new Inflater(); 44 | using (MemoryStream memoryStream = new MemoryStream(buffer)) 45 | using (InflaterInputStream inflaterInputStream = new InflaterInputStream(memoryStream, inflater)) 46 | { 47 | while (true) 48 | { 49 | int numBytes = inflaterInputStream.Read(block, 0, block.Length); 50 | if (numBytes < 1) 51 | break; 52 | outputStream.Write(block, 0, numBytes); 53 | } 54 | } 55 | 56 | return outputStream.ToArray(); 57 | } 58 | 59 | public static void Extract(BinaryReader reader, long offset, int arcVersion, Dictionary fileListDic, string outputPath) 60 | { 61 | var headPos = offset; 62 | reader.BaseStream.Position = offset; 63 | 64 | var Flags = reader.ReadInt16(); 65 | var unk02 = reader.ReadByte(); 66 | var Compressed = reader.ReadByte() != 0; 67 | var unk04 = reader.ReadInt32(); 68 | var UnpackedSize = reader.ReadInt32(); 69 | var PaletteSize = reader.ReadInt32(); 70 | var PackedSize = reader.ReadInt32(); 71 | var unk14 = reader.ReadInt32(); 72 | var OffsetX = reader.ReadInt32(); 73 | var OffsetY = reader.ReadInt32(); 74 | var Width = reader.ReadInt32(); 75 | var Height = reader.ReadInt32(); 76 | var Stride = reader.ReadInt32(); 77 | 78 | var metadataSize = Convert.ToInt32(GetImageHeaderSize(arcVersion) - (reader.BaseStream.Position - headPos)); 79 | var metadataStr = Convert.ToBase64String(Deflate(reader.ReadBytes(metadataSize))).Replace('/', '`'); 80 | 81 | var PaletteData = PaletteSize != 0 ? reader.ReadBytes(PaletteSize) : []; 82 | 83 | 84 | byte[]? ImageData; 85 | if (Compressed) 86 | { 87 | ImageData = new byte[UnpackedSize]; 88 | IarDecompressor.Unpack(reader, ImageData); 89 | } 90 | else 91 | { 92 | ImageData = reader.ReadBytes(PackedSize); 93 | } 94 | 95 | using var imageDataReader = new BinaryReader(new MemoryStream(ImageData)); 96 | //SubLayer 97 | if ((Flags & 0x1000) != 0) 98 | { 99 | using var layerImageOut = File.CreateText($"{outputPath}.layerImg"); 100 | layerImageOut.WriteLine($"LayerImg({Width},{Height},{Flags},{OffsetX},{OffsetY},{Stride},{metadataStr});"); 101 | int offset_x = 0, offset_y = 0; 102 | 103 | while (imageDataReader.BaseStream.Position != imageDataReader.BaseStream.Length) 104 | { 105 | int cmd = imageDataReader.ReadByte(); 106 | switch (cmd) 107 | { 108 | case 0x21: 109 | offset_x += imageDataReader.ReadInt16(); 110 | offset_y += imageDataReader.ReadInt16(); 111 | break; 112 | 113 | case 0x00: 114 | case 0x20: 115 | { 116 | var indexImg = fileListDic[imageDataReader.ReadInt32()]; 117 | 118 | OffsetX -= offset_x; 119 | OffsetY -= offset_y; 120 | if (cmd == 0x20) 121 | { 122 | layerImageOut.WriteLine($"Mask({indexImg},{offset_x},{offset_y});"); 123 | } 124 | else 125 | { 126 | layerImageOut.WriteLine($"Blend({indexImg},{offset_x},{offset_y});"); 127 | } 128 | break; 129 | } 130 | default: 131 | Trace.WriteLine($"Unknown layer type 0x{cmd:X8}", "IAR"); 132 | break; 133 | } 134 | } 135 | layerImageOut.Flush(); 136 | layerImageOut.Close(); 137 | } 138 | //SubImage 139 | else if ((Flags & 0x800) != 0) 140 | { 141 | var baseImgName = fileListDic[imageDataReader.ReadInt32()]; 142 | 143 | using var writer = new BinaryWriter(File.Open($"{outputPath}.base_{baseImgName}.{metadataStr}.subImg", FileMode.Create)); 144 | writer.Write(Flags); 145 | writer.Write(Width); 146 | writer.Write(Height); 147 | writer.Write(OffsetX); 148 | writer.Write(OffsetY); 149 | writer.Write(Stride); 150 | writer.Write(PaletteSize); 151 | if (PaletteSize > 0 && PaletteData != null) 152 | writer.Write(PaletteData); 153 | writer.Write(imageDataReader.ReadBytes(UnpackedSize - 4)); 154 | writer.Flush(); 155 | writer.Close(); 156 | } 157 | else 158 | { 159 | var BPP = (Flags & 0x3E) switch 160 | { 161 | 0x02 => 8, 162 | 0x1C => 24, 163 | 0x3C => 32, 164 | _ => 8 165 | }; 166 | 167 | var NewStride = Width * (BPP / 8); 168 | if (Stride != NewStride) 169 | { 170 | var NewImageData = new byte[Height * NewStride]; 171 | 172 | for (int i = 0; i < Height; i++) 173 | { 174 | Buffer.BlockCopy(ImageData, i * Stride, NewImageData, i * NewStride, NewStride); 175 | } 176 | 177 | ImageData = NewImageData; 178 | } 179 | 180 | switch (BPP) 181 | { 182 | case 8: 183 | { 184 | using var image = Image.LoadPixelData(ImageData, Width, Height); 185 | image.SaveAsPng($"{outputPath}.{OffsetX}_{OffsetY}.{metadataStr}.png"); 186 | break; 187 | } 188 | case 24: 189 | { 190 | using var image = Image.LoadPixelData(ImageData, Width, Height); 191 | image.SaveAsPng($"{outputPath}.{OffsetX}_{OffsetY}.{metadataStr}.png"); 192 | break; 193 | } 194 | case 32: 195 | { 196 | using var image = Image.LoadPixelData(ImageData, Width, Height); 197 | image.SaveAsPng($"{outputPath}.{OffsetX}_{OffsetY}.{metadataStr}.png"); 198 | break; 199 | } 200 | default: throw new NotSupportedException("Not supported IAR image format"); 201 | } 202 | } 203 | } 204 | 205 | public static void Import(BinaryWriter writer, string inputFile, int fileIndex, Dictionary fileMap, int arcVersion) 206 | { 207 | short Flags = 0; 208 | byte unk02 = 0; 209 | byte Compressed; 210 | int unk04 = 0; 211 | int UnpackedSize; 212 | int PaletteSize = 0; 213 | int PackedSize; 214 | int unk14 = 0; 215 | int OffsetX = 0; 216 | int OffsetY = 0; 217 | int Width = 0; 218 | int Height = 0; 219 | int Stride = 0; 220 | byte[]? ImageData = null; 221 | byte[]? PaletteData = null; 222 | byte[]? MetaData = null; 223 | string entryName = ""; 224 | switch (Path.GetExtension(inputFile)) 225 | { 226 | case ".png": 227 | { 228 | var fn = Path.GetFileName(inputFile); 229 | if(fn.Contains("subImg")) 230 | { 231 | return; 232 | } 233 | var match = Regex.Match(fn, @"(.+)\.(.+)_(.+)\.(.+)\.png"); 234 | if(!match.Success) 235 | { 236 | Console.WriteLine($"Invalid png file name: {inputFile}."); 237 | return; 238 | } 239 | entryName = match.Groups[1].Value; 240 | using var image = Image.Load(inputFile); 241 | Width = image.Width; 242 | Height = image.Height; 243 | Stride = image.Width; 244 | OffsetX = Convert.ToInt32(match.Groups[2].Value); 245 | OffsetY = Convert.ToInt32(match.Groups[3].Value); 246 | MetaData = Inflate(Convert.FromBase64String(match.Groups[4].Value.Replace('`', '/'))); 247 | 248 | switch (image.PixelType.BitsPerPixel) 249 | { 250 | case 8: 251 | { 252 | Flags = 2; 253 | ImageData = new byte[Stride * Height]; 254 | image.CloneAs().CopyPixelDataTo(ImageData); 255 | break; 256 | } 257 | case 24: 258 | { 259 | Flags = 0x1C; 260 | Stride *= 3; 261 | ImageData = new byte[Stride * Height]; 262 | image.CloneAs().CopyPixelDataTo(ImageData); 263 | break; 264 | } 265 | case 32: 266 | { 267 | Flags = 0x3C; 268 | Stride *= 4; 269 | ImageData = new byte[Stride * Height]; 270 | image.CloneAs().CopyPixelDataTo(ImageData); 271 | break; 272 | } 273 | default: 274 | { 275 | Console.WriteLine($"Unsupported bpp({image.PixelType.BitsPerPixel}): {inputFile}."); 276 | return; 277 | } 278 | } 279 | break; 280 | } 281 | case ".layerImg": 282 | { 283 | entryName = Path.GetFileNameWithoutExtension(inputFile); 284 | var texts = File.ReadAllLines(inputFile); 285 | if (texts.Length == 0) 286 | { 287 | Console.WriteLine($"Empty layerImg: {inputFile}."); 288 | return; 289 | } 290 | 291 | var header = Regex.Match(texts[0], @"LayerImg\((.+),(.+),(.+),(.+),(.+),(.+),(.+)\);"); 292 | if(!header.Success) 293 | { 294 | Console.WriteLine($"Invalid layerImg property: {inputFile}."); 295 | return; 296 | } 297 | 298 | Width = Convert.ToInt32(header.Groups[1].Value); 299 | Height = Convert.ToInt32(header.Groups[2].Value); 300 | Flags = Convert.ToInt16(header.Groups[3].Value); 301 | OffsetX = Convert.ToInt32(header.Groups[4].Value); 302 | OffsetY = Convert.ToInt32(header.Groups[5].Value); 303 | Stride = Convert.ToInt32(header.Groups[6].Value); 304 | MetaData = Inflate(Convert.FromBase64String(header.Groups[7].Value.Replace('`', '/'))); 305 | 306 | var ms = new MemoryStream(); 307 | 308 | { 309 | var imageDataWriter = new BinaryWriter(ms); 310 | short offset_x = 0, offset_y = 0; 311 | for (int i = 1; i < texts.Length; i++) 312 | { 313 | var cmd = Regex.Match(texts[i], @"(Mask|Blend)\((.+),(.+),(.+)\);"); 314 | 315 | if (cmd.Success) 316 | { 317 | var x = Convert.ToInt16(cmd.Groups[3].Value); 318 | var y = Convert.ToInt16(cmd.Groups[4].Value); 319 | 320 | if (fileMap.TryGetValue(cmd.Groups[2].Value, out int k)) 321 | { 322 | imageDataWriter.Write((byte)0x21); 323 | imageDataWriter.Write(Convert.ToInt16(x - offset_x)); 324 | imageDataWriter.Write(Convert.ToInt16(y - offset_y)); 325 | imageDataWriter.Write(cmd.Groups[1].Value == "Mask" ? (byte)0x20 : (byte)0x00); 326 | imageDataWriter.Write(k); 327 | } 328 | else 329 | { 330 | Console.WriteLine($"Cannot find file {cmd.Groups[2].Value} needed by layerImg {inputFile}, skipping command \"{texts[i]}\"."); 331 | } 332 | offset_x = x; 333 | offset_y = y; 334 | } 335 | else 336 | { 337 | Console.WriteLine($"Invalid layerImg command: {texts[i]}."); 338 | } 339 | } 340 | } 341 | ImageData = ms.ToArray(); 342 | break; 343 | } 344 | case ".subImg": 345 | { 346 | var match = Regex.Match(Path.GetFileName(inputFile), @"(.+)\.base_(.+)\.(.+)\.subImg"); 347 | if(!match.Success) 348 | { 349 | Console.WriteLine($"Invalid subImage file name."); 350 | return; 351 | } 352 | entryName = match.Groups[1].Value; 353 | 354 | if (!fileMap.TryGetValue(match.Groups[2].Value, out int k)) 355 | { 356 | Console.WriteLine($"Cannot find base image {match.Groups[2].Value} needed by {entryName}."); 357 | return; 358 | } 359 | 360 | MetaData = Inflate(Convert.FromBase64String(match.Groups[3].Value.Replace('`', '/'))); 361 | 362 | var ms = new MemoryStream(); 363 | 364 | { 365 | using var reader = new BinaryReader(File.Open(inputFile, FileMode.Open)); 366 | Flags = reader.ReadInt16(); 367 | Width = reader.ReadInt32(); 368 | Height = reader.ReadInt32(); 369 | OffsetX = reader.ReadInt32(); 370 | OffsetY = reader.ReadInt32(); 371 | Stride = reader.ReadInt32(); 372 | PaletteSize = reader.ReadInt32(); 373 | 374 | if (PaletteSize > 0) 375 | PaletteData = reader.ReadBytes(PaletteSize); 376 | 377 | var imageDataWriter = new BinaryWriter(ms); 378 | imageDataWriter.Write(k); 379 | imageDataWriter.Write(reader.ReadBytes(Convert.ToInt32(reader.BaseStream.Length - reader.BaseStream.Position))); 380 | } 381 | ImageData = ms.ToArray(); 382 | break; 383 | } 384 | } 385 | 386 | Trace.Assert(ImageData != null); 387 | UnpackedSize = PackedSize = ImageData.Length; 388 | var packedData = IarCompressor.Pack(ImageData); 389 | 390 | if (UnpackedSize > packedData.Length) 391 | { 392 | Compressed = 1; 393 | ImageData = packedData; 394 | PackedSize = packedData.Length; 395 | } 396 | else 397 | { 398 | Compressed = 0; 399 | } 400 | 401 | var basePos = writer.BaseStream.Position; 402 | 403 | writer.Write(Flags); 404 | writer.Write(unk02); 405 | writer.Write(Compressed); 406 | writer.Write(unk04); 407 | writer.Write(UnpackedSize); 408 | writer.Write(PaletteSize); 409 | writer.Write(PackedSize); 410 | writer.Write(unk14); 411 | writer.Write(OffsetX); 412 | writer.Write(OffsetY); 413 | writer.Write(Width); 414 | writer.Write(Height); 415 | writer.Write(Stride); 416 | 417 | var metadataSize = Convert.ToInt32(GetImageHeaderSize(arcVersion) - (writer.BaseStream.Position - basePos)); 418 | if(MetaData != null) 419 | { 420 | if(MetaData.Length > metadataSize) 421 | { 422 | Console.WriteLine($"Metadata size not match({MetaData.Length}/{metadataSize}), turncating."); 423 | MetaData = MetaData[..metadataSize]; 424 | writer.Write(MetaData); 425 | } 426 | else 427 | { 428 | writer.Write(MetaData); 429 | writer.BaseStream.Position += metadataSize - MetaData.Length; 430 | } 431 | } 432 | else 433 | { 434 | writer.BaseStream.Position += metadataSize; 435 | } 436 | 437 | 438 | if (PaletteSize != 0 && PaletteData != null) 439 | { 440 | writer.Write(PaletteData); 441 | } 442 | writer.Write(ImageData); 443 | 444 | fileMap.TryAdd(entryName, fileIndex); 445 | } 446 | static int GetImageHeaderSize(int iarVersion) 447 | { 448 | switch (iarVersion) 449 | { 450 | case 0x1000: 451 | return 0x30; 452 | case 0x2000: 453 | case 0x3000: 454 | return 0x40; 455 | case 0x4000: 456 | case 0x4001: 457 | case 0x4002: 458 | case 0x4003: 459 | return 0x48; 460 | default: 461 | return 0; 462 | } 463 | } 464 | 465 | internal sealed class IarDecompressor 466 | { 467 | class BitHelper 468 | { 469 | readonly BinaryReader m_reader; 470 | int m_bits = 1; 471 | 472 | public BitHelper(BinaryReader reader) 473 | { 474 | m_reader = reader; 475 | } 476 | 477 | public int GetNextBit() 478 | { 479 | if (1 == m_bits) 480 | { 481 | m_bits = m_reader.ReadUInt16() | 0x10000; 482 | } 483 | int b = m_bits & 1; 484 | m_bits >>= 1; 485 | return b; 486 | } 487 | } 488 | 489 | public static void Unpack(BinaryReader input, byte[] output) 490 | { 491 | var bh = new BitHelper(input); 492 | 493 | int dst = 0; 494 | while (dst < output.Length) 495 | { 496 | if (bh.GetNextBit() == 1) 497 | { 498 | output[dst++] = input.ReadByte(); 499 | continue; 500 | } 501 | int offset, count; 502 | if (bh.GetNextBit() == 1)// 3 <= duplicate count < 272 503 | { 504 | //1~8192 505 | int tmp = bh.GetNextBit(); 506 | if (bh.GetNextBit() == 1) 507 | offset = 1; 508 | else if (bh.GetNextBit() == 1) 509 | offset = 0x201; 510 | else 511 | { 512 | tmp = (tmp << 1) | bh.GetNextBit(); 513 | if (bh.GetNextBit() == 1) 514 | offset = 0x401; 515 | else 516 | { 517 | tmp = (tmp << 1) | bh.GetNextBit(); 518 | if (bh.GetNextBit() == 1) 519 | offset = 0x801; 520 | else 521 | { 522 | offset = 0x1001; 523 | tmp = (tmp << 1) | bh.GetNextBit(); 524 | } 525 | } 526 | } 527 | offset += (tmp << 8) | input.ReadByte(); 528 | 529 | if (bh.GetNextBit() == 1) 530 | count = 3; 531 | else if (bh.GetNextBit() == 1) 532 | count = 4; 533 | else if (bh.GetNextBit() == 1) 534 | count = 5; 535 | else if (bh.GetNextBit() == 1) 536 | count = 6; 537 | else if (bh.GetNextBit() == 1) 538 | count = 7 + bh.GetNextBit(); 539 | else if (bh.GetNextBit() == 1) 540 | count = 17 + input.ReadByte(); //17 ~ 272 541 | else 542 | { 543 | //9 ~ 16 544 | count = bh.GetNextBit() << 2; 545 | count |= bh.GetNextBit() << 1; 546 | count |= bh.GetNextBit(); 547 | count += 9; 548 | } 549 | } 550 | else//duplicate count == 2 551 | { 552 | count = 2; 553 | if (bh.GetNextBit() == 1) 554 | { 555 | //offset = 0x100 ~ 0x8FF (256 ~ 2303) 556 | offset = bh.GetNextBit() << 10; 557 | offset |= bh.GetNextBit() << 9; 558 | offset |= bh.GetNextBit() << 8; 559 | offset = (offset | input.ReadByte()) + 0x100; 560 | } 561 | else 562 | { 563 | //offset = 0x1 ~ 0xFF 564 | offset = 1 + input.ReadByte();//maximum == 0xFE 565 | if (0x100 == offset)//offset == 0xFF -> End 566 | break; 567 | } 568 | } 569 | CopyOverlapped(output, dst - offset, dst, count); 570 | dst += count; 571 | } 572 | } 573 | 574 | public static void CopyOverlapped(byte[] data, int src, int dst, int count) 575 | { 576 | if (dst > src) 577 | { 578 | while (count > 0) 579 | { 580 | int preceding = Math.Min(dst - src, count); 581 | Buffer.BlockCopy(data, src, data, dst, preceding); 582 | dst += preceding; 583 | count -= preceding; 584 | } 585 | } 586 | else 587 | { 588 | Buffer.BlockCopy(data, src, data, dst, count); 589 | } 590 | } 591 | } 592 | 593 | internal sealed class IarCompressor 594 | { 595 | public class BufferWriter 596 | { 597 | private ushort m_ctl; 598 | private int m_pos; 599 | private readonly List m_outputBuf; 600 | private readonly List m_codeBuf; 601 | 602 | static readonly ushort[] elemA = [1, 0x201, 0x401, 0x801, 0x1001]; 603 | static readonly ushort[] elemB = [0x40, 0x20, 8, 2, 0]; 604 | public BufferWriter() 605 | { 606 | m_outputBuf = []; 607 | m_codeBuf = []; 608 | m_ctl = 0; 609 | m_pos = 0; 610 | } 611 | 612 | void SetBits(ushort val, int bitCount = 1) 613 | { 614 | while (bitCount != 0) 615 | { 616 | if (m_pos == 16) 617 | { 618 | Flush(); 619 | } 620 | 621 | m_ctl |= (ushort)((val & 1) << m_pos++); 622 | val >>= 1; 623 | 624 | bitCount--; 625 | } 626 | } 627 | 628 | public void PutUncoded(byte input) 629 | { 630 | SetBits(1); 631 | m_codeBuf.Add(input); 632 | } 633 | 634 | public void PutPair(int offset, int length) 635 | { 636 | if (length < 2) 637 | throw new ArgumentException("Cannot put pair that length lower than 2."); 638 | if (length == 2) 639 | { 640 | SetBits(0, 2); 641 | if (offset <= 0xFF) 642 | { 643 | SetBits(0); 644 | m_codeBuf.Add(Convert.ToByte(offset - 1)); 645 | } 646 | else 647 | { 648 | SetBits(1); 649 | offset -= 0x100; 650 | SetBits(Convert.ToUInt16((offset >> 10) & 1), 1); 651 | SetBits(Convert.ToUInt16((offset >> 9) & 1), 1); 652 | SetBits(Convert.ToUInt16((offset >> 8) & 1), 1); 653 | m_codeBuf.Add(Convert.ToByte(offset & 0xFF)); 654 | } 655 | } 656 | else 657 | { 658 | //repeats greater than 2 bytes 659 | SetBits(2, 2); 660 | byte offsetPart = (byte)((offset & 0xFF) - 1); 661 | 662 | bool flag = false; 663 | for (int j = 0; j <= 0xF; j++) 664 | { 665 | if (flag) 666 | break; 667 | for (int i = 0; i < 5; i++) 668 | { 669 | if (elemA[i] > offset) 670 | continue; 671 | if ((elemA[i] + (j << 8) + offsetPart) == offset) 672 | { 673 | var bitlen = i switch 674 | { 675 | 0 => 2, 676 | 1 => 3, 677 | 2 => 5, 678 | 3 => 7, 679 | _ => 8, 680 | }; 681 | 682 | int code; 683 | if (i < 2) 684 | { 685 | code = (j & 1) << 7 | elemB[i]; 686 | } 687 | else if (i < 3) 688 | { 689 | // 1 << 7 690 | code = (j & 1) << 4 | (j & 2) << 6 | elemB[i]; 691 | } 692 | else if (i < 4) 693 | { 694 | // 1 << 4 1 << 7 695 | code = (j & 1) << 2 | (j & 2) << 3 | (j & 4) << 5 | elemB[i]; 696 | } 697 | else 698 | { 699 | // 1 << 2 1 << 4 1 << 7 700 | code = (j & 1) << 0 | (j & 2) << 1 | (j & 4) << 2 | (j & 8) << 4 | elemB[i]; 701 | } 702 | 703 | for (int b = 0; b < bitlen; b++) 704 | { 705 | SetBits((ushort)((code & 0x80) >> 7)); 706 | code <<= 1; 707 | } 708 | m_codeBuf.Add(Convert.ToByte(offsetPart)); 709 | flag = true; 710 | break; 711 | } 712 | } 713 | } 714 | 715 | switch (length) 716 | { 717 | case 3: 718 | SetBits(1); 719 | break; 720 | case 4: 721 | SetBits(2, 2);//01 722 | break; 723 | case 5: 724 | SetBits(4, 3);//001 725 | break; 726 | case 6: 727 | SetBits(8, 4);//0001 728 | break; 729 | case 7: 730 | SetBits(16, 6);//000010 731 | break; 732 | case 8: 733 | SetBits(48, 6);//000011 734 | break; 735 | default: 736 | { 737 | var count = length; 738 | if (count <= 16) 739 | { 740 | SetBits(0, 6); 741 | count -= 9; 742 | SetBits((ushort)(count >> 2)); 743 | SetBits((ushort)(count >> 1)); 744 | SetBits((ushort)(count >> 0)); 745 | } 746 | else 747 | { 748 | SetBits(32, 6); 749 | count -= 17; 750 | m_codeBuf.Add(Convert.ToByte(count)); 751 | } 752 | break; 753 | } 754 | } 755 | } 756 | } 757 | 758 | public void Flush() 759 | { 760 | m_pos = 0; 761 | 762 | m_outputBuf.Add((byte)m_ctl); 763 | m_outputBuf.Add((byte)(m_ctl >> 8)); 764 | m_ctl = 0; 765 | 766 | m_outputBuf.AddRange(m_codeBuf); 767 | m_codeBuf.Clear(); 768 | } 769 | 770 | public byte[] GetBytes() 771 | { 772 | //Set End Flag 773 | SetBits(0, 3); 774 | m_codeBuf.Add(0xFF); 775 | Flush(); 776 | return m_outputBuf.ToArray(); 777 | } 778 | } 779 | 780 | //RingBuffer, must be power of 2 781 | const int BufferSize = 1 << 13; 782 | //Maximum matching size 783 | const int SearchSize = 255; 784 | //Minimum pair length 785 | const int THRESHOLD = 2; 786 | 787 | const int NIL = BufferSize; 788 | //Original source: https://github.com/opensource-apple/kext_tools/blob/master/compression.c 789 | public class EncodeState 790 | { 791 | /* 792 | * initialize state, mostly the trees 793 | * 794 | * For i = 0 to BufferSize - 1, rchild[i] and lchild[i] will be the right and left 795 | * children of node i. These nodes need not be initialized. Also, parent[i] 796 | * is the parent of node i. These (parent nodes) are initialized to NIL (= BufferSize), which stands 797 | * for 'not used.' For i = 0 to 255, rchild[BufferSize + i + 1] is the root of the 798 | * tree for strings that begin with character i. These are initialized to NIL. 799 | * Note there are 256 trees. 800 | */ 801 | public int[] lchild = new int[BufferSize + 1]; 802 | public int[] rchild = Enumerable.Repeat(NIL, BufferSize + 1 + 256).ToArray(); 803 | public int[] parent = Enumerable.Repeat(NIL, BufferSize + 1).ToArray(); 804 | 805 | public byte[] text_buf = Enumerable.Repeat((byte)0xFF, BufferSize + SearchSize + 1).ToArray(); 806 | public int[] text_buf_map = new int[BufferSize + SearchSize + 1]; 807 | 808 | public int match_position = 0; 809 | public int match_length = 0; 810 | }; 811 | 812 | /* 813 | * Inserts string of (length=SearchSize, text_buf[index..index + SearchSize - 1]) into one of the trees 814 | * (text_buf[index]'th tree) and returns the longest-match position and length 815 | * via the global variables match_position and match_length. 816 | * If match_length = SearchSize, then removes the old node in favor of the new one, 817 | * because the old one will be deleted sooner. Note index plays double role, 818 | * as tree node and position in buffer. 819 | */ 820 | static void InsertNode(EncodeState sp, int index) 821 | { 822 | int cmp = 1; 823 | int p = BufferSize + sp.text_buf[index] + 1;//find root node of text_buf[index]'s tree 824 | sp.rchild[index] = sp.lchild[index] = NIL; 825 | sp.match_length = 0; 826 | for (; ; ) 827 | { 828 | if (cmp >= 0) 829 | { 830 | if (sp.rchild[p] != NIL) 831 | p = sp.rchild[p]; 832 | else 833 | { 834 | sp.rchild[p] = index; 835 | sp.parent[index] = p; 836 | return; 837 | } 838 | } 839 | else 840 | { 841 | if (sp.lchild[p] != NIL) 842 | p = sp.lchild[p]; 843 | else 844 | { 845 | sp.lchild[p] = index; 846 | sp.parent[index] = p; 847 | return; 848 | } 849 | } 850 | 851 | //Faster string comparsion 852 | var i = 1; 853 | while(i < SearchSize) 854 | { 855 | var u = Vector256.Create(sp.text_buf, index + i); 856 | var v = Vector256.Create(sp.text_buf, p + i); 857 | var w = BitOperations.TrailingZeroCount(~Avx2.MoveMask(Avx2.CompareEqual(u, v))); 858 | 859 | i += w; 860 | if(w != 32) 861 | { 862 | break; 863 | } 864 | } 865 | if (i > SearchSize) 866 | i = SearchSize; 867 | 868 | cmp = sp.text_buf[index + i] - sp.text_buf[p + i]; 869 | 870 | if (i > sp.match_length) 871 | { 872 | sp.match_position = p; 873 | if ((sp.match_length = i) >= SearchSize) 874 | break; 875 | } 876 | } 877 | sp.parent[index] = sp.parent[p]; 878 | sp.lchild[index] = sp.lchild[p]; 879 | sp.rchild[index] = sp.rchild[p]; 880 | sp.parent[sp.lchild[p]] = index; 881 | sp.parent[sp.rchild[p]] = index; 882 | if (sp.rchild[sp.parent[p]] == p) 883 | sp.rchild[sp.parent[p]] = index; 884 | else 885 | sp.lchild[sp.parent[p]] = index; 886 | sp.parent[p] = NIL; /* remove p */ 887 | } 888 | 889 | /* deletes node p from tree */ 890 | static void DeleteNode(EncodeState sp, int p) 891 | { 892 | int q; 893 | if (sp.parent[p] == NIL) 894 | return; /* not in tree */ 895 | if (sp.rchild[p] == NIL) 896 | q = sp.lchild[p]; 897 | else if (sp.lchild[p] == NIL) 898 | q = sp.rchild[p]; 899 | else 900 | { 901 | q = sp.lchild[p]; 902 | if (sp.rchild[q] != NIL) 903 | { 904 | do 905 | { 906 | q = sp.rchild[q]; 907 | } while (sp.rchild[q] != NIL); 908 | sp.rchild[sp.parent[q]] = sp.lchild[q]; 909 | sp.parent[sp.lchild[q]] = sp.parent[q]; 910 | sp.lchild[q] = sp.lchild[p]; 911 | sp.parent[sp.lchild[p]] = q; 912 | } 913 | sp.rchild[q] = sp.rchild[p]; 914 | sp.parent[sp.rchild[p]] = q; 915 | } 916 | sp.parent[q] = sp.parent[p]; 917 | if (sp.rchild[sp.parent[p]] == p) 918 | sp.rchild[sp.parent[p]] = q; 919 | else 920 | sp.lchild[sp.parent[p]] = q; 921 | sp.parent[p] = NIL; 922 | } 923 | 924 | public static byte[] Pack(byte[] input) 925 | { 926 | EncodeState sp = new(); 927 | 928 | int i; 929 | int len, last_match_length; 930 | 931 | int r = BufferSize - SearchSize; 932 | int s = 0; 933 | int inputIdx = 0; 934 | 935 | /* Read F bytes into the last F bytes of the buffer(wait for search) */ 936 | for (len = 0; len < SearchSize && inputIdx < input.Length; len++) 937 | { 938 | sp.text_buf[r + len] = input[inputIdx]; 939 | sp.text_buf_map[r + len] = inputIdx; 940 | inputIdx++; 941 | } 942 | 943 | /* 944 | * Insert the whole string just read. 945 | * The global variables match_length and match_position are set. 946 | */ 947 | InsertNode(sp, r); 948 | 949 | var bw = new BufferWriter(); 950 | 951 | var encode_pos = 0; 952 | do 953 | { 954 | if (encode_pos == 0 || sp.match_length < THRESHOLD) 955 | { 956 | sp.match_length = 1; 957 | bw.PutUncoded(sp.text_buf[r]); 958 | encode_pos++; 959 | } 960 | else 961 | { 962 | var offset = encode_pos - sp.text_buf_map[sp.match_position]; 963 | 964 | if(offset > 2048 && sp.match_length == THRESHOLD) 965 | { 966 | sp.match_length = 1; 967 | bw.PutUncoded(sp.text_buf[r]); 968 | encode_pos++; 969 | } 970 | else 971 | { 972 | bw.PutPair(offset, sp.match_length); 973 | encode_pos += sp.match_length; 974 | } 975 | } 976 | 977 | byte c; 978 | last_match_length = sp.match_length; 979 | for (i = 0; i < last_match_length && inputIdx < input.Length; i++) 980 | { 981 | DeleteNode(sp, s); /* Delete old strings and */ 982 | c = input[inputIdx]; 983 | sp.text_buf[s] = c; /* read new bytes */ 984 | sp.text_buf_map[s] = inputIdx; 985 | 986 | /* 987 | * If the position is near the end of buffer, extend the buffer 988 | * to make string comparison easier. 989 | */ 990 | if (s < (SearchSize - 1)) 991 | { 992 | sp.text_buf[s + BufferSize] = c; 993 | sp.text_buf_map[s + BufferSize] = inputIdx; 994 | } 995 | 996 | inputIdx++; 997 | /* Since this is a ring buffer, increment the position modulo BufferSize. */ 998 | s = (s + 1) & (BufferSize - 1); 999 | r = (r + 1) & (BufferSize - 1); 1000 | 1001 | /* Register the string in text_buf[r..r+SearchSize-1] */ 1002 | InsertNode(sp, r); 1003 | } 1004 | while (i++ < last_match_length) 1005 | { 1006 | DeleteNode(sp, s); 1007 | 1008 | /* After the end of text, no need to read, */ 1009 | s = (s + 1) & (BufferSize - 1); 1010 | r = (r + 1) & (BufferSize - 1); 1011 | 1012 | /* but buffer may not be empty. */ 1013 | if ((--len) > 0) 1014 | InsertNode(sp, r); 1015 | 1016 | //Match length can't exceed the input length 1017 | if (sp.match_length > len) 1018 | sp.match_length = 1; 1019 | } 1020 | 1021 | } while (len > 0); 1022 | 1023 | return bw.GetBytes(); 1024 | } 1025 | } 1026 | } 1027 | 1028 | public IarArchive(string fileName) 1029 | { 1030 | m_arcFileOffset = []; 1031 | m_arcName = Path.GetFileName(fileName); 1032 | m_reader = new BinaryReader(File.OpenRead(fileName)); 1033 | 1034 | Trace.Assert(m_reader.ReadUInt32() == 0x20726169); 1035 | 1036 | m_arcVersion = (m_reader.ReadInt16() << 12) | (int)m_reader.ReadInt16(); 1037 | m_isArcLongOffset = m_arcVersion >= 3000; 1038 | 1039 | var headerSize = m_reader.ReadInt32();//0xC 1040 | var infoSize = m_reader.ReadInt32();//0x14 1041 | 1042 | var trash = m_reader.ReadInt64(); 1043 | 1044 | var entryCount = m_reader.ReadInt32(); 1045 | var fileCount = m_reader.ReadInt32(); 1046 | 1047 | Trace.Assert(entryCount == fileCount); 1048 | 1049 | for(int i = 0; i < entryCount; i++) 1050 | { 1051 | m_arcFileOffset.Add(m_isArcLongOffset ? m_reader.ReadInt64() : m_reader.ReadInt32()); 1052 | } 1053 | 1054 | Trace.Assert(m_reader.BaseStream.Position == m_arcFileOffset[0]); 1055 | } 1056 | 1057 | public void ExtractTo(Dictionary fileListDic, string outputPath) 1058 | { 1059 | if (!Path.Exists(outputPath)) 1060 | { 1061 | Directory.CreateDirectory(outputPath); 1062 | } 1063 | 1064 | foreach (var key in fileListDic.Keys) 1065 | { 1066 | Console.WriteLine($"Writing {fileListDic[key]}..."); 1067 | IarImage.Extract(m_reader, m_arcFileOffset[key], m_arcVersion, fileListDic, Path.Combine(outputPath, fileListDic[key])); 1068 | } 1069 | } 1070 | 1071 | public void ExtractSingle(Dictionary fileListDic, string file, string outputPath) 1072 | { 1073 | try 1074 | { 1075 | var k = fileListDic.First(k => k.Value == file); 1076 | 1077 | if (!Path.Exists(outputPath)) 1078 | { 1079 | Directory.CreateDirectory(outputPath); 1080 | } 1081 | 1082 | Console.WriteLine($"Writing {file}..."); 1083 | IarImage.Extract(m_reader, m_arcFileOffset[k.Key], m_arcVersion, fileListDic, Path.Combine(outputPath, fileListDic[k.Key])); 1084 | } 1085 | catch(Exception ex) 1086 | { 1087 | Console.WriteLine(ex.Message); 1088 | Console.WriteLine(ex.StackTrace); 1089 | } 1090 | } 1091 | 1092 | public static Dictionary Create(string folder, string outputArcName) 1093 | { 1094 | Dictionary fileList = []; 1095 | 1096 | var normalImgList = Directory.EnumerateFiles(folder, "*.png", SearchOption.TopDirectoryOnly); 1097 | var layerImgList = Directory.EnumerateFiles(folder, "*.layerImg", SearchOption.TopDirectoryOnly); 1098 | var subImgList = Directory.EnumerateFiles(folder, "*.subImg", SearchOption.TopDirectoryOnly); 1099 | 1100 | using var writer = new BinaryWriter(File.Open(Path.Combine(folder, "..", outputArcName), FileMode.Create)); 1101 | writer.Write(0x20726169); 1102 | writer.Write(0x00010004);//Lock version to 0x4001 1103 | writer.Write(0xC); 1104 | writer.Write(0x14); 1105 | writer.Write(0); 1106 | writer.Write(0); 1107 | 1108 | var fileCountOffset = writer.BaseStream.Position; 1109 | writer.BaseStream.Position += 8 * (normalImgList.Count() + layerImgList.Count() + subImgList.Count() + 1); 1110 | 1111 | List fileOffsets = []; 1112 | 1113 | int inArcIndex = 0; 1114 | 1115 | { 1116 | foreach(var file in normalImgList) 1117 | { 1118 | Console.WriteLine($"WritingImg: {file} ..."); 1119 | fileOffsets.Add(writer.BaseStream.Position); 1120 | IarImage.Import(writer, file, inArcIndex++, fileList, 0x4001); 1121 | } 1122 | 1123 | foreach(var file in layerImgList) 1124 | { 1125 | Console.WriteLine($"WritingLayerImg: {file} ..."); 1126 | fileOffsets.Add(writer.BaseStream.Position); 1127 | IarImage.Import(writer, file, inArcIndex++, fileList, 0x4001); 1128 | } 1129 | 1130 | foreach (var file in subImgList) 1131 | { 1132 | Console.WriteLine($"WritingSubImg: {file} ..."); 1133 | fileOffsets.Add(writer.BaseStream.Position); 1134 | IarImage.Import(writer, file, inArcIndex++, fileList, 0x4001); 1135 | } 1136 | } 1137 | 1138 | writer.BaseStream.Position = fileCountOffset; 1139 | writer.Write(fileList.Count); 1140 | writer.Write(fileList.Count); 1141 | foreach(var o in fileOffsets) 1142 | { 1143 | writer.Write(o); 1144 | } 1145 | 1146 | return fileList; 1147 | } 1148 | } 1149 | } 1150 | -------------------------------------------------------------------------------- /ArcTool/Program.cs: -------------------------------------------------------------------------------- 1 | using SAS5Lib.SecResource; 2 | using System.Text; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace ArcTool 6 | { 7 | internal class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 12 | CodepageManager.Instance.SetImportEncoding("sjis"); 13 | CodepageManager.Instance.SetExportEncoding("utf8"); 14 | 15 | 16 | if(args.Length < 4) 17 | { 18 | Console.WriteLine("SAS5Tool.ArcTool"); 19 | Console.WriteLine("A tool to extract & import resource file inside .iar/.war/.gar file.\n"); 20 | Console.WriteLine("Usage:\nArcTool.exe unpack []\nArcTool.exe pack [] []\n\n"); 21 | Console.WriteLine("Modes:\n\textract\t\tExtracting all texts inside the sec5 specified and output mutiple .txt files to the folder.\n\timport\t\tImport all .txt files inside the folder and output a new sec5 file."); 22 | return; 23 | } 24 | 25 | SecScenarioProgram prog = new(args[2]); 26 | var resource = new ResourceManager(prog.GetSectionData("RES2")); 27 | var archives = new SecArcFileList(prog.GetSectionData("RTFC")); 28 | 29 | if (args[1] == "unpack") 30 | { 31 | switch (args[0]) 32 | { 33 | case "gar": 34 | var garArc = new GarArchive(args[3]); 35 | garArc.ExtractTo(args.Length > 4 ? args[4] : args[3] + "_unpack"); 36 | break; 37 | case "iar": 38 | var iarArc = new IarArchive(args[3]); 39 | iarArc.ExtractTo(resource.GetIarFileList(args[3]), args.Length > 4 ? args[4] : args[3] + "_unpack"); 40 | break; 41 | } 42 | } 43 | else if (args[1] == "pack") 44 | { 45 | var newArcFileName = args.Length > 4 ? args[4] : args[3]; 46 | var newSec5Path = args.Length > 5 ? args[5] : args[2]; 47 | switch (args[0]) 48 | { 49 | case "gar": 50 | { 51 | if(!newArcFileName.EndsWith(".gar")) 52 | newArcFileName += ".gar"; 53 | var fileList = GarArchive.Create(args[3], newArcFileName); 54 | resource.UpdateGarResourceRecord(fileList, newArcFileName); 55 | break; 56 | } 57 | case "iar": 58 | { 59 | if (!newArcFileName.EndsWith(".iar")) 60 | newArcFileName += ".iar"; 61 | var fileList = IarArchive.Create(args[3], newArcFileName); 62 | resource.UpdateIarResourceRecord(fileList, newArcFileName); 63 | break; 64 | } 65 | } 66 | archives.AddArc(newArcFileName); 67 | prog.SetSectionData("RES2", resource.GetData()); 68 | prog.SetSectionData("REFC", archives.GetData()); 69 | prog.Save(newSec5Path); 70 | Console.WriteLine("Finished."); 71 | } 72 | else 73 | { 74 | Console.WriteLine($"Unknown operation: {args[1]}"); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ArcTool/WarArchive.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace ArcTool 8 | { 9 | internal class WarArchive 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SAS5Lib/Misc.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace SAS5Lib 4 | { 5 | public class CodepageManager : Singleton 6 | { 7 | readonly Dictionary _encodings = new Dictionary 8 | { 9 | { "utf8", Encoding.GetEncoding("utf-8")}, 10 | { "gbk", Encoding.GetEncoding("gbk")}, 11 | { "sjis", Encoding.GetEncoding("shift_jis")}, 12 | }; 13 | 14 | Encoding _exportEncoding; 15 | Encoding _importEncoding; 16 | 17 | public int ExportCodePage { get { return _exportEncoding.CodePage; } } 18 | public int ImportCodePage { get { return _importEncoding.CodePage; } } 19 | 20 | public void SetExportEncoding(string encoding) 21 | { 22 | _exportEncoding = _encodings[encoding]; 23 | } 24 | 25 | public void SetExportEncoding(int encoding) 26 | { 27 | _exportEncoding = Encoding.GetEncoding(encoding); 28 | } 29 | 30 | public void SetImportEncoding(string encoding) 31 | { 32 | _importEncoding = _encodings[encoding]; 33 | } 34 | 35 | public void SetImportEncoding(int encoding) 36 | { 37 | _importEncoding = Encoding.GetEncoding(encoding); 38 | } 39 | 40 | public string ExportGetString(byte[] bytes) 41 | { 42 | return _exportEncoding.GetString(bytes); 43 | } 44 | 45 | public byte[] ExportGetBytes(string str) 46 | { 47 | return _exportEncoding.GetBytes(str); 48 | } 49 | 50 | public string ImportGetString(byte[] bytes) 51 | { 52 | return _importEncoding.GetString(bytes); 53 | } 54 | 55 | public byte[] ImportGetBytes(string str) 56 | { 57 | return _importEncoding.GetBytes(str); 58 | } 59 | } 60 | 61 | public struct EditableString 62 | { 63 | public bool IsEdited; 64 | public string Text; 65 | 66 | public EditableString(string text, bool isEdited = true) 67 | { 68 | IsEdited = isEdited; 69 | Text = text; 70 | } 71 | 72 | public EditableString() 73 | { 74 | IsEdited = false; 75 | Text = ""; 76 | } 77 | 78 | public override readonly string ToString() 79 | { 80 | return $"\"{Text}\", Edited: {IsEdited}"; 81 | } 82 | 83 | public override readonly int GetHashCode() 84 | { 85 | return Text.GetHashCode(); 86 | } 87 | } 88 | 89 | public struct CodeOffset 90 | { 91 | public long Old; 92 | public long New; 93 | } 94 | 95 | public class Utils 96 | { 97 | public static string ReadCString(BinaryReader reader) 98 | { 99 | List charArray = []; 100 | byte b = reader.ReadByte(); 101 | while (b != 0) 102 | { 103 | charArray.Add(b); 104 | b = reader.ReadByte(); 105 | } 106 | return CodepageManager.Instance.ImportGetString([.. charArray]); 107 | } 108 | } 109 | 110 | public class Singleton where T : Singleton, new() 111 | { 112 | private static T? instance; 113 | public static T Instance 114 | { 115 | get 116 | { 117 | instance ??= new T(); 118 | return instance; 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /SAS5Lib/SAS5Lib.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | $(SolutionDir)Build\bin 8 | $(SolutionDir)Build\obj\SAS5Lib\ 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /SAS5Lib/SecArcFileList.cs: -------------------------------------------------------------------------------- 1 | namespace SAS5Lib 2 | { 3 | public class SecArcFileList 4 | { 5 | List m_arcFiles; 6 | 7 | public SecArcFileList(byte[]? input) 8 | { 9 | m_arcFiles = []; 10 | if(input == null) 11 | { 12 | return; 13 | } 14 | using var reader = new BinaryReader(new MemoryStream(input)); 15 | var count = reader.ReadInt32(); 16 | for(int i = 0; i < count; i++) 17 | { 18 | m_arcFiles.Add(Utils.ReadCString(reader)); 19 | } 20 | } 21 | 22 | public void AddArc(string arcName) 23 | { 24 | foreach(string arc in m_arcFiles) 25 | { 26 | if (arc == arcName) 27 | return; 28 | } 29 | m_arcFiles.Add(arcName); 30 | } 31 | 32 | public void RemoveArc(string arcName) 33 | { 34 | m_arcFiles.Remove(arcName); 35 | } 36 | 37 | public byte[] GetData() 38 | { 39 | var ms = new MemoryStream(); 40 | using var writer = new BinaryWriter(ms); 41 | writer.Write(m_arcFiles.Count); 42 | foreach(var s in m_arcFiles) 43 | { 44 | writer.Write(CodepageManager.Instance.ExportGetBytes(s)); 45 | } 46 | return ms.ToArray(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /SAS5Lib/SecCode/ExecutorCommand.cs: -------------------------------------------------------------------------------- 1 | namespace SAS5Lib.SecCode 2 | { 3 | public class ExecutorCommand 4 | { 5 | public CodeOffset Offset; 6 | public byte Type; 7 | public short ExecutorIndex; 8 | public List? Expression; 9 | 10 | public ExecutorCommand(byte type, short index, List? expression = null, long offset = -1) 11 | { 12 | Offset.Old = offset; 13 | Type = type; 14 | ExecutorIndex = index; 15 | Expression = expression; 16 | } 17 | 18 | public ExecutorCommand() 19 | { 20 | } 21 | 22 | public void GetExpression(BinaryReader reader) 23 | { 24 | Expression ??= []; 25 | byte argID; 26 | while ((argID = reader.ReadByte()) != 0xFF) 27 | { 28 | var exprLen = SecScenarioProgram.LegacyVersion ? reader.ReadInt32() : reader.ReadInt16(); 29 | var expr = new Expression(Convert.ToInt16(exprLen), argID, reader.BaseStream.Position, reader.ReadBytes(exprLen)); 30 | Expression.Add(expr); 31 | } 32 | } 33 | 34 | public void Write(BinaryWriter writer, ref Dictionary addresses) 35 | { 36 | Offset.New = writer.BaseStream.Position; 37 | addresses.TryAdd(Offset.Old, Offset.New); 38 | 39 | writer.Write(Type); 40 | if (Type == 0x1B) 41 | { 42 | writer.Write(ExecutorIndex); 43 | if (Expression != null) 44 | { 45 | foreach (var expr in Expression) 46 | { 47 | expr.Write(writer, ref addresses); 48 | } 49 | } 50 | writer.Write((byte)0xFF); 51 | } 52 | } 53 | 54 | public override string ToString() 55 | { 56 | return $"{SecCodeProp.ExecutorName(ExecutorIndex)}(Offset: ({Offset.Old:X8},{Offset.New:X8}), ExecutorIndex: {ExecutorIndex:X4}, Expression(s): {Expression?.Count})"; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /SAS5Lib/SecCode/Expression.cs: -------------------------------------------------------------------------------- 1 | namespace SAS5Lib.SecCode 2 | { 3 | /* 4 | * Expression 5 | * Prepare argument(s) for CmdExecutors 6 | */ 7 | public class Expression 8 | { 9 | public short Length; 10 | public byte ArgID; 11 | public List Operations; 12 | 13 | public Expression(short exprLength, byte exprArgID, List operations) 14 | { 15 | Length = exprLength; 16 | ArgID = exprArgID; 17 | Operations = operations; 18 | } 19 | 20 | public Expression(short exprLength, byte exprArgID, long basePos, byte[]? exprData = null) 21 | { 22 | Operations = []; 23 | Length = exprLength; 24 | ArgID = exprArgID; 25 | //Have operation(s) 26 | //Expression ends with 0xFF(EXPR_END) 27 | if (exprData != null) 28 | { 29 | using var reader = new BinaryReader(new MemoryStream(exprData)); 30 | 31 | while (reader.BaseStream.Position < reader.BaseStream.Length) 32 | { 33 | Operations.Add(new ExpressionOperation(reader, basePos)); 34 | } 35 | } 36 | } 37 | 38 | public void Write(BinaryWriter writer, ref Dictionary addresses) 39 | { 40 | writer.Write(ArgID); 41 | var lenPos = writer.BaseStream.Position; 42 | if(SecScenarioProgram.LegacyVersion) 43 | { 44 | writer.Write(0); 45 | } 46 | else 47 | { 48 | writer.Write(Length); 49 | } 50 | 51 | if (Operations.Count > 0) 52 | { 53 | foreach (var operation in Operations) 54 | { 55 | operation.Write(writer, ref addresses); 56 | } 57 | } 58 | var curPos = writer.BaseStream.Position; 59 | writer.BaseStream.Position = lenPos; 60 | if (SecScenarioProgram.LegacyVersion) 61 | { 62 | writer.Write(Convert.ToInt32(curPos - lenPos - 4)); 63 | } 64 | else 65 | { 66 | writer.Write(Convert.ToInt16(curPos - lenPos - 2)); 67 | } 68 | writer.BaseStream.Position = curPos; 69 | } 70 | 71 | public override string ToString() 72 | { 73 | return $"ArgExpr(ArgID:{ArgID:X2}, OpCount:{Operations.Count})"; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /SAS5Lib/SecCode/ExpressionOperation.cs: -------------------------------------------------------------------------------- 1 | using SAS5Lib.SecVariable; 2 | using static SAS5Lib.SecCode.SecCodeProp; 3 | 4 | namespace SAS5Lib.SecCode 5 | { 6 | public class NativeFunCall 7 | { 8 | public byte PresetObjIndex; 9 | public int NativeFuncIndex; 10 | 11 | public NativeFunCall(BinaryReader reader) 12 | { 13 | PresetObjIndex = reader.ReadByte(); 14 | NativeFuncIndex = reader.ReadInt32(); 15 | } 16 | 17 | public override string ToString() 18 | { 19 | return $"NativeFunCall(PresetObjIndex:{PresetObjIndex}, NativeFuncIndex:{NativeFuncIndex})"; 20 | } 21 | } 22 | 23 | public class ExpressionOperation 24 | { 25 | readonly ExprOpProp Prop; 26 | public CodeOffset DataOffset; 27 | public CodeOffset Offset; 28 | public byte Op; 29 | public object? Data; 30 | 31 | public enum JmpMode 32 | { 33 | None = 0, 34 | Direct, 35 | Offset 36 | } 37 | 38 | public ExpressionOperation(byte op, object? data) 39 | { 40 | Offset.Old = -1; 41 | DataOffset.Old = -1; 42 | Op = op; 43 | Data = data; 44 | Prop = GetOpProp(Op); 45 | } 46 | 47 | public ExpressionOperation(BinaryReader reader, long basePos = 0) 48 | { 49 | Offset.Old = reader.BaseStream.Position + basePos; 50 | Op = reader.ReadByte(); 51 | DataOffset.Old = reader.BaseStream.Position + basePos; 52 | Prop = GetOpProp(Op); 53 | if(Prop.ReaderFunc != null) 54 | { 55 | Data = Prop.ReaderFunc(reader); 56 | } 57 | } 58 | 59 | public void Write(BinaryWriter writer, ref Dictionary addresses) 60 | { 61 | Offset.New = writer.BaseStream.Position; 62 | addresses.TryAdd(Offset.Old, Offset.New); 63 | 64 | writer.Write(Op); 65 | DataOffset.New = writer.BaseStream.Position; 66 | 67 | if(Op == 0xFF) 68 | { 69 | return; 70 | } 71 | 72 | switch (Data) 73 | { 74 | case EditableString str: 75 | { 76 | var bs = str.IsEdited ? CodepageManager.Instance.ExportGetBytes(str.Text) : CodepageManager.Instance.ImportGetBytes(str.Text); 77 | //Array -> string 78 | if (Op == 0x1E) 79 | { 80 | //Primitive type id(0) 81 | writer.Write(0); 82 | writer.Write(bs.Length); 83 | } 84 | else 85 | { 86 | writer.Write((byte)0); 87 | writer.Write(Convert.ToByte(bs.Length)); 88 | } 89 | writer.Write(bs); 90 | break; 91 | } 92 | case NativeFunCall nc: 93 | { 94 | writer.Write(nc.PresetObjIndex); 95 | writer.Write(nc.NativeFuncIndex); 96 | break; 97 | } 98 | case SecVariable.Object v: 99 | { 100 | writer.Write(v.Index); 101 | //Array Type 102 | var vtSize = v.Type.Type.GetSize(); 103 | if (vtSize != v.Data.Length) 104 | { 105 | writer.Write(Convert.ToUInt32(v.Data.Length / vtSize)); 106 | } 107 | writer.Write(v.Data); 108 | break; 109 | } 110 | case byte b: writer.Write(b); break; 111 | case sbyte sb: writer.Write(sb); break; 112 | case short s: writer.Write(s); break; 113 | case ushort us: writer.Write(us); break; 114 | case int i: writer.Write(i); break; 115 | case uint ui: writer.Write(ui); break; 116 | case float f: writer.Write(f); break; 117 | case double d: writer.Write(d); break; 118 | case long l: writer.Write(l); break; 119 | case ulong ul: writer.Write(ul); break; 120 | default: 121 | break; 122 | } 123 | } 124 | 125 | public override string ToString() 126 | { 127 | string name = Prop.Name; 128 | 129 | if (string.IsNullOrEmpty(name)) 130 | { 131 | name = $"Expr_{Op:X2}"; 132 | } 133 | 134 | return $"{name}({Prop.FormatterFunc(Data)})"; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /SAS5Lib/SecCode/OrphanExpression.cs: -------------------------------------------------------------------------------- 1 | namespace SAS5Lib.SecCode 2 | { 3 | public class OrphanExpression 4 | { 5 | public CodeOffset Offset; 6 | public List Clauses; 7 | 8 | public OrphanExpression(BinaryReader reader) 9 | { 10 | Offset.Old = reader.BaseStream.Position; 11 | Clauses = []; 12 | 13 | byte b; 14 | do 15 | { 16 | b = reader.ReadByte(); 17 | reader.BaseStream.Position--; 18 | Clauses.Add(new ExpressionOperation(reader)); 19 | 20 | } while (b != 0x11);//0x11 -> RET 21 | } 22 | 23 | public void Write(BinaryWriter writer, ref Dictionary addresses) 24 | { 25 | Offset.New = writer.BaseStream.Position; 26 | addresses.TryAdd(Offset.Old, Offset.New); 27 | 28 | foreach (var clause in Clauses) 29 | { 30 | clause.Write(writer, ref addresses); 31 | } 32 | } 33 | 34 | public override string ToString() 35 | { 36 | return $"OrphanExpression(Offset: ({Offset.Old:X8},{Offset.New:X8}), Clauses: {Clauses.Count})"; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SAS5Lib/SecCode/ScenarioCode.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace SAS5Lib.SecCode 4 | { 5 | public class NamedCode 6 | { 7 | public string Name; 8 | public List Code; 9 | 10 | public NamedCode(string name, List code) 11 | { 12 | Name = name; 13 | Code = code; 14 | } 15 | 16 | public override string ToString() 17 | { 18 | try 19 | { 20 | var c = (ExecutorCommand)Code.First(o => o is ExecutorCommand); 21 | return $"ScriptName: {Name}, StartAddr:({c.Offset.Old:X8}, {c.Offset.New:X8}), CodesCount: {Code.Count}"; 22 | } 23 | catch(InvalidOperationException) 24 | { 25 | return $"ScriptName: {Name}, CodesCount: {Code.Count}"; 26 | } 27 | } 28 | } 29 | 30 | public class ScenarioCode 31 | { 32 | readonly BinaryReader _reader; 33 | readonly SecSource? _source; 34 | public List Code { get; private set;} 35 | 36 | public ScenarioCode(string code, SecSource? src = null) 37 | { 38 | _source = src; 39 | _reader = new BinaryReader(File.OpenRead(code)); 40 | Code = []; 41 | } 42 | 43 | public ScenarioCode(byte[]? input, SecSource? src = null) 44 | { 45 | _source = src; 46 | Code = []; 47 | if (input == null) 48 | { 49 | _reader = new BinaryReader(new MemoryStream()); 50 | return; 51 | } 52 | _reader = new BinaryReader(new MemoryStream(input)); 53 | } 54 | 55 | public void Disasemble() 56 | { 57 | Console.WriteLine("Disasembling..."); 58 | if(_source!= null) 59 | { 60 | var sourceFile = _source.SourceFiles; 61 | for(var i = 0; i < sourceFile.Count; i++) 62 | { 63 | while(_reader.BaseStream.Position < sourceFile[i].Position) 64 | { 65 | Code.Add(GetCommand()); 66 | } 67 | Trace.Assert(_reader.BaseStream.Position == sourceFile[i].Position); 68 | 69 | List code = []; 70 | var endPos = i < sourceFile.Count - 1 ? sourceFile[i+1].Position : _reader.BaseStream.Length; 71 | 72 | while(_reader.BaseStream.Position < endPos) 73 | { 74 | code.Add(GetCommand()); 75 | } 76 | Trace.Assert(_reader.BaseStream.Position == endPos); 77 | 78 | Code.Add(new NamedCode(sourceFile[i].Name, code)); 79 | } 80 | } 81 | else 82 | { 83 | while (_reader.BaseStream.Position < _reader.BaseStream.Length) 84 | { 85 | Code.Add(GetCommand()); 86 | } 87 | } 88 | } 89 | 90 | public Tuple> Assemble() 91 | { 92 | Console.WriteLine("Assembling..."); 93 | var ms = new MemoryStream(); 94 | using var writer = new BinaryWriter(ms); 95 | 96 | Dictionary addresses = []; 97 | 98 | void WriteCodeObj(object cb) 99 | { 100 | if (cb is OrphanExpression orp) 101 | { 102 | orp.Write(writer, ref addresses); 103 | } 104 | else if (cb is NamedCode nc) 105 | { 106 | if(_source != null) 107 | { 108 | var src = _source.SourceFiles.Find(f => f.Name == nc.Name); 109 | if(src != null) 110 | { 111 | src.Position = Convert.ToInt32(writer.BaseStream.Position); 112 | } 113 | } 114 | foreach(var obj in nc.Code) 115 | { 116 | WriteCodeObj(obj); 117 | } 118 | } 119 | else if (cb is ExecutorCommand expc) 120 | { 121 | expc.Write(writer, ref addresses); 122 | } 123 | else if (cb is EditableString str) 124 | { 125 | if (str.IsEdited) 126 | { 127 | writer.Write(CodepageManager.Instance.ExportGetBytes(str.Text)); 128 | } 129 | else 130 | { 131 | writer.Write(CodepageManager.Instance.ImportGetBytes(str.Text)); 132 | } 133 | } 134 | else 135 | { 136 | throw new Exception("Unknown object in code"); 137 | } 138 | } 139 | 140 | foreach (var cb in Code) 141 | { 142 | WriteCodeObj(cb); 143 | } 144 | 145 | void UpdateAddress(ExpressionOperation clause) 146 | { 147 | var jmpMode = SecCodeProp.GetOpJmpMode(clause.Op); 148 | if (jmpMode == ExpressionOperation.JmpMode.Offset || jmpMode == ExpressionOperation.JmpMode.Direct) 149 | { 150 | writer.BaseStream.Position = clause.DataOffset.New; 151 | if (jmpMode == ExpressionOperation.JmpMode.Offset && clause.Data is int dest) 152 | { 153 | if (!addresses.TryGetValue(dest + clause.DataOffset.Old + 4, out var newAddr)) 154 | { 155 | throw new Exception("Unknown jmp dest."); 156 | } 157 | writer.Write(Convert.ToInt32(newAddr - clause.DataOffset.New - 4)); 158 | } 159 | else if(clause.Data is uint udest) 160 | { 161 | if (!addresses.TryGetValue(udest, out var newAddr)) 162 | { 163 | // Fix: Workaround for old version 164 | if (clause.Op == 0x19) 165 | { 166 | return; 167 | } 168 | throw new Exception("Unknown jmp dest."); 169 | } 170 | writer.Write(Convert.ToInt32(newAddr)); 171 | } 172 | else 173 | { 174 | throw new Exception("Jump data error."); 175 | } 176 | } 177 | } 178 | 179 | void FixCodeObj(object obj) 180 | { 181 | if (obj is OrphanExpression orpExpr) 182 | { 183 | foreach (var clause in orpExpr.Clauses) 184 | { 185 | UpdateAddress(clause); 186 | } 187 | } 188 | else if (obj is ExecutorCommand execCmd) 189 | { 190 | if (execCmd.Expression != null) 191 | { 192 | foreach (var expr in execCmd.Expression) 193 | { 194 | foreach (var clause in expr.Operations) 195 | { 196 | UpdateAddress(clause); 197 | } 198 | } 199 | } 200 | } 201 | else if(obj is NamedCode nc) 202 | { 203 | foreach(var code in nc.Code) 204 | { 205 | FixCodeObj(code); 206 | } 207 | } 208 | } 209 | 210 | foreach (var obj in Code) 211 | { 212 | FixCodeObj(obj); 213 | } 214 | 215 | return new Tuple>(ms.ToArray(), addresses); 216 | } 217 | 218 | public object DisasembleSingle(long position) 219 | { 220 | _reader.BaseStream.Position = position; 221 | return GetCommand(); 222 | } 223 | 224 | object GetCommand() 225 | { 226 | var cmd = new ExecutorCommand(); 227 | 228 | cmd.Offset.Old = _reader.BaseStream.Position; 229 | cmd.Type = _reader.ReadByte(); 230 | 231 | switch (cmd.Type) 232 | { 233 | case 0x1A: 234 | { 235 | return cmd; 236 | } 237 | //WTF???? 238 | case 0x69: 239 | { 240 | return cmd; 241 | } 242 | //Regular command 243 | case 0x1B: 244 | { 245 | cmd.ExecutorIndex = _reader.ReadInt16(); 246 | if (SecCodeProp.ExecutorHaveExpression(cmd.ExecutorIndex)) 247 | { 248 | cmd.GetExpression(_reader); 249 | } 250 | else 251 | { 252 | //This command must reaches its end at curent position 253 | Trace.Assert(_reader.ReadByte() == 0xFF); 254 | } 255 | return cmd; 256 | } 257 | //Only reaches here when we hits the end os the script 258 | case 0xFF: 259 | { 260 | return cmd; 261 | } 262 | default: 263 | { 264 | _reader.BaseStream.Position--; 265 | if (cmd.Type != 0x20 && (cmd.Type < 0x81 || cmd.Type > 0xFD)) 266 | { 267 | //This is probably an orphan expression which uses 0x11(VM_RET) at the end of the expression. (like library function) 268 | var prevPos = _reader.BaseStream.Position; 269 | try 270 | { 271 | return new OrphanExpression(_reader); 272 | } 273 | catch(Exception) 274 | { 275 | //Unwind to previous position 276 | _reader.BaseStream.Position = prevPos; 277 | return new EditableString(ReadString(_reader)); 278 | } 279 | } 280 | else 281 | { 282 | //Assuming that it's a message string 283 | return new EditableString(ReadString(_reader)); 284 | } 285 | } 286 | } 287 | } 288 | 289 | static string ReadString(BinaryReader reader) 290 | { 291 | List charArray = new(); 292 | byte b; 293 | while (true) 294 | { 295 | b = reader.ReadByte(); 296 | if (b == 0 || b == 0x1A || b == 0x1B) 297 | { 298 | if (b == 0x1A || b == 0x1B) 299 | { 300 | reader.BaseStream.Position--; 301 | } 302 | break; 303 | } 304 | charArray.Add(b); 305 | } 306 | return CodepageManager.Instance.ImportGetString([.. charArray]); 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /SAS5Lib/SecCodePage.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace SAS5Lib 4 | { 5 | public class SecCodePage 6 | { 7 | public int FileReadingCodePage; 8 | public int ExecutorCodePage; 9 | public int ExpressionCodePage; 10 | public int ReservedCodePage; 11 | 12 | public SecCodePage(BinaryReader reader) 13 | { 14 | FileReadingCodePage = reader.ReadInt32(); 15 | ExecutorCodePage = reader.ReadInt32(); 16 | ExpressionCodePage = reader.ReadInt32(); 17 | ReservedCodePage = reader.ReadInt32(); 18 | } 19 | 20 | public SecCodePage(byte[]? data) 21 | { 22 | if (data == null) 23 | { 24 | return; 25 | } 26 | using MemoryStream ms = new(data); 27 | using BinaryReader reader = new(ms); 28 | FileReadingCodePage = reader.ReadInt32(); 29 | ExecutorCodePage = reader.ReadInt32(); 30 | ExpressionCodePage = reader.ReadInt32(); 31 | ReservedCodePage = reader.ReadInt32(); 32 | } 33 | 34 | public SecCodePage(int charset) 35 | { 36 | SetCharset(charset); 37 | } 38 | 39 | public void SetCharset(int charset) 40 | { 41 | FileReadingCodePage = ExecutorCodePage = ExpressionCodePage = charset; 42 | } 43 | 44 | public byte[] GetData() 45 | { 46 | var ms = new MemoryStream(); 47 | using var writer = new BinaryWriter(ms); 48 | writer.Write(FileReadingCodePage); 49 | writer.Write(ExecutorCodePage); 50 | writer.Write(ExpressionCodePage); 51 | writer.Write(ReservedCodePage); 52 | return ms.ToArray(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SAS5Lib/SecOption/OptionManager.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Diagnostics; 3 | 4 | namespace SAS5Lib.SecOption 5 | { 6 | public class OptionManager 7 | { 8 | SecOptionMap? _secOptionMap; 9 | 10 | public OptionManager(byte[]? input) 11 | { 12 | if (input == null) 13 | { 14 | return; 15 | } 16 | using var reader = new BinaryReader(new MemoryStream(input)); 17 | //Must be an OptionMap 18 | Trace.Assert(reader.ReadByte() == 0); 19 | _secOptionMap = new SecOptionMap(reader); 20 | } 21 | 22 | public byte[] GetData() 23 | { 24 | if(_secOptionMap == null) 25 | { 26 | return []; 27 | } 28 | var ms = new MemoryStream(); 29 | using var writer = new BinaryWriter(ms); 30 | _secOptionMap.Save(writer); 31 | return ms.ToArray(); 32 | } 33 | 34 | public void Save(string path) 35 | { 36 | File.WriteAllText(path, JsonConvert.SerializeObject(_secOptionMap, Formatting.Indented , new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Objects })); 37 | } 38 | 39 | public void Load(string path, bool debugBuild) 40 | { 41 | _secOptionMap = JsonConvert.DeserializeObject(File.ReadAllText(path), new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Objects }); 42 | if (debugBuild) 43 | { 44 | if (_secOptionMap != null 45 | && _secOptionMap.Map.TryGetValue("Title", out var val) 46 | && val is SecOptionString s1 47 | && _secOptionMap.Map.TryGetValue("Registry", out var val2) 48 | && val2 is SecOptionMap m1 49 | && m1.Map.TryGetValue("Application", out var val3) 50 | && val3 is SecOptionString s2) 51 | { 52 | var suffix = $" Debug build[{DateTime.Now.ToString().Replace('/', '-').Replace(':', '-')}]"; 53 | s1.Value.Text += suffix; 54 | s2.Value.Text += suffix; 55 | s1.Value.IsEdited = true; 56 | s2.Value.IsEdited = true; 57 | } 58 | } 59 | } 60 | 61 | public void UpdateExportFuncAddr(Dictionary addresses) 62 | { 63 | var opt = GetOptionByName("EXPORT_FUNS"); 64 | if (opt is SecOptionMap mapVal) 65 | { 66 | foreach(var k in mapVal.Map.Keys) 67 | { 68 | if (mapVal.Map[k] is not SecOptionInteger optionInt) 69 | { 70 | continue; 71 | } 72 | if(addresses.TryGetValue(optionInt.Value, out var newAddress)) 73 | { 74 | Console.WriteLine($"{k}: {optionInt.Value} -> {newAddress}"); 75 | optionInt.Value = Convert.ToInt32(newAddress); 76 | } 77 | else 78 | { 79 | throw new Exception("Unknown export function addr."); 80 | } 81 | } 82 | } 83 | } 84 | 85 | public OptionType? GetOptionByName(string name) 86 | { 87 | if (_secOptionMap != null && _secOptionMap.Map.TryGetValue(name, out var ret)) 88 | { 89 | return ret; 90 | } 91 | else 92 | { 93 | return null; 94 | } 95 | } 96 | 97 | public void PrintGameInfo() 98 | { 99 | try 100 | { 101 | var saveDataGuid = ((SecOptionString)GetOptionByName("ContextFileGameGuid")).Value.Text; 102 | var saveDataKey = ((SecOptionInteger)GetOptionByName("ContextKey")).Value; 103 | var saveDataVersion = ((SecOptionString)GetOptionByName("ContextVersion")).Value.Text; 104 | var gameId = ((SecOptionString)GetOptionByName("GlobalAppId")).Value.Text; 105 | 106 | var gameInfo = ((SecOptionMap)GetOptionByName("Registry")).Map; 107 | var gameName = ((SecOptionString)gameInfo["Application"]).Value.Text; 108 | var gameVersion = ((SecOptionString)gameInfo["Category"]).Value.Text; 109 | var gameManufacturer = ((SecOptionString)gameInfo["Manufacturer"]).Value.Text; 110 | 111 | Console.WriteLine("--------------Game Info---------------"); 112 | Console.WriteLine($"Name: {gameName} (Ver {gameVersion})"); 113 | Console.WriteLine($"AppID: {gameId}"); 114 | Console.WriteLine($"Manufacturer: {gameManufacturer}\n"); 115 | Console.WriteLine($"SaveDataGUID: {saveDataGuid}"); 116 | Console.WriteLine($"SaveDataKey: 0x{saveDataKey:X4}"); 117 | Console.WriteLine($"SaveDataVersion: {saveDataVersion}"); 118 | Console.WriteLine("--------------------------------------\n"); 119 | } 120 | catch 121 | { 122 | 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /SAS5Lib/SecOption/OptionType.cs: -------------------------------------------------------------------------------- 1 | namespace SAS5Lib.SecOption 2 | { 3 | public abstract class OptionType 4 | { 5 | protected byte TypeID; 6 | 7 | public OptionType(byte typeID) 8 | { 9 | TypeID = typeID; 10 | } 11 | 12 | public static OptionType ReadOptionType(BinaryReader reader) 13 | { 14 | var typeID = reader.ReadByte(); 15 | switch(typeID) 16 | { 17 | case 0: 18 | return new SecOptionMap(reader); 19 | case 1: 20 | return new SecOptionTuple(reader); 21 | case 2: 22 | return new SecOptionInteger(reader); 23 | case 3: 24 | return new SecOptionString(reader); 25 | case 4: 26 | return new SecOptionResource(reader); 27 | case 5: 28 | return new SecOptionLabel(reader); 29 | case 6: 30 | return new SecOptionBinary(reader); 31 | default: 32 | throw new Exception("Unknown OptionTypeID"); 33 | } 34 | } 35 | 36 | public static EditableString ReadOptionString(BinaryReader reader) 37 | { 38 | var length = reader.ReadInt32(); 39 | return new EditableString(CodepageManager.Instance.ImportGetString(reader.ReadBytes(length))); 40 | } 41 | 42 | public static void WriteOptionString(BinaryWriter writer, EditableString str) 43 | { 44 | var b = str.IsEdited ? CodepageManager.Instance.ExportGetBytes(str.Text) : CodepageManager.Instance.ImportGetBytes(str.Text); 45 | writer.Write(b.Length); 46 | writer.Write(b); 47 | } 48 | 49 | public abstract void Save(BinaryWriter writer); 50 | } 51 | 52 | public class SecOptionMap : OptionType 53 | { 54 | public Dictionary Map; 55 | 56 | public SecOptionMap() : base(0) 57 | { 58 | Map = []; 59 | } 60 | 61 | public SecOptionMap(BinaryReader reader) : base(0) 62 | { 63 | Map = []; 64 | var count = reader.ReadInt32(); 65 | for(int i = 0; i < count; i++) 66 | { 67 | Map.TryAdd(ReadOptionString(reader).Text, ReadOptionType(reader)); 68 | } 69 | } 70 | 71 | public override void Save(BinaryWriter writer) 72 | { 73 | writer.Write(TypeID); 74 | writer.Write(Map.Count); 75 | foreach(var k in Map.Keys) 76 | { 77 | WriteOptionString(writer, new EditableString(k, false)); 78 | Map[k].Save(writer); 79 | } 80 | } 81 | } 82 | 83 | public class SecOptionTuple : OptionType 84 | { 85 | public List Members; 86 | 87 | public SecOptionTuple() : base(1) 88 | { 89 | Members = []; 90 | } 91 | 92 | public SecOptionTuple(BinaryReader reader) : base(1) 93 | { 94 | Members = []; 95 | var count = reader.ReadInt32(); 96 | for(var i = 0; i < count; ++i) 97 | { 98 | Members.Add(ReadOptionType(reader)); 99 | } 100 | } 101 | 102 | public override void Save(BinaryWriter writer) 103 | { 104 | writer.Write(TypeID); 105 | writer.Write(Members.Count); 106 | foreach (var member in Members) 107 | { 108 | member.Save(writer); 109 | } 110 | } 111 | 112 | public override string ToString() 113 | { 114 | return $"Tuple(Length: {Members.Count})"; 115 | } 116 | } 117 | 118 | public class SecOptionInteger : OptionType 119 | { 120 | public int Value; 121 | 122 | public SecOptionInteger() : base(2) 123 | { 124 | Value = 0; 125 | } 126 | 127 | public SecOptionInteger(BinaryReader reader) : base(2) 128 | { 129 | Value = reader.ReadInt32(); 130 | } 131 | 132 | public override void Save(BinaryWriter writer) 133 | { 134 | writer.Write(TypeID); 135 | writer.Write(Value); 136 | } 137 | 138 | public override string ToString() 139 | { 140 | return $"Integer({Value})"; 141 | } 142 | } 143 | 144 | public class SecOptionString : OptionType 145 | { 146 | public EditableString Value; 147 | 148 | public SecOptionString() : base(3) 149 | { 150 | } 151 | 152 | public SecOptionString(BinaryReader reader) : base(3) 153 | { 154 | Value = ReadOptionString(reader); 155 | } 156 | 157 | public override void Save(BinaryWriter writer) 158 | { 159 | writer.Write(TypeID); 160 | WriteOptionString(writer, Value); 161 | } 162 | 163 | 164 | public override string ToString() 165 | { 166 | return $"String({Value})"; 167 | } 168 | } 169 | 170 | public class SecOptionResource : OptionType 171 | { 172 | public int Value; 173 | 174 | public SecOptionResource() : base(4) 175 | { 176 | 177 | } 178 | 179 | public SecOptionResource(BinaryReader reader) : base(4) 180 | { 181 | Value = reader.ReadInt32(); 182 | } 183 | 184 | public override void Save(BinaryWriter writer) 185 | { 186 | writer.Write(TypeID); 187 | writer.Write(Value); 188 | } 189 | 190 | 191 | public override string ToString() 192 | { 193 | return $"Resource({Value})"; 194 | } 195 | } 196 | 197 | public class SecOptionLabel : OptionType 198 | { 199 | public int Value; 200 | 201 | public SecOptionLabel() : base(5) 202 | { 203 | 204 | } 205 | 206 | public SecOptionLabel(BinaryReader reader) : base(5) 207 | { 208 | Value = reader.ReadInt32(); 209 | } 210 | 211 | public override void Save(BinaryWriter writer) 212 | { 213 | writer.Write(TypeID); 214 | writer.Write(Value); 215 | } 216 | 217 | 218 | public override string ToString() 219 | { 220 | return $"Label({Value})"; 221 | } 222 | } 223 | 224 | public class SecOptionBinary : OptionType 225 | { 226 | public byte[] Value; 227 | 228 | public SecOptionBinary() : base(6) 229 | { 230 | Value = []; 231 | } 232 | 233 | public SecOptionBinary(BinaryReader reader) : base(6) 234 | { 235 | Value = reader.ReadBytes(reader.ReadInt32()); 236 | } 237 | 238 | public override void Save(BinaryWriter writer) 239 | { 240 | writer.Write(TypeID); 241 | writer.Write(Value.Length); 242 | writer.Write(Value); 243 | } 244 | 245 | 246 | public override string ToString() 247 | { 248 | return $"byte[{Value.Length}]"; 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /SAS5Lib/SecResource/ResourceManager.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Linq; 3 | using System.Reflection; 4 | 5 | namespace SAS5Lib.SecResource 6 | { 7 | public class ResourceManager 8 | { 9 | List Resources; 10 | 11 | class ResourceRecord 12 | { 13 | public string Name; 14 | public string Type; 15 | public string Source; 16 | 17 | public Dictionary Properties; 18 | 19 | public ResourceRecord(string name, string type, string source) 20 | { 21 | Name = name; 22 | Type = type; 23 | Source = source; 24 | Properties = []; 25 | } 26 | 27 | public void Write(BinaryWriter bw, Dictionary refData) 28 | { 29 | WriteResObj(bw, Name, refData); 30 | WriteResObj(bw, Type, refData); 31 | WriteResObj(bw, Source, refData); 32 | 33 | WriteResObj(bw, Properties.Count, refData); 34 | foreach(var k in Properties.Keys) 35 | { 36 | WriteResObj(bw, k, refData); 37 | WriteResObj(bw, Properties[k], refData); 38 | } 39 | } 40 | 41 | public override string ToString() 42 | { 43 | return $"Name: {Name}, Type: {Type}, Location: {Source}"; 44 | } 45 | } 46 | 47 | public ResourceManager(byte[]? input) 48 | { 49 | Resources = []; 50 | if (input == null) 51 | { 52 | return; 53 | } 54 | using var reader = new BinaryReader(new MemoryStream(input)); 55 | 56 | var size = reader.ReadInt32(); 57 | var data = reader.ReadBytes(size); 58 | var count = reader.ReadInt32(); 59 | 60 | void AddRecord() 61 | { 62 | var a = ReadResObj(reader, data); 63 | var b = ReadResObj(reader, data); 64 | var c = ReadResObj(reader, data); 65 | 66 | if (a is string resourceName && b is string resourceType && c is string resourcePos) 67 | { 68 | ResourceRecord record = new(resourceName, resourceType, resourcePos); 69 | if (ReadResObj(reader, data) is int valCount) 70 | { 71 | for (int i = 0; i < valCount; i++) 72 | { 73 | if (ReadResObj(reader, data) is string fileName) 74 | { 75 | record.Properties.TryAdd(fileName, ReadResObj(reader, data)); 76 | } 77 | } 78 | Resources.Add(record); 79 | } 80 | else 81 | { 82 | Trace.Assert(false); 83 | } 84 | } 85 | else 86 | { 87 | Trace.Assert(false); 88 | } 89 | } 90 | 91 | for(int j = 0; j < count; j++) 92 | { 93 | AddRecord(); 94 | } 95 | } 96 | 97 | static int ReadResVal(BinaryReader reader, byte flag) 98 | { 99 | var bitCount = (flag & 7) + 1; 100 | var result = 0; 101 | Trace.Assert(bitCount > 0 && bitCount <= 4); 102 | 103 | for (int i = 0; i < bitCount; i++) 104 | { 105 | result |= reader.ReadByte() << (i * 8);//LE 106 | } 107 | 108 | if (bitCount == 4) 109 | return result; 110 | 111 | var v6 = result & (1 << (8 * bitCount - 1)); 112 | if (v6 != 0) 113 | result -= 2 * v6; 114 | return result; 115 | } 116 | 117 | static void WriteResVal(BinaryWriter writer, int value, byte flag) 118 | { 119 | int x = Math.Abs(value) >> 8; 120 | byte byteLen = 0; 121 | while(x != 0) 122 | { 123 | x >>= 8; 124 | byteLen++; 125 | } 126 | 127 | if ((value & (1 << (8 * (byteLen + 1) - 1))) != 0 && value > 0) 128 | byteLen++; 129 | 130 | flag |= byteLen; 131 | writer.Write(flag); 132 | for(int i = 0; i < byteLen + 1; i++) 133 | { 134 | writer.Write((byte)(value >> i * 8)); 135 | } 136 | } 137 | 138 | static object ReadResObj(BinaryReader reader, byte[] refData) 139 | { 140 | var type = reader.ReadByte(); 141 | if((type & 0xE0) == 0) 142 | { 143 | return (type & 0xF) - (type & 0x10); 144 | } 145 | 146 | if((type & 0xF8) == 0x80) 147 | { 148 | return ReadResVal(reader, type); 149 | } 150 | 151 | if ((type & 0xF8) == 0x90) 152 | { 153 | var offset = ReadResVal(reader, type); 154 | var len = refData[offset++] | refData[offset++] << 8 | refData[offset++] << 16 | refData[offset++] << 24; 155 | return CodepageManager.Instance.ImportGetString(refData[offset..(offset + len)]); 156 | } 157 | 158 | Trace.Assert(false); 159 | return null; 160 | } 161 | 162 | static void WriteResObj(BinaryWriter writer, object obj, Dictionary refData) 163 | { 164 | if(obj is int numVal) 165 | { 166 | if (numVal <= 15 && numVal >= -16) 167 | { 168 | if(numVal < 0) 169 | numVal += 16; 170 | writer.Write(Convert.ToByte(numVal)); 171 | } 172 | else 173 | { 174 | WriteResVal(writer, numVal, 0x80); 175 | } 176 | } 177 | else if(obj is string strVal) 178 | { 179 | if(refData.TryGetValue(strVal, out int offset)) 180 | { 181 | WriteResVal(writer, offset, 0x90); 182 | } 183 | } 184 | } 185 | 186 | public void UpdateGarResourceRecord(List> fileList, string newArcName) 187 | { 188 | Console.WriteLine($"Updating GAR file records({newArcName})..."); 189 | foreach(var file in fileList) 190 | { 191 | var records = Resources.Where(record => record.Properties.ContainsKey("arc-path") && record.Properties["arc-path"].ToString().Contains(file.Item2)); 192 | 193 | foreach(var rec in records) 194 | { 195 | rec.Properties["path"] = newArcName; 196 | } 197 | } 198 | } 199 | 200 | public void UpdateIarResourceRecord(Dictionary fileList, string newArcName) 201 | { 202 | Console.WriteLine($"Updating IAR file records({newArcName})..."); 203 | foreach (var file in fileList) 204 | { 205 | var records = Resources.Where(record => record.Properties.ContainsKey("arc-index") && record.Properties.ContainsKey("path") && record.Name == file.Key); 206 | 207 | foreach (var rec in records) 208 | { 209 | rec.Properties["path"] = newArcName; 210 | rec.Properties["arc-index"] = file.Value; 211 | } 212 | } 213 | } 214 | 215 | public byte[] GetData() 216 | { 217 | var ms = new MemoryStream(); 218 | using var writer = new BinaryWriter(ms); 219 | var vals = Resources.Select(record => record.Source).Distinct().ToList(); 220 | vals.AddRange(Resources.Select(record => record.Type).Distinct().ToList()); 221 | vals.AddRange(Resources.SelectMany(record => record.Properties.Keys).Distinct().ToList()); 222 | vals.AddRange(Resources.SelectMany(record => record.Properties.Values).OfType().Distinct().ToList()); 223 | vals.AddRange(Resources.Select(record => record.Name).Distinct().ToList()); 224 | vals = vals.Distinct().ToList(); 225 | 226 | Dictionary offsetMap = []; 227 | var ms2 = new MemoryStream(); 228 | using var stringPoolWriter = new BinaryWriter(ms2); 229 | foreach(var record in vals) 230 | { 231 | offsetMap.TryAdd(record, Convert.ToInt32(stringPoolWriter.BaseStream.Position)); 232 | var bytes = CodepageManager.Instance.ImportGetBytes(record); 233 | stringPoolWriter.Write(bytes.Length); 234 | stringPoolWriter.Write(bytes); 235 | } 236 | 237 | var stringPool = ms2.ToArray(); 238 | writer.Write(stringPool.Length); 239 | writer.Write(stringPool); 240 | writer.Write(Resources.Count); 241 | 242 | foreach(var resource in Resources) 243 | { 244 | resource.Write(writer, offsetMap); 245 | } 246 | 247 | return ms.ToArray(); 248 | } 249 | 250 | public Dictionary GetIarFileList(string arcName) 251 | { 252 | arcName = Path.GetFileName(arcName); 253 | var records = Resources.Where(record => record.Properties.ContainsKey("arc-index") && record.Properties.ContainsKey("path") && record.Properties["path"].ToString().Contains(arcName)); 254 | Dictionary ret = []; 255 | foreach (var rec in records) 256 | { 257 | if(rec.Properties["arc-index"] is int index) 258 | { 259 | ret.TryAdd(index, rec.Name); 260 | } 261 | } 262 | return ret; 263 | } 264 | 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /SAS5Lib/SecScenarioProgram.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace SAS5Lib 4 | { 5 | public class SecScenarioProgram 6 | { 7 | readonly Dictionary _sections; 8 | public static int Version { get; private set; } 9 | public static bool LegacyVersion { get; private set; } 10 | 11 | public SecScenarioProgram(string path) 12 | { 13 | _sections = []; 14 | using var reader = new BinaryReader(File.OpenRead(path)); 15 | if (reader.ReadInt32() != 0x35434553)//SEC5 16 | { 17 | Console.WriteLine("Invalid Sec5File format."); 18 | return; 19 | } 20 | Version = reader.ReadInt32(); 21 | LegacyVersion = Version < 109000; 22 | 23 | if(Version < 108000) 24 | { 25 | throw new NotSupportedException($"This sec5 (version {Version}) is too old!"); 26 | } 27 | 28 | while (reader.BaseStream.Position < reader.BaseStream.Length) 29 | { 30 | var sectionName = Encoding.ASCII.GetString(reader.ReadBytes(4)); 31 | var sectionData = reader.ReadBytes(reader.ReadInt32()); 32 | if (sectionName == "CODE") 33 | { 34 | byte k = 0, b = 0; 35 | for (var i = 0; i < sectionData.Length; i++) 36 | { 37 | b = sectionData[i]; 38 | sectionData[i] ^= k; 39 | k += (byte)(b + 0x12); 40 | } 41 | } 42 | _sections.TryAdd(sectionName, sectionData); 43 | } 44 | PrintSecProgramInfo(); 45 | } 46 | 47 | public void Save(string path) 48 | { 49 | using var writer = new BinaryWriter(File.Open(path, FileMode.Create)); 50 | 51 | writer.Write(0x35434553); 52 | writer.Write(Version); 53 | 54 | static byte[] GetSectionName(string name) 55 | { 56 | return CodepageManager.Instance.ImportGetBytes(name)[..4]; 57 | } 58 | 59 | foreach (var sectionName in _sections.Keys) 60 | { 61 | writer.Write(GetSectionName(sectionName)); 62 | var sectionData = _sections[sectionName]; 63 | writer.Write(sectionData.Length); 64 | if (sectionName == "CODE") 65 | { 66 | byte k = 0; 67 | for (var i = 0; i < sectionData.Length; i++) 68 | { 69 | sectionData[i] ^= k; 70 | k += (byte)(sectionData[i] + 0x12); 71 | } 72 | } 73 | writer.Write(sectionData); 74 | } 75 | } 76 | 77 | public byte[]? GetSectionData(string sectionName) 78 | { 79 | if (_sections.TryGetValue(sectionName, out var ret)) 80 | { 81 | return ret; 82 | } 83 | else 84 | { 85 | return null; 86 | } 87 | } 88 | 89 | public void SetSectionData(string sectionName, byte[] sectionData) 90 | { 91 | if (_sections.ContainsKey(sectionName)) 92 | { 93 | _sections[sectionName] = sectionData; 94 | } 95 | } 96 | 97 | public void PrintSecProgramInfo() 98 | { 99 | Console.WriteLine("--------------Code Info---------------"); 100 | Console.WriteLine($"Version: {Version}"); 101 | Console.WriteLine($"Section(s): {string.Join(',', _sections.Keys)}"); 102 | Console.WriteLine("--------------------------------------\n"); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /SAS5Lib/SecSource.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace SAS5Lib 4 | { 5 | public class SecSource 6 | { 7 | public class SecSourceLine 8 | { 9 | public int v1; 10 | public int v2; 11 | public int v3; 12 | public int v4; 13 | object v5; 14 | 15 | public SecSourceLine(BinaryReader reader) 16 | { 17 | v1 = reader.ReadInt32(); 18 | v2 = reader.ReadInt32(); 19 | v3 = reader.ReadInt32(); 20 | v4 = reader.ReadInt32(); 21 | if (v4 == 0) 22 | { 23 | v5 = Array.Empty(); 24 | return; 25 | } 26 | if ((v1 & 1) != 0) 27 | { 28 | var arr = new ushort[v4]; 29 | for (int i = 0; i < v4; i++) 30 | { 31 | arr[i] = reader.ReadUInt16(); 32 | } 33 | v5 = arr; 34 | } 35 | else 36 | { 37 | var arr = new uint[v4]; 38 | for (int i = 0; i < v4; i++) 39 | { 40 | arr[i] = reader.ReadUInt32(); 41 | } 42 | v5 = arr; 43 | } 44 | } 45 | 46 | public void Write(BinaryWriter bw) 47 | { 48 | bw.Write(v1); 49 | bw.Write(v2); 50 | bw.Write(v3); 51 | bw.Write(v4); 52 | if (v4 == 0) 53 | { 54 | return; 55 | } 56 | if ((v1 & 1) != 0) 57 | { 58 | if (v5 is ushort[] arr) 59 | { 60 | foreach (var v in arr) 61 | { 62 | bw.Write(v); 63 | } 64 | } 65 | } 66 | else 67 | { 68 | if (v5 is uint[] arr) 69 | { 70 | foreach (var v in arr) 71 | { 72 | bw.Write(v); 73 | } 74 | } 75 | } 76 | } 77 | 78 | public override string ToString() 79 | { 80 | return $"V1: {v1:X8} V2: {v2:X8} V3: {v3:X8} V4: {v4:X8}"; 81 | } 82 | } 83 | 84 | public class SecSourceFile 85 | { 86 | public string Name; 87 | public int Position; 88 | public List Positions; 89 | 90 | public SecSourceFile(BinaryReader reader) 91 | { 92 | Name = Utils.ReadCString(reader); 93 | Position = reader.ReadInt32(); 94 | Positions = []; 95 | } 96 | 97 | public void Write(BinaryWriter bw) 98 | { 99 | bw.Write(CodepageManager.Instance.ExportGetBytes(Name)); 100 | bw.Write((byte)0); 101 | bw.Write(Position); 102 | } 103 | 104 | public override string ToString() 105 | { 106 | return $"Source: {Name}, Pos: {Position:X8}, Points: {Positions.Count}"; 107 | } 108 | } 109 | 110 | public List SourceFiles; 111 | 112 | public SecSource(byte[]? input) 113 | { 114 | SourceFiles = []; 115 | 116 | if (input == null) 117 | { 118 | return; 119 | } 120 | 121 | using var reader = new BinaryReader(new MemoryStream(input)); 122 | var count = reader.ReadInt32(); 123 | 124 | for (int i = 0; i < count; i++) 125 | { 126 | SourceFiles.Add(new SecSourceFile(reader)); 127 | } 128 | 129 | if (reader.BaseStream.Position != reader.BaseStream.Length) 130 | { 131 | Trace.Assert(reader.ReadInt32() == 0x54505A43);//CZPT 132 | 133 | foreach (var source in SourceFiles) 134 | { 135 | var lines = reader.ReadInt32(); 136 | for (var i = 0; i < lines; ++i) 137 | { 138 | source.Positions.Add(new SecSourceLine(reader)); 139 | } 140 | } 141 | 142 | Trace.Assert(reader.BaseStream.Position == reader.BaseStream.Length); 143 | } 144 | else 145 | { 146 | //Old version ? 147 | } 148 | } 149 | 150 | public byte[] GetData() 151 | { 152 | var ms = new MemoryStream(); 153 | using var writer = new BinaryWriter(ms); 154 | 155 | writer.Write(SourceFiles.Count); 156 | for (int i = 0; i < SourceFiles.Count; i++) 157 | { 158 | SourceFiles[i].Write(writer); 159 | } 160 | 161 | if (SourceFiles.Max(o => o.Positions.Count) > 0) 162 | { 163 | writer.Write(0x54505A43); 164 | for (int i = 0; i < SourceFiles.Count; i++) 165 | { 166 | writer.Write(SourceFiles[i].Positions.Count); 167 | foreach (var line in SourceFiles[i].Positions) 168 | { 169 | line.Write(writer); 170 | } 171 | } 172 | } 173 | 174 | return ms.ToArray(); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /SAS5Lib/SecVariable/ObjectType.cs: -------------------------------------------------------------------------------- 1 | namespace SAS5Lib.SecVariable 2 | { 3 | /* 4 | * Variable Type (Runtime): 5 | * 0 : Byte, Word, Dword 6 | * 1 : Qword 7 | * 2 : Float 8 | * 3 : Double 9 | * 4 : Obj 10 | * 5 : ObjRef 11 | * 6 : InstructionAddr(CodePtr) 12 | */ 13 | 14 | /* 15 | * PrimitiveType(NativeType) ID: 16 | * 0 : Byte 17 | * 1 : Word 18 | * 2 : Dword 19 | * 3 : Qword 20 | * 4 : Float 21 | * 5 : Double 22 | * 6 : Obj sizeof(Object*) == 4 23 | * 7 : ObjRef sizeof(ObjectRefrence) == 12 24 | * 8 : InstructionAddr sizeof(uint32_t*) == 4 25 | */ 26 | 27 | /* 28 | * CompositionType ID: 29 | * 0 : PrimitiveType 30 | * 1 : ArrayType 31 | * 2 : TupleType 32 | * 3 : RecordType 33 | */ 34 | public abstract class BasicType 35 | { 36 | protected byte BasicTypeID; 37 | protected int Size; 38 | public BasicType(byte typeID) 39 | { 40 | BasicTypeID = typeID; 41 | } 42 | 43 | public virtual int GetSize() { return Size; } 44 | } 45 | 46 | public class PrimitiveType : BasicType 47 | { 48 | public byte PrimitiveTypeID; 49 | public PrimitiveType(byte primitiveTypeID) : base(0) 50 | { 51 | PrimitiveTypeID = primitiveTypeID; 52 | } 53 | 54 | public override string ToString() 55 | { 56 | return PrimitiveTypeID switch 57 | { 58 | 0 => "Byte", 59 | 1 => "Word", 60 | 2 => "Dword", 61 | 3 => "Qword", 62 | 4 => "Float", 63 | 5 => "Double", 64 | 6 => "Object", 65 | 7 => "ObjRef", 66 | 8 => "InstructionAddr", 67 | _ => $"NativeType({PrimitiveTypeID})" 68 | }; 69 | } 70 | 71 | public override int GetSize() 72 | { 73 | switch (PrimitiveTypeID) 74 | { 75 | case 0: 76 | return 1; 77 | case 1: 78 | return 2; 79 | case 2: 80 | case 4: 81 | case 8: 82 | return 4; 83 | case 3: 84 | case 5: 85 | return 8; 86 | case 6: 87 | case 7: 88 | return 0; 89 | default: 90 | throw new Exception("Fatal: Unknown primitive type."); 91 | } 92 | } 93 | } 94 | 95 | public class ArrayType : BasicType 96 | { 97 | public int ElementCount; 98 | public BasicType ElementType; 99 | 100 | public ArrayType(int elemCount, BasicType elemType) : base(1) 101 | { 102 | ElementCount = elemCount; 103 | ElementType = elemType; 104 | Size = elemCount * elemType.GetSize(); 105 | } 106 | 107 | public override string ToString() 108 | { 109 | return $"Array<{ElementType}>[{ElementCount}]"; 110 | } 111 | } 112 | 113 | public class TupleType : BasicType 114 | { 115 | //TODO: Analyze this type(probably same as record type) 116 | public TupleType() : base(2) 117 | { 118 | } 119 | } 120 | 121 | public class RecordType : BasicType 122 | { 123 | public int MemberCount; 124 | public List Members; 125 | 126 | public RecordType(int memberCount, List members) : base(3) 127 | { 128 | MemberCount = memberCount; 129 | Members = members; 130 | foreach (var member in Members) 131 | { 132 | Size += member.Type.GetSize(); 133 | } 134 | } 135 | 136 | public override string ToString() 137 | { 138 | return "Struct"; 139 | } 140 | } 141 | 142 | public class ObjectType 143 | { 144 | public string Name; 145 | public BasicType Type; 146 | 147 | public ObjectType(string name, BasicType type) 148 | { 149 | Name = name; 150 | Type = type; 151 | } 152 | 153 | public override string ToString() 154 | { 155 | return $"Name: {Name}, Type: {Type}, ElementSize: {Type.GetSize()}"; 156 | } 157 | } 158 | 159 | public class Object 160 | { 161 | public int Index; 162 | public ObjectType Type; 163 | public byte[] Data; 164 | 165 | public Object(int index, ObjectType type, byte[] data) 166 | { 167 | Index = index; 168 | Type = type; 169 | Data = data; 170 | } 171 | 172 | public override string ToString() 173 | { 174 | return $"Object{{{Type}}}"; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /SAS5Lib/SecVariable/PresetVariables.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SAS5Lib.SecVariable 8 | { 9 | public class PresetVariables 10 | { 11 | readonly int VarAIndex; 12 | readonly ObjectType VarAType; 13 | readonly int VarBIndex; 14 | readonly ObjectType VarBType; 15 | 16 | public PresetVariables(byte[]? input) 17 | { 18 | if(input == null) 19 | { 20 | VarAType = VarBType = new ObjectType("placeholder", new PrimitiveType(0)); 21 | return; 22 | } 23 | 24 | using var reader = new BinaryReader(new MemoryStream(input)); 25 | VarAIndex = reader.ReadInt32(); 26 | VarBIndex = reader.ReadInt32(); 27 | VarAType = VariableManager.Instance.GetType(VarAIndex); 28 | VarBType = VariableManager.Instance.GetType(VarBIndex); 29 | } 30 | 31 | public override string ToString() 32 | { 33 | return $"GlobalStatic:{VarAType}({VarAIndex}), GlobalPersistent:{VarBType}({VarBIndex})"; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SAS5Lib/SecVariable/VariableManager.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace SAS5Lib.SecVariable 4 | { 5 | public class VariableManager : Singleton 6 | { 7 | readonly List VariableTypes = []; 8 | 9 | BasicType ReadType(BinaryReader reader) 10 | { 11 | var basicTypeID = reader.ReadByte(); 12 | switch (basicTypeID) 13 | { 14 | case 0xFF: 15 | return GetType(reader.ReadInt32()).Type; 16 | case 0x00: 17 | return new PrimitiveType(reader.ReadByte()); 18 | case 0x01: 19 | return new ArrayType(reader.ReadInt32(), ReadType(reader)); 20 | //case 0x02: 21 | case 0x03: 22 | { 23 | var typeList = new List(); 24 | var count = reader.ReadInt32(); 25 | for (int i = 0; i < count; i++) 26 | { 27 | typeList.Add(new ObjectType(Utils.ReadCString(reader), ReadType(reader))); 28 | } 29 | return new RecordType(count, typeList); 30 | } 31 | default: 32 | throw new Exception($"Unknown BasicTypeID:{basicTypeID}"); 33 | } 34 | } 35 | 36 | public void LoadVariablesList(string varDefFile) 37 | { 38 | LoadVariablesList(File.ReadAllBytes(varDefFile)); 39 | } 40 | 41 | public void LoadVariablesList(byte[]? input) 42 | { 43 | if (input == null) 44 | { 45 | return; 46 | } 47 | using var reader = new BinaryReader(new MemoryStream(input)); 48 | VariableTypes.Capacity = reader.ReadInt32(); 49 | for (int i = 0; i < VariableTypes.Capacity; i++) 50 | { 51 | VariableTypes.Add(new ObjectType(Utils.ReadCString(reader), ReadType(reader))); 52 | } 53 | } 54 | 55 | public ObjectType GetType(int typeIndex) 56 | { 57 | return VariableTypes[typeIndex]; 58 | } 59 | 60 | public int GetVariableTypeIndexByNameRegex(string pattern) 61 | { 62 | var vt = VariableTypes.FirstOrDefault(x => Regex.IsMatch(x.Name, pattern)); 63 | if(vt == null) 64 | { 65 | return -1; 66 | } 67 | return VariableTypes.IndexOf(vt); 68 | } 69 | 70 | public string GetVariableTypeName(int index) 71 | { 72 | return VariableTypes[index].Name; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SAS5Tool.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.33328.57 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecTool", "SecTool\SecTool.csproj", "{99436766-D2B6-4519-ACF3-C267939BF1FB}" 7 | ProjectSection(ProjectDependencies) = postProject 8 | {44B75E99-ABE6-463C-8CF6-9CA8AAAC9103} = {44B75E99-ABE6-463C-8CF6-9CA8AAAC9103} 9 | EndProjectSection 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArcTool", "ArcTool\ArcTool.csproj", "{5794DD37-A1D3-4010-8D23-D173CDC20329}" 12 | ProjectSection(ProjectDependencies) = postProject 13 | {44B75E99-ABE6-463C-8CF6-9CA8AAAC9103} = {44B75E99-ABE6-463C-8CF6-9CA8AAAC9103} 14 | EndProjectSection 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SAS5Lib", "SAS5Lib\SAS5Lib.csproj", "{44B75E99-ABE6-463C-8CF6-9CA8AAAC9103}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {99436766-D2B6-4519-ACF3-C267939BF1FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {99436766-D2B6-4519-ACF3-C267939BF1FB}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {99436766-D2B6-4519-ACF3-C267939BF1FB}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {99436766-D2B6-4519-ACF3-C267939BF1FB}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {5794DD37-A1D3-4010-8D23-D173CDC20329}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {5794DD37-A1D3-4010-8D23-D173CDC20329}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {5794DD37-A1D3-4010-8D23-D173CDC20329}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {5794DD37-A1D3-4010-8D23-D173CDC20329}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {44B75E99-ABE6-463C-8CF6-9CA8AAAC9103}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {44B75E99-ABE6-463C-8CF6-9CA8AAAC9103}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {44B75E99-ABE6-463C-8CF6-9CA8AAAC9103}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {44B75E99-ABE6-463C-8CF6-9CA8AAAC9103}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {CE5B478B-88FA-4D7C-8078-29B1C20EB006} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /SecTool/Program.cs: -------------------------------------------------------------------------------- 1 | using SAS5Lib.SecCode; 2 | using SAS5Lib.SecOption; 3 | using SAS5Lib.SecVariable; 4 | using System.Text; 5 | using CommandLine; 6 | using CommandLine.Text; 7 | using Newtonsoft.Json; 8 | using static SecTool.Program; 9 | 10 | namespace SecTool 11 | { 12 | internal class Program 13 | { 14 | public class BaseOptions 15 | { 16 | [Option('s', "sec5", Required = true, HelpText = "Path to sec5 file.")] 17 | public string InputFile { get; set; } 18 | 19 | [Option('r', "read-encoding", Default = null, HelpText = "Encoding of the strings inside the sec5. You can set the encoding to one of the following values: sjis, gbk, utf8.")] 20 | public string? ImportEncoding { get; set; } 21 | 22 | [Option('w', "write-encoding", Default = "utf8", HelpText = "Encoding of the strings inside the sec5. You can set the encoding to one of the following values: sjis, gbk, utf8.")] 23 | public string ExportEncoding { get; set; } 24 | } 25 | 26 | [Verb("export", aliases: ["ex"], HelpText = "Extracting dialogues inside the sec5 specified and output mutiple .txt files to the folder.")] 27 | public class ExportOptions : BaseOptions 28 | { 29 | [Option('o', "output", Required = true, HelpText = "Destnation of text files.")] 30 | public string OutputFolder { get; set; } 31 | 32 | [Usage(ApplicationAlias = "SecTool")] 33 | public static IEnumerable Examples 34 | { 35 | get 36 | { 37 | yield return new Example("Export a file using default encoding", new ExportOptions { InputFile = "A:/B/Games/main.sec5", OutputFolder = "A:/B/Games/Texts" }); 38 | yield return new Example("Specify a read encoding", new ExportOptions { InputFile = "A:/B/Games/main.sec5", OutputFolder = "A:/B/Games/Texts", ImportEncoding = "sjis" }); 39 | } 40 | } 41 | } 42 | 43 | [Verb("export-str", aliases: ["exstr"], HelpText = "Extracting strings inside the sec5 specified with code name(s) and output .txt file.")] 44 | public class ExportStrOptions : BaseOptions 45 | { 46 | [Option('o', "output", Required = false, HelpText = "Destnation of text files.")] 47 | public string OutputFolder { get; set; } 48 | 49 | [Option('n', "codename", Required = true, HelpText = "target file name.")] 50 | public IEnumerable CodeNames { get; set; } 51 | 52 | [Usage(ApplicationAlias = "SecTool")] 53 | public static IEnumerable Examples 54 | { 55 | get 56 | { 57 | yield return new Example("Export a file using default encoding", new ExportStrOptions { InputFile = "A:/B/Games/main.sec5", CodeNames = ["msg.sal"], OutputFolder = "A:/B/Games/Strings" }); 58 | yield return new Example("Specify a read encoding", new ExportStrOptions { InputFile = "A:/B/Games/main.sec5", CodeNames = ["msg.sal", "scene.sal"], OutputFolder = "A:/B/Games/Strings", ImportEncoding = "sjis" }); 59 | } 60 | } 61 | } 62 | 63 | [Verb("import", aliases: ["im"], HelpText = "Import all .txt files inside the folder and output a new sec5 file.")] 64 | public class ImportOptions : BaseOptions 65 | { 66 | [Option('i', "input", Required = true, HelpText = "Path to text files you want to import.")] 67 | public string InputFolder { get; set; } 68 | 69 | [Option('o', "output", HelpText = "FullPath or FileName of the new sec5.")] 70 | public string OutputPath { get; set; } 71 | 72 | [Option('d', "debugbuild", HelpText = "Generates a datetime on game's title(isolate save data).", Default = false)] 73 | public bool DebugBuild { get; set; } 74 | 75 | [Usage(ApplicationAlias = "SecTool")] 76 | public static IEnumerable Examples 77 | { 78 | get 79 | { 80 | yield return new Example("Import a file using default encoding", new ImportOptions { InputFile = "A:/B/Games/main.sec5", InputFolder = "A:/B/Games/Texts", OutputPath = "A:/B/Games/main.sec5.new" }); 81 | yield return new Example("Specify a read encoding", new ImportOptions { InputFile = "A:/B/Games/main.sec5", InputFolder = "A:/B/Games/Texts", OutputPath = "A:/B/Games/main.sec5.new", ImportEncoding = "sjis" }); 82 | yield return new Example("Specify a write encoding", new ImportOptions { InputFile = "A:/B/Games/main.sec5", InputFolder = "A:/B/Games/Texts", OutputPath = "A:/B/Games/main.sec5.new", ExportEncoding = "sjis" }); 83 | yield return new Example("Specify both read encoding and write encoding", new ImportOptions { InputFile = "A:/B/Games/main.sec5", InputFolder = "A:/B/Games/Texts", OutputPath = "A:/B/Games/main.sec5.new", ImportEncoding = "sjis", ExportEncoding = "sjis" }); 84 | yield return new Example("Don't specify the output path(will use input file's path and name + .new)", new ImportOptions { InputFile = "A:/B/Games/main.sec5", InputFolder = "A:/B/Games/Texts" }); 85 | yield return new Example("Specify only output file name(will use input file's path)", new ImportOptions { InputFile = "A:/B/Games/main.sec5", InputFolder = "A:/B/Games/Texts", OutputPath = "main.sec5.new" }); 86 | yield return new Example("Specify only output file path(will use input file's name + .new)", new ImportOptions { InputFile = "A:/B/Games/main.sec5", InputFolder = "A:/B/Games/Texts", OutputPath = "A:/B/Games/" }); 87 | } 88 | } 89 | } 90 | 91 | [Verb("import-str", aliases: ["imstr"], HelpText = "Importing strings inside the sec5 specified with code name(s).")] 92 | public class ImportStrOptions : BaseOptions 93 | { 94 | [Option('o', "output", Required = false, HelpText = "FullPath or FileName of the new sec5.")] 95 | public string OutputPath { get; set; } 96 | 97 | [Option('i', "input", Required = true, HelpText = "Pairs of code name and input file path")] 98 | public IEnumerable CodeNames { get; set; } 99 | 100 | [Usage(ApplicationAlias = "SecTool")] 101 | public static IEnumerable Examples 102 | { 103 | get 104 | { 105 | yield return new Example("Export a file using default encoding", new ImportStrOptions { InputFile = "A:/B/Games/main.sec5", CodeNames = ["msg.sal", "A:/B/Games/Strings/msg.sal_strings.txt"], OutputPath = "main.sec5.new" }); 106 | yield return new Example("Specify a read encoding", new ImportStrOptions { InputFile = "A:/B/Games/main.sec5", CodeNames = ["msg.sal", "A:/B/Games/Strings/msg.sal_strings.txt", "scene.sal", "A:/B/Games/Strings/scene.sal_strings.txt"], OutputPath = "main.sec5.new", ImportEncoding = "sjis" }); 107 | } 108 | } 109 | } 110 | 111 | [Verb("get", HelpText = "Get a raw data section from a .sec5 file.")] 112 | public class SectionGetOptions : BaseOptions 113 | { 114 | [Option('n', "section-name", Required = true, HelpText = "You can get one of the following sections: 'CODE', 'OPTN', 'CHAR', 'CZIT', 'DTDE', 'RES2', 'RTFC', 'VARI'")] 115 | public string SectionName { get; set; } 116 | 117 | [Option('o', "output")] 118 | public string OutputPath { get; set; } 119 | } 120 | 121 | [Verb("set", HelpText = "Set a raw data section to a .sec5 file.")] 122 | public class SectionSetOptions : BaseOptions 123 | { 124 | [Option('n', "section-name", Required = true, HelpText = "You can set one of the following sections: 'CODE', 'OPTN', 'CHAR', 'CZIT', 'DTDE', 'RES2', 'RTFC', 'VARI'")] 125 | public string SectionName { get; set; } 126 | 127 | [Option('i', "input", Required = true)] 128 | public string InputSectionFile { get; set; } 129 | 130 | [Option('o', "output")] 131 | public string OutputPath { get; set; } 132 | } 133 | 134 | [Verb("info", HelpText = "View the info of a .sec5 file.")] 135 | public class InfoOptions 136 | { 137 | [Option('s', "sec5", Required = true, HelpText = "Path to sec5 file.")] 138 | public string InputFile { get; set; } 139 | } 140 | 141 | static void Main(string[] args) 142 | { 143 | Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 144 | var parser = new Parser(with => with.HelpWriter = null); 145 | var parserResult = parser.ParseArguments(args); 146 | parserResult 147 | .WithParsed(Run) 148 | .WithParsed(Run) 149 | .WithParsed(Run) 150 | .WithParsed(Run) 151 | .WithParsed(ProcessSection) 152 | .WithParsed(ProcessSection) 153 | .WithParsed(PrintInfo) 154 | .WithNotParsed(errs => DisplayHelp(parserResult)); 155 | } 156 | 157 | static HelpText GetHelpText() 158 | { 159 | return new HelpText() 160 | { 161 | AutoHelp = false, 162 | AutoVersion = false, 163 | AdditionalNewLineAfterOption = false, 164 | Heading = "SAS5Tool.SecTool 1.0.0", 165 | Copyright = "A tool to extract & import messages/strings inside .sec5 file." 166 | }; 167 | } 168 | 169 | static void DisplayHelp(ParserResult result) 170 | { 171 | var helpText = HelpText.AutoBuild(result, h => 172 | { 173 | return HelpText.DefaultParsingErrorsHandler(result, GetHelpText()); 174 | }, e => e, 175 | verbsIndex: true); 176 | Console.WriteLine(helpText); 177 | } 178 | 179 | static void Run(BaseOptions opt) 180 | { 181 | SecScenarioProgram prog = new(opt.InputFile); 182 | 183 | if (opt.ImportEncoding != null) 184 | { 185 | CodepageManager.Instance.SetImportEncoding(opt.ImportEncoding); 186 | } 187 | else 188 | { 189 | var c = new SecCodePage(prog.GetSectionData("CHAR")); 190 | if (c.FileReadingCodePage != 0) 191 | { 192 | CodepageManager.Instance.SetImportEncoding(c.FileReadingCodePage); 193 | } 194 | else 195 | { 196 | CodepageManager.Instance.SetImportEncoding("sjis"); 197 | } 198 | } 199 | CodepageManager.Instance.SetExportEncoding(opt.ExportEncoding); 200 | 201 | VariableManager.Instance.LoadVariablesList(prog.GetSectionData("DTDE")); 202 | var vari = new PresetVariables(prog.GetSectionData("VARI")); 203 | SecTextTool.SetTextFlag(); 204 | 205 | var charset = opt.ExportEncoding == "gbk" ? new SecCodePage(0) : new SecCodePage(CodepageManager.Instance.ExportCodePage); 206 | var source = new SecSource(prog.GetSectionData("CZIT")); 207 | var code = new ScenarioCode(prog.GetSectionData("CODE"), source); 208 | var option = new OptionManager(prog.GetSectionData("OPTN")); 209 | option.PrintGameInfo(); 210 | code.Disasemble(); 211 | 212 | switch (opt) 213 | { 214 | case ExportOptions expOpt: 215 | { 216 | var text = SecTextTool.GetText(code.Code); 217 | if (!Path.Exists(expOpt.OutputFolder)) 218 | { 219 | Directory.CreateDirectory(expOpt.OutputFolder); 220 | } 221 | SecTextTool.ExportText(expOpt.OutputFolder, text.Item1); 222 | File.WriteAllText(Path.Combine(expOpt.OutputFolder, "names.json"), JsonConvert.SerializeObject(text.Item2, Formatting.Indented)); 223 | option.Save(Path.Combine(expOpt.OutputFolder, "options.json")); 224 | break; 225 | } 226 | case ExportStrOptions expsOpt: 227 | { 228 | if (!Path.Exists(expsOpt.OutputFolder)) 229 | { 230 | Directory.CreateDirectory(expsOpt.OutputFolder); 231 | } 232 | foreach (var name in expsOpt.CodeNames) 233 | { 234 | SecTextTool.ExportStrings(SecTextTool.GetString(code.Code, name), Path.Combine(expsOpt.OutputFolder, $"{name}_strings.txt")); 235 | } 236 | break; 237 | } 238 | case ImportOptions impOpt: 239 | { 240 | if (Path.Exists(impOpt.InputFolder)) 241 | { 242 | var txt = SecTextTool.ImportText(impOpt.InputFolder); 243 | option.Load(Path.Combine(impOpt.InputFolder, "options.json"), impOpt.DebugBuild); 244 | 245 | if (Path.Exists(Path.Combine(impOpt.InputFolder, "names.json"))) 246 | { 247 | Console.WriteLine($"Using names.json..."); 248 | var nameMap = JsonConvert.DeserializeObject>(File.ReadAllText(Path.Combine(impOpt.InputFolder, "names.json"))); 249 | SecTextTool.SetText(txt, code.Code, nameMap); 250 | } 251 | else 252 | { 253 | SecTextTool.SetText(txt, code.Code); 254 | } 255 | var secData = code.Assemble(); 256 | option.UpdateExportFuncAddr(secData.Item2); 257 | 258 | prog.SetSectionData("CODE", secData.Item1); 259 | prog.SetSectionData("CZIT", source.GetData()); 260 | prog.SetSectionData("CHAR", charset.GetData()); 261 | prog.SetSectionData("OPTN", option.GetData()); 262 | 263 | prog.Save(GetOutputFilepath(impOpt.OutputPath, opt.InputFile)); 264 | } 265 | else 266 | { 267 | Console.WriteLine($"Input directory '{impOpt.InputFolder}' dosen't exists."); 268 | } 269 | break; 270 | } 271 | case ImportStrOptions impsOpt: 272 | { 273 | var lst = impsOpt.CodeNames.ToList(); 274 | for (int i = 0; i < lst.Count; i += 2) 275 | { 276 | SecTextTool.SetString(code.Code, lst[i], SecTextTool.ImportStrings(lst[i + 1])); 277 | } 278 | var secData = code.Assemble(); 279 | option.UpdateExportFuncAddr(secData.Item2); 280 | 281 | prog.SetSectionData("CODE", secData.Item1); 282 | prog.SetSectionData("CZIT", source.GetData()); 283 | prog.SetSectionData("CHAR", charset.GetData()); 284 | prog.SetSectionData("OPTN", option.GetData()); 285 | 286 | prog.Save(GetOutputFilepath(impsOpt.OutputPath, opt.InputFile)); 287 | break; 288 | } 289 | } 290 | } 291 | 292 | static void PrintInfo(object opt) 293 | { 294 | if(opt is InfoOptions infoOptions) 295 | { 296 | SecScenarioProgram prog = new(infoOptions.InputFile); 297 | 298 | var charset = new SecCodePage(prog.GetSectionData("CHAR")); 299 | if(charset.FileReadingCodePage != 0) 300 | { 301 | CodepageManager.Instance.SetImportEncoding(charset.FileReadingCodePage); 302 | } 303 | else 304 | { 305 | CodepageManager.Instance.SetImportEncoding("sjis"); 306 | } 307 | 308 | var option = new OptionManager(prog.GetSectionData("OPTN")); 309 | option.PrintGameInfo(); 310 | } 311 | } 312 | 313 | static void ProcessSection(BaseOptions opt) 314 | { 315 | SecScenarioProgram prog = new(opt.InputFile); 316 | 317 | switch (opt) 318 | { 319 | case SectionGetOptions sectionGetOptions: 320 | { 321 | File.WriteAllBytes(sectionGetOptions.OutputPath ?? $"{sectionGetOptions.InputFile}.{sectionGetOptions.SectionName}", prog.GetSectionData(sectionGetOptions.SectionName)); 322 | break; 323 | } 324 | case SectionSetOptions sectionSetOptions: 325 | { 326 | prog.SetSectionData(sectionSetOptions.SectionName, File.ReadAllBytes(sectionSetOptions.InputSectionFile)); 327 | prog.Save(GetOutputFilepath(sectionSetOptions.OutputPath, sectionSetOptions.InputFile)); 328 | break; 329 | } 330 | } 331 | } 332 | 333 | static string GetOutputFilepath(string path, string defaultPath) 334 | { 335 | if (path != null) 336 | { 337 | var dir = Path.GetDirectoryName(path); 338 | var file = Path.GetFileName(path); 339 | if (dir != "" && file != "") 340 | { 341 | return path; 342 | } 343 | else 344 | { 345 | if (dir != "") 346 | { 347 | return Path.Combine(dir, Path.GetFileName(defaultPath) + ".new"); 348 | } 349 | else 350 | { 351 | return Path.Combine(Path.GetDirectoryName(defaultPath), file); 352 | } 353 | } 354 | } 355 | else 356 | { 357 | return defaultPath + ".new"; 358 | } 359 | } 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /SecTool/SecTextTool.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text.RegularExpressions; 3 | using SAS5Lib.SecCode; 4 | using SAS5Lib.SecVariable; 5 | 6 | namespace SecTool 7 | { 8 | using Script = List>>; 9 | 10 | partial class SecRegex 11 | { 12 | [GeneratedRegex(@"(\{(.+?):(.+?)\})")] 13 | public static partial Regex RubyPattern(); 14 | } 15 | 16 | public class SecTextTool 17 | { 18 | // "Flag" is actually index of variable type array. 19 | static int FLG_NAME; 20 | static int FLG_TITLE; 21 | static int FLG_SELECT; 22 | public SecTextTool() 23 | { 24 | } 25 | 26 | public static void SetTextFlag() 27 | { 28 | FLG_NAME = VariableManager.Instance.GetVariableTypeIndexByNameRegex(@"msg\.sal::name::"); 29 | FLG_TITLE = VariableManager.Instance.GetVariableTypeIndexByNameRegex(@"scene\.sal::scene::"); 30 | FLG_SELECT = VariableManager.Instance.GetVariableTypeIndexByNameRegex(@"selection\.sal::(.+?_?)option::"); 31 | } 32 | 33 | static string GetRubyText(ExecutorCommand cmd) 34 | { 35 | Trace.Assert(cmd.Expression != null); 36 | var ret = ""; 37 | for (int i = 0; i < cmd.Expression.Count; i += 2) 38 | { 39 | if (cmd.Expression[i].Operations[0].Data is EditableString origText && cmd.Expression[i + 1].Operations[0].Data is EditableString rubyText) 40 | { 41 | ret += $"{{{origText.Text}:{rubyText.Text}}}"; 42 | } 43 | else 44 | { 45 | throw new Exception("Invalid ruby text format"); 46 | } 47 | } 48 | return ret; 49 | } 50 | 51 | public class Dialogue(string character, string text) 52 | { 53 | public string Character = character; 54 | public string Text = text; 55 | 56 | public override string ToString() 57 | { 58 | return $"[{Character}] {Text}"; 59 | } 60 | } 61 | 62 | public static List GetString(List secCode, string scriptName) 63 | { 64 | List result = []; 65 | 66 | foreach (var obj in secCode) 67 | { 68 | if (obj is not NamedCode nc || !nc.Name.Contains(scriptName)) 69 | { 70 | continue; 71 | } 72 | foreach (var code in nc.Code) 73 | { 74 | if (code is ExecutorCommand cmd) 75 | { 76 | if (cmd.Expression == null) 77 | { 78 | continue; 79 | } 80 | foreach (var exp in cmd.Expression) 81 | { 82 | if (exp.Operations == null) 83 | { 84 | continue; 85 | } 86 | foreach (var operation in exp.Operations) 87 | { 88 | if (operation.Data is EditableString str && !string.IsNullOrEmpty(str.Text)) 89 | { 90 | result.Add(str.Text); 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | 98 | return result; 99 | } 100 | public static void SetString(List secCode, string scriptName, List strs) 101 | { 102 | var idx = 0; 103 | foreach (var obj in secCode) 104 | { 105 | if (obj is not NamedCode nc || !nc.Name.Contains(scriptName)) 106 | { 107 | continue; 108 | } 109 | foreach (var code in nc.Code) 110 | { 111 | if (code is ExecutorCommand cmd) 112 | { 113 | if (cmd.Expression == null) 114 | { 115 | continue; 116 | } 117 | foreach (var exp in cmd.Expression) 118 | { 119 | if (exp.Operations == null) 120 | { 121 | continue; 122 | } 123 | foreach (var operstion in exp.Operations) 124 | { 125 | if (operstion.Data is EditableString str && !string.IsNullOrEmpty(str.Text)) 126 | { 127 | str.Text = strs[idx]; 128 | operstion.Data = str; 129 | idx++; 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | Trace.Assert(idx == strs.Count); 137 | } 138 | 139 | public static Tuple> GetText(List secCode) 140 | { 141 | Script text = []; 142 | Dictionary nameMap = []; 143 | List messages = []; 144 | string character = ""; 145 | string dialogue = ""; 146 | 147 | byte STR_PUSH_OP = 0x20; 148 | byte STR_LOAD_OP = 0x1E; 149 | int STR_LOAD_INDEX = 6; 150 | int STR_LOAD_ID2_INDEX = 14; 151 | int STR_LOAD_INDEX2 = 16; 152 | 153 | if(!SecScenarioProgram.LegacyVersion) 154 | { 155 | STR_PUSH_OP = 0x78; 156 | STR_LOAD_OP = 0x7A; 157 | STR_LOAD_INDEX = 2; 158 | STR_LOAD_ID2_INDEX = 5; 159 | STR_LOAD_INDEX2 = 6; 160 | } 161 | 162 | foreach (var obj in secCode) 163 | { 164 | if (obj is not NamedCode nc) 165 | { 166 | continue; 167 | } 168 | foreach (var code in nc.Code) 169 | { 170 | if (code is EditableString s) 171 | { 172 | dialogue += s.Text; 173 | } 174 | else if (code is ExecutorCommand cmd) 175 | { 176 | if (cmd.ExecutorIndex == 0x03) 177 | { 178 | if (dialogue != null && dialogue != "") 179 | { 180 | messages.Add(new Dialogue(character, dialogue)); 181 | dialogue = ""; 182 | character = ""; 183 | } 184 | } 185 | else if (cmd.ExecutorIndex == 0x1F8) 186 | { 187 | if (dialogue != "") 188 | { 189 | dialogue += "\\n"; 190 | } 191 | } 192 | else if (cmd.ExecutorIndex == 0x1F9) 193 | { 194 | dialogue += GetRubyText(cmd); 195 | } 196 | else if (cmd.ExecutorIndex == 0x12) 197 | { 198 | if (cmd.Expression == null || (!SecScenarioProgram.LegacyVersion && cmd.Expression.Count < 2)) 199 | { 200 | continue; 201 | } 202 | var operations = cmd.Expression[0].Operations; 203 | if (operations[0].Op != STR_PUSH_OP || (SecScenarioProgram.LegacyVersion && operations.Count < STR_LOAD_INDEX)) 204 | { 205 | continue; 206 | } 207 | //This is kind of string related 208 | if (operations[0].Data is not uint id || operations[STR_LOAD_INDEX].Op != STR_LOAD_OP || operations[STR_LOAD_INDEX].Data is not EditableString data) 209 | { 210 | continue; 211 | } 212 | if (id == FLG_NAME) 213 | { 214 | character = data.Text; 215 | if (operations.Count >= STR_LOAD_INDEX2) 216 | { 217 | if (Convert.ToByte(operations[STR_LOAD_ID2_INDEX].Data) == 0x03 && operations[STR_LOAD_INDEX2].Data is EditableString data2) 218 | { 219 | character += $" -> {data2.Text}"; 220 | } 221 | } 222 | nameMap.TryAdd(character, character); 223 | } 224 | else if (id == FLG_TITLE) 225 | { 226 | messages.Add(new Dialogue("标题", data.Text)); 227 | } 228 | else if (id == FLG_SELECT) 229 | { 230 | messages.Add(new Dialogue("选项", data.Text)); 231 | } 232 | } 233 | } 234 | } 235 | if (messages.Count > 0) 236 | { 237 | text.Add(new Tuple>(nc.Name, messages)); 238 | messages = []; 239 | } 240 | } 241 | return new Tuple>(text, nameMap); 242 | } 243 | 244 | public static void SetText(Script text, List secCode, Dictionary? nameMap = null) 245 | { 246 | var TYPE_DIALOGUE = 1 << 0; 247 | var TYPE_CHARNAME = 1 << 1; 248 | var TYPE_TITLE = 1 << 2; 249 | var TYPE_SELECT = 1 << 3; 250 | var TITLE_SCENE = 0x80; 251 | var TITLE_PLOT = 0x40; 252 | 253 | byte STR_PUSH_OP = 0x20; 254 | byte STR_LOAD_POS_OP1 = 0x13; 255 | byte STR_LOAD_OP = 0x1E; 256 | byte STR_LOAD_POS_OP2 = 0x13; 257 | int STR_LOAD_POS = 4; 258 | int STR_LOAD_INDEX = 6; 259 | int STR_LOAD_ID2_INDEX = 14; 260 | int STR_LOAD_INDEX2 = 16; 261 | int NAME_EXPR_CP_POS1 = 13; 262 | int NAME_EXPR_CP_POS2 = 23; 263 | 264 | if (!SecScenarioProgram.LegacyVersion) 265 | { 266 | STR_PUSH_OP = 0x78; 267 | STR_LOAD_POS_OP1 = 0x79; 268 | STR_LOAD_OP = 0x7A; 269 | STR_LOAD_POS_OP2 = 0x7B; 270 | STR_LOAD_POS = 1; 271 | STR_LOAD_INDEX = 2; 272 | STR_LOAD_ID2_INDEX = 5; 273 | STR_LOAD_INDEX2 = 6; 274 | NAME_EXPR_CP_POS1 = 5; 275 | NAME_EXPR_CP_POS2 = 9; 276 | } 277 | 278 | NamedCode current; 279 | Dictionary> dict = []; 280 | foreach (var t in text) 281 | { 282 | dict.Add(t.Item1, t.Item2); 283 | } 284 | 285 | object ConvertDialogue(int type, Dialogue dialogue, object? cmdref = null) 286 | { 287 | if ((type & TYPE_DIALOGUE) != 0)//normal text 288 | { 289 | var ret = new List(); 290 | var text = dialogue.Text; 291 | foreach (var line in text.Split("\\n")) 292 | { 293 | if (SecRegex.RubyPattern().IsMatch(line)) 294 | { 295 | ExecutorCommand cmd = new(0x1B, 0x1F9) 296 | { 297 | Expression = [] 298 | }; 299 | 300 | var group = SecRegex.RubyPattern().Matches(line); 301 | var lastMatchEnd = 0; 302 | for (int i = 0; i < group.Count; ++i) 303 | { 304 | if (lastMatchEnd != group[i].Index) 305 | { 306 | if (cmd.Expression.Count > 0) 307 | { 308 | ret.Add(cmd); 309 | cmd = new(0x1B, 0x1F9) 310 | { 311 | Expression = [] 312 | }; 313 | } 314 | ret.Add(new EditableString(line[lastMatchEnd..group[i].Index], true)); 315 | } 316 | 317 | var g = group[i].Groups; 318 | var expr = new List 319 | { 320 | new(0, 1, [ 321 | new ExpressionOperation(STR_LOAD_OP, new EditableString(line.Substring(g[2].Index, g[2].Length), true)), 322 | new ExpressionOperation(0xFF, null) 323 | ]), 324 | new(0, 2, [ 325 | new ExpressionOperation(STR_LOAD_OP, new EditableString(line.Substring(g[3].Index, g[3].Length), true)), 326 | new ExpressionOperation(0xFF, null) 327 | ]) 328 | }; 329 | cmd.Expression.AddRange(expr); 330 | 331 | lastMatchEnd = group[i].Index + group[i].Length; 332 | } 333 | ret.Add(cmd); 334 | if (lastMatchEnd != line.Length) 335 | { 336 | ret.Add(new EditableString(line[lastMatchEnd..], true)); 337 | } 338 | } 339 | else 340 | { 341 | ret.Add(new EditableString(line, true)); 342 | } 343 | ret.Add(new ExecutorCommand(0x1B, 0x1F8));//Line break; 344 | } 345 | ret.RemoveAt(ret.Count - 1); 346 | return ret; 347 | } 348 | else if ((type & TYPE_CHARNAME) != 0)//this is a character name 349 | { 350 | if (cmdref == null || cmdref is not ExecutorCommand cmd || cmd.Expression == null || (!SecScenarioProgram.LegacyVersion && cmd.Expression.Count < 2)) 351 | { 352 | throw new Exception("Generate name need a correct command refrence"); 353 | } 354 | 355 | var name = dialogue.Character.Split(" -> "); 356 | if (nameMap != null) 357 | { 358 | if (cmd.Expression[0].Operations[0].Data is uint id 359 | && cmd.Expression[0].Operations[STR_LOAD_INDEX].Op == STR_LOAD_OP 360 | && cmd.Expression[0].Operations[STR_LOAD_INDEX].Data is EditableString data) 361 | { 362 | var character = data.Text; 363 | if (cmd.Expression[0].Operations.Count >= STR_LOAD_INDEX2) 364 | { 365 | if (Convert.ToByte(cmd.Expression[0].Operations[STR_LOAD_ID2_INDEX].Data) == 0x03 && cmd.Expression[0].Operations[STR_LOAD_INDEX2].Data is EditableString data2) 366 | { 367 | character += $" -> {data2.Text}"; 368 | } 369 | } 370 | if (nameMap.TryGetValue(character, out var newChar)) 371 | { 372 | name = newChar.Split(" -> "); 373 | } 374 | } 375 | } 376 | var charName = name[0]; 377 | var charOverrideName = name.Length == 2 ? name[1] : name[0]; 378 | var expr = SecScenarioProgram.LegacyVersion ? new List 379 | { 380 | new(0, 1, [ 381 | new ExpressionOperation(0x20, FLG_NAME), 382 | new ExpressionOperation(0x50, null), 383 | new ExpressionOperation(0x23, null), 384 | 385 | new ExpressionOperation(0x50, null), 386 | new ExpressionOperation(0x13, (byte)0x01), 387 | new ExpressionOperation(0x24, null), 388 | new ExpressionOperation(0x1E, new EditableString(charName, true)), 389 | 390 | new ExpressionOperation(0x42, null), 391 | new ExpressionOperation(0x50, null), 392 | new ExpressionOperation(0x13, (byte)0x00), 393 | new ExpressionOperation(0x24, null), 394 | new ExpressionOperation(0x13, (byte)0x01), 395 | new ExpressionOperation(0x3C, null), 396 | 397 | new ExpressionOperation(0x50, null), 398 | new ExpressionOperation(0x13, (byte)0x03), 399 | new ExpressionOperation(0x24, null), 400 | new ExpressionOperation(0x1E, new EditableString(charOverrideName, true)), 401 | 402 | new ExpressionOperation(0x42, null), 403 | new ExpressionOperation(0x50, null), 404 | new ExpressionOperation(0x13, (byte)0x02), 405 | new ExpressionOperation(0x24, null), 406 | new ExpressionOperation(0x13, (byte)0x01), 407 | new ExpressionOperation(0x3C, null), 408 | ]) 409 | }: 410 | new List 411 | { 412 | new(0, 1, [ 413 | new ExpressionOperation(0x78, FLG_NAME), 414 | 415 | new ExpressionOperation(0x79, (byte)0x01), 416 | new ExpressionOperation(0x7A, new EditableString(charName, true)), 417 | new ExpressionOperation(0x42, null), 418 | new ExpressionOperation(0x7B, (byte)0x00), 419 | 420 | new ExpressionOperation(0x79, (byte)0x03), 421 | new ExpressionOperation(0x7A, new EditableString(charOverrideName, true)), 422 | new ExpressionOperation(0x42, null), 423 | new ExpressionOperation(0x7B, (byte)0x02) 424 | ]), 425 | cmd.Expression[1] 426 | }; 427 | 428 | //Copy old clauses 429 | if (cmd.Expression[0].Operations[STR_LOAD_ID2_INDEX].Op == STR_LOAD_POS_OP1 && Convert.ToByte(cmd.Expression[0].Operations[STR_LOAD_ID2_INDEX].Data) == 0x03) 430 | { 431 | //origially has overrde name, skip 432 | expr[0].Operations.AddRange(cmd.Expression[0].Operations[NAME_EXPR_CP_POS2..]); 433 | } 434 | else 435 | { 436 | expr[0].Operations.AddRange(cmd.Expression[0].Operations[NAME_EXPR_CP_POS1..]); 437 | } 438 | 439 | var ret = new ExecutorCommand(0x1B, 0x12, expr, cmd.Offset.Old); 440 | return ret; 441 | } 442 | else if ((type & TYPE_TITLE) != 0)//this is a title string 443 | { 444 | if (cmdref == null || cmdref is not ExecutorCommand cmd || cmd.Expression == null || (!SecScenarioProgram.LegacyVersion && cmd.Expression.Count < 2)) 445 | { 446 | throw new Exception("Generate title need a correct command refrence"); 447 | } 448 | ExpressionOperation c1; 449 | ExpressionOperation c2; 450 | if ((type & TITLE_SCENE) != 0) 451 | { 452 | c1 = new ExpressionOperation(STR_LOAD_POS_OP1, (byte)0x01); 453 | c2 = new ExpressionOperation(STR_LOAD_POS_OP2, (byte)0x00); 454 | } 455 | else 456 | { 457 | c1 = new ExpressionOperation(STR_LOAD_POS_OP1, (byte)0x03); 458 | c2 = new ExpressionOperation(STR_LOAD_POS_OP2, (byte)0x02); 459 | } 460 | var expr = SecScenarioProgram.LegacyVersion ? new List 461 | { 462 | new(0, 1, [ 463 | new ExpressionOperation(0x20, FLG_TITLE), 464 | new ExpressionOperation(0x50, null), 465 | new ExpressionOperation(0x23, null), 466 | 467 | new ExpressionOperation(0x50, null), 468 | c1, 469 | new ExpressionOperation(0x24, null), 470 | 471 | new ExpressionOperation(0x1E, new EditableString(dialogue.Text, true)), 472 | new ExpressionOperation(0x42, null), 473 | 474 | new ExpressionOperation(0x50, null), 475 | c2, 476 | new ExpressionOperation(0x24, null), 477 | 478 | new ExpressionOperation(0x13, (byte)0x01), 479 | new ExpressionOperation(0x3C, null), 480 | new ExpressionOperation(0x52, null), 481 | new ExpressionOperation(0xFF, null) 482 | ]) 483 | }: 484 | new List 485 | { 486 | new(0, 1, [ 487 | new ExpressionOperation(0x78, FLG_TITLE), 488 | 489 | c1, 490 | new ExpressionOperation(0x7A, new EditableString(dialogue.Text, true)), 491 | new ExpressionOperation(0x42, null), 492 | c2, 493 | 494 | new ExpressionOperation(0x52, null), 495 | new ExpressionOperation(0xFF, null) 496 | ]), 497 | cmd.Expression[1] 498 | }; 499 | var ret = new ExecutorCommand(0x1B, 0x12, expr, cmd.Offset.Old); 500 | return ret; 501 | } 502 | else if ((type & TYPE_SELECT) != 0) 503 | { 504 | if (cmdref == null || cmdref is not ExecutorCommand cmd || cmd.Expression == null || (!SecScenarioProgram.LegacyVersion && cmd.Expression.Count < 2)) 505 | { 506 | throw new Exception("Generate selection need a correct command refrence"); 507 | } 508 | var expr = SecScenarioProgram.LegacyVersion ? new List 509 | { 510 | new(0, 1, [ 511 | new ExpressionOperation(0x20, FLG_SELECT), 512 | new ExpressionOperation(0x50, null), 513 | new ExpressionOperation(0x23, null), 514 | new ExpressionOperation(0x50, null), 515 | new ExpressionOperation(0x13, (byte)0x01), 516 | new ExpressionOperation(0x24, null), 517 | 518 | new ExpressionOperation(0x1E, new EditableString(dialogue.Text, true)), 519 | new ExpressionOperation(0x42, null), 520 | new ExpressionOperation(0x50, null), 521 | new ExpressionOperation(0x13, (byte)0x00), 522 | new ExpressionOperation(0x24, null), 523 | new ExpressionOperation(0x13, (byte)0x01), 524 | 525 | new ExpressionOperation(0x3C, null), 526 | new ExpressionOperation(0x52, null), 527 | new ExpressionOperation(0xFF, null) 528 | ]) 529 | } : 530 | new List 531 | { 532 | new(0, 1, [ 533 | new ExpressionOperation(0x78, FLG_SELECT), 534 | 535 | new ExpressionOperation(0x79, (byte)0x01), 536 | new ExpressionOperation(0x7A, new EditableString(dialogue.Text, true)), 537 | new ExpressionOperation(0x42, null), 538 | new ExpressionOperation(0x7B, (byte)0x00), 539 | 540 | new ExpressionOperation(0x52, null), 541 | new ExpressionOperation(0xFF, null) 542 | ]), 543 | cmd.Expression[1] 544 | }; 545 | var ret = new ExecutorCommand(0x1B, 0x12, expr, cmd.Offset.Old); 546 | return ret; 547 | } 548 | else 549 | { 550 | throw new Exception("Unknown dialogue type."); 551 | } 552 | } 553 | 554 | IEnumerable> GetTextBlockProp() 555 | { 556 | var code = current.Code; 557 | for (int i = 0; i < code.Count; i++) 558 | { 559 | if (code[i] is EditableString s || (code[i] is ExecutorCommand e && e.ExecutorIndex == 0x1F9)) 560 | { 561 | var start = i; 562 | while (code[i] is not ExecutorCommand cmd || cmd.ExecutorIndex != 0x03) 563 | { 564 | i++; 565 | } 566 | yield return new Tuple(TYPE_DIALOGUE, start, i); 567 | } 568 | else if (code[i] is ExecutorCommand cmd) 569 | { 570 | if (cmd.ExecutorIndex != 0x12) 571 | { 572 | continue; 573 | } 574 | if (cmd.Expression == null || (!SecScenarioProgram.LegacyVersion && cmd.Expression.Count < 2)) 575 | { 576 | continue; 577 | } 578 | var operations = cmd.Expression[0].Operations; 579 | if (operations[0].Op != STR_PUSH_OP || (SecScenarioProgram.LegacyVersion && operations.Count < STR_LOAD_INDEX)) 580 | { 581 | continue; 582 | } 583 | //This is kind of string related 584 | if (operations[0].Data is not uint id || operations[STR_LOAD_INDEX].Op != STR_LOAD_OP || operations[STR_LOAD_INDEX].Data is not EditableString data) 585 | { 586 | continue; 587 | } 588 | if (id == FLG_NAME) 589 | { 590 | yield return new Tuple(TYPE_CHARNAME, i, i + 1); 591 | } 592 | else if (id == FLG_TITLE) 593 | { 594 | if (operations[STR_LOAD_POS].Op == STR_LOAD_POS_OP1) 595 | { 596 | if (Convert.ToByte(operations[STR_LOAD_POS].Data) == 0x01) 597 | { 598 | yield return new Tuple(TYPE_TITLE | TITLE_SCENE, i, i + 1); 599 | } 600 | else 601 | { 602 | yield return new Tuple(TYPE_TITLE | TITLE_PLOT, i, i + 1); 603 | } 604 | } 605 | } 606 | else if (id == FLG_SELECT) 607 | { 608 | yield return new Tuple(TYPE_SELECT, i, i + 1); 609 | } 610 | } 611 | } 612 | } 613 | 614 | for (int i = 0; i < secCode.Count; i++) 615 | { 616 | if (secCode[i] is not NamedCode nc) 617 | { 618 | continue; 619 | } 620 | 621 | if (!dict.TryGetValue(Path.GetFileName(nc.Name), out var dialogues)) 622 | { 623 | continue; 624 | } 625 | 626 | current = nc; 627 | Console.WriteLine($"Importing {nc.Name}..."); 628 | 629 | try 630 | { 631 | var nnc = new NamedCode(nc.Name, []); 632 | int prevPos = 0; 633 | foreach (var t in GetTextBlockProp()) 634 | { 635 | if (dialogues.Count == 0) 636 | { 637 | throw new Exception("Dialogue count doesn't match"); 638 | } 639 | nnc.Code.AddRange(nc.Code[prevPos..t.Item2]); 640 | var generatedBlock = ConvertDialogue(t.Item1, dialogues[0], nc.Code[t.Item2]); 641 | if (generatedBlock is List l) 642 | { 643 | nnc.Code.AddRange(l); 644 | } 645 | else 646 | { 647 | nnc.Code.Add(generatedBlock); 648 | } 649 | prevPos = t.Item3; 650 | if ((t.Item1 & TYPE_CHARNAME) == 0) 651 | { 652 | dialogues.RemoveAt(0); 653 | } 654 | } 655 | nnc.Code.AddRange(current.Code[prevPos..]); 656 | secCode[i] = nnc; 657 | } 658 | catch (Exception ex) 659 | { 660 | Console.WriteLine($"An error occurred while trying to import {nc.Name}, this file will be skipped."); 661 | Console.WriteLine(ex.Message); 662 | Console.WriteLine(ex.StackTrace); 663 | } 664 | } 665 | } 666 | 667 | public static Script ImportText(string path) 668 | { 669 | bool haveFormatError = false; 670 | var ret = new Script(); 671 | foreach (var file in Directory.EnumerateFiles(path, "*.txt", SearchOption.AllDirectories)) 672 | { 673 | Console.WriteLine($"Reading {file}..."); 674 | var messages = new List(); 675 | foreach (var line in File.ReadAllLines(file)) 676 | { 677 | if (line.StartsWith('★')) 678 | { 679 | var text = line.Split('★'); 680 | if (text.Length < 4) 681 | { 682 | haveFormatError = true; 683 | Console.WriteLine($"Invalid format: {line}"); 684 | continue; 685 | } 686 | messages.Add(new Dialogue(text[2], text[3])); 687 | } 688 | } 689 | ret.Add(new Tuple>(Path.GetFileNameWithoutExtension(file), messages)); 690 | } 691 | 692 | if (haveFormatError) 693 | { 694 | throw new ArgumentException($"Format error detected. See messages above."); 695 | } 696 | return ret; 697 | } 698 | 699 | public static void ExportText(string outputPath, Script text, bool blankLine = false) 700 | { 701 | var total = 0; 702 | foreach (var dialogues in text) 703 | { 704 | var output = File.CreateText(Path.Combine(outputPath, $"{Path.GetFileName(dialogues.Item1)}.txt")); 705 | Console.Write($"Exporting {dialogues.Item1}..."); 706 | var idx = 0; 707 | var chars = 0; 708 | foreach (var dialogue in dialogues.Item2) 709 | { 710 | output.WriteLine($"☆{idx:X8}☆{dialogue.Character}☆{dialogue.Text}"); 711 | if (blankLine && dialogue.Text != "") 712 | { 713 | output.WriteLine($"★{idx:X8}★{dialogue.Character}★{(dialogue.Text[0] == '「' ? "「」" : dialogue.Text[0] == '『' ? "『』" : "")}\n"); 714 | } 715 | else 716 | { 717 | output.WriteLine($"★{idx:X8}★{dialogue.Character}★{dialogue.Text}\n"); 718 | } 719 | chars += dialogue.Text.Length; 720 | idx++; 721 | } 722 | Console.WriteLine($" ({chars} characters)"); 723 | total += chars; 724 | output.Flush(); 725 | output.Close(); 726 | } 727 | Console.WriteLine($"Total: {total} characters."); 728 | } 729 | 730 | public static void ExportStrings(List strs, string path) 731 | { 732 | using (var sw = new StreamWriter(File.OpenWrite(path))) 733 | { 734 | var idx = 0; 735 | foreach (var s in strs) 736 | { 737 | sw.WriteLine($"☆{idx:X8}☆{s}"); 738 | sw.WriteLine($"★{idx:X8}★{s}\n"); 739 | idx++; 740 | } 741 | } 742 | } 743 | public static List ImportStrings(string path) 744 | { 745 | List result = []; 746 | foreach (var line in File.ReadAllLines(path)) 747 | { 748 | if (line.StartsWith('★')) 749 | { 750 | var text = line.Split('★'); 751 | result.Add(text[2]); 752 | } 753 | } 754 | return result; 755 | } 756 | } 757 | 758 | } 759 | 760 | 761 | -------------------------------------------------------------------------------- /SecTool/SecTool.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | true 6 | net8.0 7 | enable 8 | enable 9 | $(SolutionDir)Build\bin 10 | $(SolutionDir)Build\obj\SecTool\ 11 | False 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## SecTool 2 | 3 | #### Disasembler & Assembler for SAS5's .sec5 file 4 | 5 | 6 | wip --------------------------------------------------------------------------------