├── appsettings.Development.json
├── appsettings.json
├── app.manifest
├── .gitignore
├── Properties
└── launchSettings.json
├── Config
└── userconfig.json
├── Models
└── ApiConfig.cs
├── Utils
├── DirUtils.cs
├── PathUtils.cs
└── TextureExtractor.cs
├── LICENSE
├── CodeWalker.API.sln
├── Controllers
├── HashController.cs
├── SearchController.cs
├── ReplaceController.cs
├── ConfigController.cs
├── DownloadController.cs
└── ImportController.cs
├── Services
├── ServiceFactory.cs
├── ServiceManager.cs
├── ConfigService.cs
├── RpfService.cs
└── ReloadableServiceContainer.cs
├── CodeWalker.API.csproj
├── .github
└── workflows
│ └── release.yml
├── README.md
├── Program.cs
└── CodeWalker.API.http
/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "Microsoft.AspNetCore": "Warning",
6 | "CodeWalker.API": "Debug"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "Microsoft.AspNetCore": "Warning",
6 | "CodeWalker.API": "Debug"
7 | }
8 | },
9 | "AllowedHosts": "localhost;127.0.0.1"
10 | }
11 |
--------------------------------------------------------------------------------
/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Build artifacts
2 | bin/
3 | obj/
4 |
5 | # User-specific files
6 | .vs/
7 | *.user
8 | *.suo
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # Rider-specific files
13 | .idea/
14 | *.sln.iml
15 |
16 | # Auto-generated packages
17 | *.nupkg
18 | packages/
19 | NuGet.Config
20 | NuGet.exe
21 | project.lock.json
22 | project.assets.json
23 |
24 | # Visual Studio Code
25 | .vscode/
26 |
27 | # Logs
28 | *.log
29 |
--------------------------------------------------------------------------------
/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "http": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": false,
8 | "applicationUrl": "http://localhost:5024",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Config/userconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "CodewalkerOutputDir": "C:\\GTA_FILES\\cw_out",
3 | "BlenderOutputDir": "C:\\GTA_FILES\\blender_out",
4 | "FivemOutputDir": "C:\\GTA_FILES\\fivem_out",
5 | "RpfArchivePath": "C:\\Program Files\\Rockstar Games\\Grand Theft Auto V\\modstore\\new.rpf",
6 | "GTAPath": "C:\\Program Files\\Rockstar Games\\Grand Theft Auto V",
7 | "Gen9": false,
8 | "DLC": "tfd",
9 | "EnableMods": true,
10 | "Port": 0
11 | }
--------------------------------------------------------------------------------
/Models/ApiConfig.cs:
--------------------------------------------------------------------------------
1 | namespace CodeWalker.API.Models
2 | {
3 | public class ApiConfig
4 | {
5 | public string CodewalkerOutputDir { get; set; } = "";
6 | public string BlenderOutputDir { get; set; } = "";
7 | public string FivemOutputDir { get; set; } = "";
8 | public string RpfArchivePath { get; set; } = "";
9 | public string GTAPath { get; set; } = "";
10 | public bool Gen9 { get; set; } = false;
11 | public string Dlc { get; set; } = "";
12 | public bool EnableMods { get; set; } = false;
13 | public int Port { get; set; } = 0;
14 | }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/Utils/DirUtils.cs:
--------------------------------------------------------------------------------
1 | namespace CodeWalker.API.Utils
2 | {
3 | public static class DirUtils
4 | {
5 | public static void CopyDirectory(string sourceDir, string targetDir)
6 | {
7 | Directory.CreateDirectory(targetDir);
8 | foreach (var file in Directory.GetFiles(sourceDir))
9 | System.IO.File.Copy(file, Path.Combine(targetDir, Path.GetFileName(file)), true);
10 |
11 | foreach (var dir in Directory.GetDirectories(sourceDir))
12 | CopyDirectory(dir, Path.Combine(targetDir, Path.GetFileName(dir)));
13 | }
14 |
15 | }
16 |
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/Utils/PathUtils.cs:
--------------------------------------------------------------------------------
1 | namespace CodeWalker.API.Utils
2 | {
3 | public static class PathUtils
4 | {
5 | public static string NormalizePath(string path, bool useBackslashes = false)
6 | {
7 | var parts = path
8 | .Replace('\\', '/')
9 | .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
10 |
11 | var joined = string.Join("/", parts);
12 | return useBackslashes ? joined.Replace('/', '\\') : joined;
13 | }
14 |
15 | public static bool PathEquals(string a, string b)
16 | {
17 | return NormalizePath(a).Equals(NormalizePath(b), StringComparison.OrdinalIgnoreCase);
18 | }
19 |
20 | }
21 |
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 tr1cks
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CodeWalker.API.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.13.35825.156 d17.13
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeWalker.API", "CodeWalker.API.csproj", "{158CA20B-F031-1235-E9A6-E20F9744F0CE}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {158CA20B-F031-1235-E9A6-E20F9744F0CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {158CA20B-F031-1235-E9A6-E20F9744F0CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {158CA20B-F031-1235-E9A6-E20F9744F0CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {158CA20B-F031-1235-E9A6-E20F9744F0CE}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {7C348D83-7338-4D82-A39E-078D061E1C78}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Controllers/HashController.cs:
--------------------------------------------------------------------------------
1 | using CodeWalker.GameFiles;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Swashbuckle.AspNetCore.Annotations;
4 |
5 | namespace CodeWalker.API.Controllers
6 | {
7 | [ApiController]
8 | [Route("api/hash")]
9 | public class HashController : ControllerBase
10 | {
11 | [HttpGet("jenkins")]
12 | [SwaggerOperation(
13 | Summary = "Generate Jenkins hash",
14 | Description = "Generates a Jenkins hash from the input string and returns its uint, int, and hex representations."
15 | )]
16 | [SwaggerResponse(200, "Jenkins hash generated", typeof(JenkHash))]
17 | [SwaggerResponse(400, "Missing or invalid input")]
18 | public IActionResult GetJenkinsHash(
19 | [FromQuery, SwaggerParameter("The string to hash", Required = true)] string input,
20 | [FromQuery, SwaggerParameter("Encoding to use: UTF8 (0) or ASCII (1)")] JenkHashInputEncoding encoding = JenkHashInputEncoding.UTF8
21 | )
22 | {
23 | if (string.IsNullOrWhiteSpace(input))
24 | return BadRequest("Missing input string.");
25 |
26 | var result = new JenkHash(input, encoding);
27 | return Ok(new
28 | {
29 | input = result.Text,
30 | encoding = result.Encoding.ToString(),
31 | hashUint = result.HashUint,
32 | hashInt = result.HashInt,
33 | hashHex = result.HashHex
34 | });
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Controllers/SearchController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Swashbuckle.AspNetCore.Annotations;
3 | using System.Collections.Generic;
4 |
5 | [ApiController]
6 | [Route("api")]
7 | public class SearchController : ControllerBase
8 | {
9 | private readonly RpfService _rpfService;
10 |
11 | public SearchController(RpfService rpfService)
12 | {
13 | _rpfService = rpfService;
14 | }
15 |
16 | [HttpGet("search-file")]
17 | [SwaggerOperation(
18 | Summary = "Searches for files in the RPF archives",
19 | Description = "Searches for a file by name in the RPF archive and returns the matching results."
20 | )]
21 | [SwaggerResponse(200, "Successful search, returns matching file results", typeof(List))]
22 | [SwaggerResponse(400, "Bad request due to missing filename")]
23 | [SwaggerResponse(404, "File not found")]
24 | [SwaggerResponse(503, "Service unavailable - GTA path not configured")]
25 | public IActionResult SearchFile(
26 | [FromQuery, SwaggerParameter("Filename to search in RPF archive, e.g., filename=prop_alien_egg_01.ydr", Required = true)] string filename)
27 |
28 | {
29 | if (string.IsNullOrWhiteSpace(filename))
30 | {
31 | return BadRequest("Filename is required.");
32 | }
33 |
34 | try
35 | {
36 | var results = _rpfService.SearchFile(filename);
37 | return results.Count > 0 ? Ok(results) : NotFound($"File '{filename}' not found.");
38 | }
39 | catch (InvalidOperationException ex)
40 | {
41 | return StatusCode(503, new {
42 | error = "Service unavailable",
43 | message = ex.Message,
44 | solution = "Use /api/set-config to configure a valid GTA path"
45 | });
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Services/ServiceFactory.cs:
--------------------------------------------------------------------------------
1 | using CodeWalker.GameFiles;
2 | using Microsoft.Extensions.Logging;
3 |
4 | namespace CodeWalker.API.Services
5 | {
6 | public class ServiceFactory
7 | {
8 | private readonly ILogger _logger;
9 | private readonly ConfigService _configService;
10 |
11 | public ServiceFactory(ILogger logger, ConfigService configService)
12 | {
13 | _logger = logger;
14 | _configService = configService;
15 | }
16 |
17 | public RpfService CreateRpfService(ILogger logger)
18 | {
19 | return new RpfService(logger, _configService);
20 | }
21 |
22 | public GameFileCache CreateGameFileCache()
23 | {
24 | var config = _configService.Get();
25 | string gtaPath = config.GTAPath;
26 |
27 | long cacheSize = 4L * 1024 * 1024 * 1024; // 4GB Cache size considering gta v enhanced
28 | double cacheTime = 60.0;
29 | bool isGen9 = config.Gen9;
30 | string dlc = config.Dlc;
31 | bool enableMods = config.EnableMods;
32 | string excludeFolders = "";
33 |
34 | var gameFileCache = new GameFileCache(cacheSize, cacheTime, gtaPath, isGen9, dlc, enableMods, excludeFolders);
35 | gameFileCache.EnableDlc = true; // this ensures Init() runs InitDlc()
36 | gameFileCache.Init(
37 | message => Console.WriteLine($"[GameFileCache] {message}"),
38 | error => Console.Error.WriteLine($"[GameFileCache ERROR] {error}")
39 | );
40 |
41 | _logger.LogInformation("[ServiceFactory] Created GameFileCache with GTA Path: {GtaPath}, Archetypes: {Count}",
42 | gtaPath, gameFileCache.YtypDict?.Count ?? 0);
43 |
44 | return gameFileCache;
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/CodeWalker.API.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | enable
5 | enable
6 | app.manifest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | PreserveNewest
32 | Always
33 |
34 |
35 | strings.txt
36 | PreserveNewest
37 | Always
38 |
39 |
40 | ShadersGen9Conversion.xml
41 | PreserveNewest
42 | Always
43 |
44 |
45 | Resources\magic.dat
46 | PreserveNewest
47 | Always
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Services/ServiceManager.cs:
--------------------------------------------------------------------------------
1 | using CodeWalker.GameFiles;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using Microsoft.Extensions.Logging;
4 | using System;
5 |
6 | namespace CodeWalker.API.Services
7 | {
8 | public class ServiceManager
9 | {
10 | private readonly IServiceProvider _serviceProvider;
11 | private readonly ILogger _logger;
12 | private readonly ConfigService _configService;
13 |
14 | public ServiceManager(IServiceProvider serviceProvider, ILogger logger, ConfigService configService)
15 | {
16 | _serviceProvider = serviceProvider;
17 | _logger = logger;
18 | _configService = configService;
19 |
20 | // Subscribe to GTA path changes
21 | _configService.GtaPathChanged += OnGtaPathChanged;
22 | }
23 |
24 | private void OnGtaPathChanged(string newGtaPath)
25 | {
26 | _logger.LogInformation("[ServiceManager] GTA path changed to: {NewPath}", newGtaPath);
27 | ReloadServices();
28 | }
29 |
30 | public void ReloadServices()
31 | {
32 | try
33 | {
34 | _logger.LogInformation("[ServiceManager] Starting service reload...");
35 |
36 | // Reload RPF decryption keys
37 | var config = _configService.Get();
38 | string gtaPath = config.GTAPath;
39 |
40 | if (!Directory.Exists(gtaPath))
41 | {
42 | _logger.LogError("[ServiceManager] GTA V directory not found at {GtaPath}", gtaPath);
43 | return;
44 | }
45 |
46 | // Load RPF decryption keys
47 | try
48 | {
49 | _logger.LogInformation("[ServiceManager] Loading RPF decryption keys...");
50 | GTA5Keys.LoadFromPath(gtaPath);
51 | _logger.LogInformation("[ServiceManager] RPF decryption keys loaded successfully.");
52 | }
53 | catch (Exception ex)
54 | {
55 | _logger.LogError(ex, "[ServiceManager] Failed to load RPF keys: {Message}", ex.Message);
56 | return;
57 | }
58 |
59 | // Note: For a complete reload, we would need to recreate the services
60 | // However, since they're registered as singletons, we need to use a different approach
61 | // This is a limitation of the current DI container setup
62 | _logger.LogInformation("[ServiceManager] Service reload completed. Note: Services will be recreated on next request.");
63 | }
64 | catch (Exception ex)
65 | {
66 | _logger.LogError(ex, "[ServiceManager] Error during service reload: {Message}", ex.Message);
67 | }
68 | }
69 |
70 | public void Dispose()
71 | {
72 | _configService.GtaPathChanged -= OnGtaPathChanged;
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Build and Release CodeWalker API
2 | on:
3 | push:
4 | tags:
5 | - "v*" # Runs when a version tag like v1.0.0 is pushed
6 |
7 | jobs:
8 | build:
9 | runs-on: windows-latest # Windows required for CodeWalker
10 |
11 | steps:
12 | - name: Checkout CodeWalker.API Repo
13 | uses: actions/checkout@v4
14 | with:
15 | path: CodeWalker.API # Clones into ${{ github.workspace }}/CodeWalker.API
16 |
17 | - name: Clone Full CodeWalker Repo (for CodeWalker.Core)
18 | run: |
19 | git clone --depth=1 https://github.com/dexyfex/CodeWalker.git "${{ github.workspace }}/CodeWalkerRepo"
20 |
21 | - name: Move CodeWalker.Core to Workdirectory
22 | run: |
23 | move "${{ github.workspace }}\CodeWalkerRepo\CodeWalker.Core" "${{ github.workspace }}"
24 |
25 | - name: Verify CodeWalker.Core Exists
26 | shell: pwsh
27 | run: |
28 | if (!(Test-Path "${{ github.workspace }}/CodeWalker.Core/CodeWalker.Core.csproj")) {
29 | Write-Host "❌ CodeWalker.Core is missing!"
30 | exit 1
31 | } else {
32 | Write-Host "✅ CodeWalker.Core is present."
33 | }
34 |
35 | - name: 🔧 Patch CodeWalker.Core to fix Assembly.GetExecutingAssembly().Location
36 | shell: pwsh
37 | run: |
38 | $file = "${{ github.workspace }}/CodeWalker.Core/GameFiles/RpfManager.cs"
39 | (Get-Content $file) -replace 'var path = System\.Reflection\.Assembly\.GetExecutingAssembly\(\)\.Location;', 'var path = AppContext.BaseDirectory;' | Set-Content $file
40 | Write-Host "✅ Patched RpfManager.cs to use AppContext.BaseDirectory"
41 |
42 | - name: Setup MSBuild
43 | uses: microsoft/setup-msbuild@v1
44 |
45 | - name: Restore NuGet Packages
46 | run: |
47 | nuget restore "${{ github.workspace }}/CodeWalker.Core/CodeWalker.Core.csproj"
48 | nuget restore "${{ github.workspace }}/CodeWalker.API/CodeWalker.API.csproj"
49 |
50 | - name: Publish CodeWalker.API (as admin-elevated .exe)
51 | run: |
52 | cd "${{ github.workspace }}/CodeWalker.API"
53 | dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true
54 |
55 | - name: Package published output as ZIP
56 | shell: cmd
57 | run: |
58 | @echo on
59 | set PUBLISH_DIR=${{ github.workspace }}\CodeWalker.API\bin\Release\net9.0\win-x64\publish
60 | set OUTPUT_ZIP=${{ github.workspace }}\CodeWalker.API.zip
61 | echo "Zipping full publish directory..."
62 | powershell Compress-Archive -Path "%PUBLISH_DIR%\*" -DestinationPath "%OUTPUT_ZIP%"
63 | if not exist "%OUTPUT_ZIP%" (
64 | echo "❌ ERROR: ZIP file was not created!"
65 | exit 1
66 | ) else (
67 | echo "✅ ZIP file created successfully."
68 | )
69 |
70 | - name: Debug - List Workspace Contents
71 | shell: cmd
72 | run: |
73 | @echo on
74 | dir /s "${{ github.workspace }}"
75 |
76 | - name: Upload Release
77 | uses: softprops/action-gh-release@v2
78 | with:
79 | files: "${{ github.workspace }}/CodeWalker.API.zip"
80 | env:
81 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
82 |
--------------------------------------------------------------------------------
/Controllers/ReplaceController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Swashbuckle.AspNetCore.Annotations;
3 | using Microsoft.Extensions.Logging;
4 | using System.IO;
5 | using CodeWalker.GameFiles;
6 | using CodeWalker.API.Services;
7 | using CodeWalker.API.Utils;
8 | using System;
9 | using System.Linq;
10 |
11 | [ApiController]
12 | [Route("api")]
13 | public class ReplaceController : ControllerBase
14 | {
15 | private readonly RpfService _rpfService;
16 | private readonly ConfigService _configService;
17 | private readonly ILogger _logger;
18 |
19 | public ReplaceController(RpfService rpfService, ConfigService configService, ILogger logger)
20 | {
21 | _rpfService = rpfService;
22 | _configService = configService;
23 | _logger = logger;
24 | }
25 |
26 | public class ReplaceFileRequest
27 | {
28 | public string? RpfFilePath { get; set; }
29 | public string? LocalFilePath { get; set; }
30 | }
31 |
32 | [HttpPost("replace-file")]
33 | [Consumes("application/json")]
34 | [SwaggerOperation(Summary = "Replaces a file in an RPF (JSON)", Description = "Replaces the file inside an RPF using a JSON body.")]
35 | [SwaggerResponse(200, "File replaced successfully")]
36 | [SwaggerResponse(400, "Bad request")]
37 | [SwaggerResponse(503, "Service unavailable - GTA path not configured")]
38 | public IActionResult ReplaceFileJson([FromBody] ReplaceFileRequest jsonBody)
39 | {
40 | return HandleReplacement(jsonBody?.RpfFilePath, jsonBody?.LocalFilePath);
41 | }
42 |
43 | [HttpPost("replace-file")]
44 | [Consumes("application/x-www-form-urlencoded")]
45 | [ApiExplorerSettings(IgnoreApi = true)]
46 | public IActionResult ReplaceFileForm(
47 | [FromForm] string? rpfFilePath,
48 | [FromForm] string? localFilePath)
49 | {
50 | return HandleReplacement(rpfFilePath, localFilePath);
51 | }
52 |
53 | private IActionResult HandleReplacement(string? rpfFilePath, string? localFilePath)
54 | {
55 | if (string.IsNullOrWhiteSpace(localFilePath) || !System.IO.File.Exists(localFilePath))
56 | return BadRequest("Invalid or missing localFilePath.");
57 |
58 | if (string.IsNullOrWhiteSpace(rpfFilePath))
59 | return BadRequest("Invalid or missing rpfFilePath.");
60 |
61 | try
62 | {
63 | if (!_rpfService.TryResolveEntryPath(rpfFilePath, out var rpfFile, out var targetDir, out var fileName, out var resolveError))
64 | {
65 | _logger.LogWarning("Failed to resolve RPF path: {Error}", resolveError);
66 | return BadRequest($"Failed to resolve RPF path: {resolveError}");
67 | }
68 |
69 | if (targetDir == null)
70 | {
71 | _logger.LogWarning("Target directory is null.");
72 | return BadRequest("Target directory is null.");
73 | }
74 |
75 | if (string.IsNullOrWhiteSpace(fileName))
76 | {
77 | _logger.LogWarning("File name is null or empty.");
78 | return BadRequest("File name is null or empty.");
79 | }
80 |
81 | var localFileBytes = System.IO.File.ReadAllBytes(localFilePath);
82 | var newEntry = RpfFile.CreateFile(targetDir, fileName, localFileBytes);
83 |
84 | _logger.LogInformation("File replaced successfully: {Path}", newEntry.Path);
85 | return Ok(new { message = "File replaced successfully.", path = newEntry.Path });
86 | }
87 | catch (InvalidOperationException ex)
88 | {
89 | return StatusCode(503, new {
90 | error = "Service unavailable",
91 | message = ex.Message,
92 | solution = "Use /api/set-config to configure a valid GTA path"
93 | });
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Services/ConfigService.cs:
--------------------------------------------------------------------------------
1 | using CodeWalker.API.Models;
2 | using System.Text.Json;
3 |
4 | namespace CodeWalker.API.Services
5 | {
6 | public class ConfigService
7 | {
8 | private readonly string ConfigFilePath;
9 | private ApiConfig _config = new();
10 | private string _lastGtaPath = "";
11 |
12 | // Event to notify when GTA path changes
13 | public event Action? GtaPathChanged;
14 |
15 | public ConfigService()
16 | {
17 | ConfigFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config", "userconfig.json");
18 | Load(); // Load from disk on startup
19 | _lastGtaPath = _config.GTAPath; // Store initial GTA path
20 | }
21 |
22 | public ApiConfig Get() => _config;
23 |
24 | public void Set(ApiConfig config)
25 | {
26 | var oldGtaPath = _config.GTAPath;
27 | _config = config;
28 | Save();
29 |
30 | // Check if GTA path has changed
31 | if (!string.Equals(oldGtaPath, config.GTAPath, StringComparison.OrdinalIgnoreCase))
32 | {
33 | _lastGtaPath = config.GTAPath;
34 | GtaPathChanged?.Invoke(config.GTAPath);
35 | }
36 | }
37 |
38 | public bool HasGtaPathChanged()
39 | {
40 | return !string.Equals(_lastGtaPath, _config.GTAPath, StringComparison.OrdinalIgnoreCase);
41 | }
42 |
43 | public string GetCurrentGtaPath() => _config.GTAPath;
44 |
45 | private void Save()
46 | {
47 | try
48 | {
49 | var dir = Path.GetDirectoryName(ConfigFilePath);
50 | if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
51 | Directory.CreateDirectory(dir);
52 |
53 | var json = JsonSerializer.Serialize(_config, new JsonSerializerOptions { WriteIndented = true });
54 | File.WriteAllText(ConfigFilePath, json);
55 | Console.WriteLine($"[CONFIG] ✅ Saved to {ConfigFilePath}");
56 | }
57 | catch (Exception ex)
58 | {
59 | Console.WriteLine($"[CONFIG] ❌ Failed to save config: {ex.Message}");
60 | }
61 | }
62 |
63 | private void Load()
64 | {
65 | try
66 | {
67 | Console.WriteLine($"[CONFIG] Looking for config at: {Path.GetFullPath(ConfigFilePath)}");
68 |
69 | if (!File.Exists(ConfigFilePath))
70 | {
71 | Console.WriteLine($"[CONFIG] ❌ File missing: {ConfigFilePath}");
72 | return;
73 | }
74 |
75 | var json = File.ReadAllText(ConfigFilePath);
76 | Console.WriteLine($"[CONFIG] Raw JSON: {json}");
77 |
78 | var loadedConfig = JsonSerializer.Deserialize(json);
79 | if (loadedConfig == null)
80 | {
81 | Console.WriteLine($"[CONFIG] ❌ Failed to parse config.");
82 | return;
83 | }
84 |
85 | _config = loadedConfig;
86 | Console.WriteLine($"[CONFIG] ✅ Loaded config from {ConfigFilePath}");
87 | Console.WriteLine($"[CONFIG] GTAPath = {_config.GTAPath}");
88 |
89 | if (string.IsNullOrWhiteSpace(_config.GTAPath))
90 | {
91 | Console.Error.WriteLine($"[CONFIG] ❌ GTAPath is null or empty. Cannot continue.");
92 | Environment.Exit(1); // Hard stop
93 | }
94 | }
95 | catch (Exception ex)
96 | {
97 | Console.WriteLine($"[CONFIG] ❌ Failed to load config: {ex.Message}");
98 | }
99 | }
100 |
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CodeWalker API
2 |
3 | CodeWalker API is a .NET 9 web API that allows extracting, converting, and interacting with GTA V assets using CodeWalker's libraries.
4 |
5 | ## 📥 Download & Install
6 |
7 | You can download the latest pre-built release from the [Releases](https://github.com/flobros/CodeWalker.API/releases) page. Extract and run the API without needing to build it manually.
8 |
9 | ## 🔧 Configuration
10 |
11 | The API now reads its configuration from a `Config/userconfig.json` file located in the installation directory. You can modify this file to customize paths and ports used by the API.
12 |
13 | ### Example `Config/userconfig.json`:
14 |
15 | ```json
16 | {
17 | "CodewalkerOutputDir": "C:\\GTA_FILES\\cw_out",
18 | "BlenderOutputDir": "C:\\GTA_FILES\\blender_out",
19 | "FivemOutputDir": "C:\\GTA_FILES\\fivem_out",
20 | "RpfArchivePath": "C:\\Program Files\\Rockstar Games\\Grand Theft Auto V\\modstore\\new.rpf",
21 | "GTAPath": "C:\\Program Files\\Rockstar Games\\Grand Theft Auto V",
22 | "Port": 5555
23 | }
24 | ```
25 |
26 | ## 🔄 Service Reloading
27 |
28 | The API now supports automatic and manual service reloading when the GTA path changes:
29 |
30 | ### Automatic Reload
31 | When you update the `GTAPath` configuration via the API, the system automatically:
32 | - Detects the GTA path change
33 | - Reloads RPF decryption keys from the new path
34 | - **Immediately recreates all services** (RpfService, GameFileCache)
35 | - **Preheats the new services** (loads RPF entries and caches)
36 | - Preloads known meta types for optimal performance
37 |
38 | ### Manual Reload
39 | You can manually trigger a service reload using the `/api/reload-services` endpoint:
40 |
41 | ```bash
42 | POST http://localhost:5555/api/reload-services
43 | ```
44 |
45 | This is useful when you want to ensure all services are using the latest configuration immediately.
46 |
47 | ### Service Status
48 | Check the current service status using the `/api/service-status` endpoint:
49 |
50 | ```bash
51 | GET http://localhost:5555/api/service-status
52 | ```
53 |
54 | This returns the current GTA path, reload version, and service readiness status.
55 |
56 | ### Graceful Startup with Invalid GTA Path
57 | The API now starts gracefully even when the initial GTA path is invalid:
58 |
59 | - **No Hard Exit**: The API will start and run even with an invalid GTA path
60 | - **Helpful Messages**: Clear error messages guide users to fix the configuration
61 | - **Service Status**: Use `/api/service-status` to check if services are ready
62 | - **Easy Fix**: Use `/api/set-config` to set a valid GTA path and automatically reload services
63 |
64 | When the GTA path is invalid, API endpoints will return a `503 Service Unavailable` status with helpful error messages and instructions on how to fix the issue.
65 |
66 | ## 🚀 Running the API
67 |
68 | To start the API, simply execute:
69 |
70 | ```sh
71 | CodeWalker.API.exe
72 | ```
73 |
74 | The API will run on the configured port (default: `5555`).
75 |
76 | ## 🛠 Features
77 |
78 | - Extract and convert GTA V files (YDR, YTD, etc.)
79 | - Export models to XML
80 | - Extract textures
81 | - Search for files within RPF archives
82 | - Import XML or raw files back into RPF
83 | - Replace existing files in RPFs
84 | - **Automatic service reloading when GTA path changes**
85 | - **Manual service reloading via API endpoint**
86 |
87 | ## 🌐 Swagger UI (Recommended for Testing)
88 |
89 | Open the Swagger UI in your browser to test endpoints interactively:
90 |
91 | ```
92 | http://localhost:5555
93 | ```
94 |
95 | ## 📝 Example Requests
96 |
97 | (See detailed examples for downloading, importing, replacing, and searching files in the original request)
98 |
99 | ## 📜 License
100 |
101 | This project is released under the MIT License.
102 |
103 | ## 🤝 Contributing
104 |
105 | Pull requests are welcome! If you encounter issues, feel free to open an [issue](https://github.com/flobros/CodeWalker.API/issues).
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using CodeWalker.GameFiles;
4 | using CodeWalker.API.Services;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Hosting;
9 | using Microsoft.Extensions.Logging;
10 | using Microsoft.OpenApi.Models;
11 | using CodeWalker.API.Models;
12 |
13 | // ✅ Create the builder
14 | var builder = WebApplication.CreateBuilder(args);
15 |
16 | // ✅ (No need to load appsettings.json manually!)
17 |
18 | // ✅ Register ConfigService
19 | builder.Services.AddSingleton();
20 |
21 | // ✅ Register ServiceFactory
22 | builder.Services.AddSingleton();
23 |
24 | // ✅ Register ReloadableServiceContainer
25 | builder.Services.AddSingleton();
26 |
27 | // ✅ Logging setup
28 | builder.Logging.ClearProviders();
29 | builder.Logging.AddConsole();
30 | builder.Logging.AddDebug();
31 |
32 | // ✅ Register services
33 | builder.Services.AddEndpointsApiExplorer();
34 | builder.Services.AddSwaggerGen(c =>
35 | {
36 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "CodeWalker API", Version = "v1" });
37 | c.EnableAnnotations();
38 | });
39 | builder.Services.AddControllers()
40 | .AddJsonOptions(options =>
41 | {
42 | options.JsonSerializerOptions.PropertyNamingPolicy = null; // Use PascalCase
43 | });
44 |
45 | // ✅ Register RpfService as transient (will be managed by ReloadableServiceContainer)
46 | builder.Services.AddTransient(serviceProvider =>
47 | {
48 | var container = serviceProvider.GetRequiredService();
49 | var logger = serviceProvider.GetRequiredService>();
50 | return container.GetRpfService(logger);
51 | });
52 |
53 | // ✅ Register GameFileCache as transient (will be managed by ReloadableServiceContainer)
54 | builder.Services.AddTransient(serviceProvider =>
55 | {
56 | var container = serviceProvider.GetRequiredService();
57 | return container.GetGameFileCache();
58 | });
59 |
60 | // ✅ Build the app
61 | var app = builder.Build();
62 |
63 | // ✅ Get config after app built
64 | var configService = app.Services.GetRequiredService();
65 | var config = configService.Get();
66 | string gtaPath = config.GTAPath;
67 | int port = config.Port;
68 |
69 | if (port == 0)
70 | {
71 | Console.WriteLine("[WARN] Port is set to 0. Using default port 5555...");
72 | port = 5555;
73 | }
74 |
75 | // ✅ Check GTA V directory but don't exit if invalid
76 | bool gtaPathValid = false;
77 | if (!string.IsNullOrWhiteSpace(gtaPath))
78 | {
79 | if (Directory.Exists(gtaPath))
80 | {
81 | gtaPathValid = true;
82 | Console.WriteLine($"[INFO] GTA V directory found at {gtaPath}");
83 | }
84 | else
85 | {
86 | Console.WriteLine($"[WARN] GTA V directory not found at {gtaPath}");
87 | Console.WriteLine("[INFO] API will start but services will not be initialized until a valid GTA path is configured.");
88 | }
89 | }
90 | else
91 | {
92 | Console.WriteLine("[WARN] GTA path is not configured.");
93 | Console.WriteLine("[INFO] API will start but services will not be initialized until a valid GTA path is configured.");
94 | }
95 |
96 | // ✅ Try to load RPF decryption keys if GTA path is valid
97 | if (gtaPathValid)
98 | {
99 | try
100 | {
101 | Console.WriteLine("[INFO] Loading RPF decryption keys...");
102 | GTA5Keys.LoadFromPath(gtaPath);
103 | Console.WriteLine("[INFO] RPF decryption keys loaded successfully.");
104 | }
105 | catch (Exception ex)
106 | {
107 | Console.WriteLine($"[WARN] Failed to load RPF keys: {ex.Message}");
108 | Console.WriteLine("[INFO] API will start but services will not be initialized until a valid GTA path is configured.");
109 | gtaPathValid = false;
110 | }
111 | }
112 |
113 | // ✅ Bind the server to the configured port
114 | app.Urls.Add($"http://0.0.0.0:{port}");
115 |
116 | // ✅ Logging API startup
117 | var logger = app.Services.GetRequiredService>();
118 | logger.LogInformation($"API is starting on port {port}...");
119 |
120 | if (!gtaPathValid)
121 | {
122 | logger.LogWarning("API is starting with invalid GTA path. Use /api/set-config to configure a valid GTA path.");
123 | Console.WriteLine("[INFO] API is ready. Use the config endpoints to set a valid GTA path.");
124 | }
125 |
126 | // ✅ Swagger setup
127 | app.UseSwagger();
128 | app.UseSwaggerUI(c =>
129 | {
130 | c.SwaggerEndpoint("/swagger/v1/swagger.json", "CodeWalker API v1");
131 | c.RoutePrefix = ""; // Swagger available at root
132 | });
133 | app.UseRouting();
134 | app.UseAuthorization();
135 | app.MapControllers();
136 |
137 | // ✅ Only preheat if GTA path is valid
138 | if (gtaPathValid)
139 | {
140 | try
141 | {
142 | var rpfService = app.Services.GetRequiredService();
143 | int count = rpfService.Preheat();
144 |
145 | var gameFileCache = app.Services.GetRequiredService();
146 | Console.WriteLine("[Startup] Preloading cache with known meta types...");
147 | // Preload by hash
148 | uint hash = JenkHash.GenHash("prop_alien_egg_01");
149 | var ydr = gameFileCache.GetYdr(hash);
150 | if (ydr != null)
151 | Console.WriteLine("[Startup] YDR preloaded successfully.");
152 | else
153 | Console.WriteLine("[Startup] YDR not found in archive.");
154 |
155 | Console.WriteLine("[Startup] Archetype dict contains: " + gameFileCache.GetArchetype(hash)?.Name);
156 |
157 | logger.LogInformation($"[Startup] Preheated RPF with {count} entries.");
158 | }
159 | catch (Exception ex)
160 | {
161 | Console.WriteLine($"[Startup ERROR] Cache preloading failed: {ex.Message}");
162 | logger.LogError(ex, "[Startup] Failed to preheat services");
163 | }
164 | }
165 | else
166 | {
167 | Console.WriteLine("[INFO] Skipping service preheating due to invalid GTA path.");
168 | Console.WriteLine("[INFO] Services will be initialized when a valid GTA path is configured.");
169 | }
170 |
171 | // ✅ Run the app
172 | app.Run();
173 |
--------------------------------------------------------------------------------
/CodeWalker.API.http:
--------------------------------------------------------------------------------
1 | @CodeWalkerAPI_HostAddress = http://localhost:5555
2 |
3 | ### ✅ With XML Conversion (Downloads files and converts to XML)
4 | # This request downloads the specified YDR files and converts them to XML before saving.
5 | GET {{CodeWalkerAPI_HostAddress}}/api/download-files?fullPaths=x64c.rpf\levels\gta5\props\lev_des\lev_des.rpf\prop_alien_egg_01.ydr&fullPaths=modstore\new.rpf\prop_alien_egg_01.ydr&xml=true&outputFolderPath=C:\GTA_YDR_FILES\out
6 | Accept: application/json
7 |
8 | ### ✅ Without XML Conversion (Downloads files as they are)
9 | # This request downloads the specified YDR files without converting them to XML.
10 | GET {{CodeWalkerAPI_HostAddress}}/api/download-files?fullPaths=x64c.rpf\levels\gta5\props\lev_des\lev_des.rpf\prop_alien_egg_01.ydr&fullPaths=modstore\new.rpf\prop_alien_egg_01.ydr&xml=false&outputFolderPath=C:\GTA_FILES\fivem_out
11 | Accept: application/json
12 |
13 | ### ✅ Form Import with outputFolder, with xml conversion
14 | POST {{CodeWalkerAPI_HostAddress}}/api/import
15 | Content-Type: application/x-www-form-urlencoded
16 |
17 | xml=true&filePaths=C:\GTA_FILES\cw_out\prop_alien_egg_01.ydr.xml&filePaths=C:\GTA_FILES\cw_out\ap1_02_planes003.ydr.xml&rpfArchivePath=C:\Program Files\Rockstar Games\Grand Theft Auto V\modstore\new.rpf\some&outputFolder=C:\GTA_FILES\fivem_out
18 |
19 | ### ✅ JSON Import with outputFolder, with XML conversion
20 | POST {{CodeWalkerAPI_HostAddress}}/api/import
21 | Content-Type: application/json
22 |
23 | {
24 | "xml": true,
25 | "filePaths": [
26 | "C:\\GTA_FILES\\cw_out\\prop_alien_egg_01.ydr.xml",
27 | "C:\\GTA_FILES\\cw_out\\ap1_02_planes003.ydr.xml"
28 | ],
29 | "rpfArchivePath": "C:\\Program Files\\Rockstar Games\\Grand Theft Auto V\\modstore\\new.rpf\\some",
30 | "outputFolder": "C:\\GTA_FILES\\fivem_out"
31 | }
32 |
33 | ### ✅ Form Import with outputFolder, no xml conversion
34 | POST {{CodeWalkerAPI_HostAddress}}/api/import
35 | Content-Type: application/x-www-form-urlencoded
36 |
37 | xml=false&filePaths=C:\GTA_FILES\fivem_out\prop_alien_egg_01.ydr&filePaths=C:\GTA_FILES\fivem_out\cs4_02_airplanes.ydr&rpfArchivePath=C:\Program Files\Rockstar Games\Grand Theft Auto V\modstore\new.rpf&outputFolder=C:\GTA_FILES\fivem_out
38 |
39 | ### ✅ JSON Import with outputFolder, no xml conversion
40 | POST {{CodeWalkerAPI_HostAddress}}/api/import
41 | Content-Type: application/json
42 |
43 | {
44 | "xml": false,
45 | "filePaths": [
46 | "C:\\GTA_FILES\\fivem_out\\prop_alien_egg_01.ydr",
47 | "C:\\GTA_FILES\\fivem_out\\cs4_02_airplanes.ydr"
48 | ],
49 | "rpfArchivePath": "C:\\Program Files\\Rockstar Games\\Grand Theft Auto V\\modstore\\new.rpf",
50 | "outputFolder": "C:\\GTA_FILES\\fivem_out"
51 | }
52 |
53 |
54 | ### ✅ Search for Files in RPF Archives
55 | # This request searches for files in the RPF archive that match the given query.
56 | GET {{CodeWalkerAPI_HostAddress}}/api/search-file?filename=prop_alien_egg_01
57 | Accept: application/json
58 |
59 | ### ✅ Form replace File in subfolder of RPF
60 | POST {{CodeWalkerAPI_HostAddress}}/api/replace-file
61 | Content-Type: application/x-www-form-urlencoded
62 |
63 | rpfFilePath=modstore/new.rpf/some/some.rpf/some&localFilePath=C:\GTA_FILES\fivem_out\prop_alien_egg_01.ydr
64 |
65 | ### ✅ JSON replace File in subfolder of RPF
66 | POST {{CodeWalkerAPI_HostAddress}}/api/replace-file
67 | Content-Type: application/json
68 |
69 | {
70 | "rpfFilePath": "modstore/new.rpf/some/some.rpf/some",
71 | "localFilePath": "C:\\GTA_FILES\\fivem_out\\prop_alien_egg_01.ydr"
72 | }
73 |
74 | ### ✅ Form Set Config
75 | POST {{CodeWalkerAPI_HostAddress}}/api/set-config
76 | Content-Type: application/x-www-form-urlencoded
77 |
78 | CodewalkerOutputDir=C:\GTA_FILES\cw_out&BlenderOutputDir=C:\GTA_FILES\blender_out&FivemOutputDir=C:\FXServer\txData\QboxProject_C81BA2.base\resources\[standalone]\test_map\stream\&RpfArchivePath=C:\Program Files\Rockstar Games\Grand Theft Auto V\modstore\new.rpf>APath=C:\Program Files\Rockstar Games\Grand Theft Auto V&Port=5555
79 |
80 |
81 | ### ✅ JSON Set Config
82 | POST {{CodeWalkerAPI_HostAddress}}/api/set-config
83 | Content-Type: application/json
84 |
85 | {
86 | "CodewalkerOutputDir": "C:\\GTA_FILES\\cw_out",
87 | "BlenderOutputDir": "C:\\GTA_FILES\\blender_out",
88 | "FivemOutputDir": "C:\\GTA_FILES\\fivem_out",
89 | "RpfArchivePath": "C:\\Program Files\\Rockstar Games\\Grand Theft Auto V\\modstore\\new.rpf",
90 | "GTAPath": "C:\\Program Files\\Rockstar Games\\Grand Theft Auto V",
91 | "Port": 5555
92 | }
93 |
94 | ### ✅ Reload Services (Manual Trigger)
95 | # This request manually triggers a reload of all services (RpfService, GameFileCache, etc.)
96 | # This is useful when you've changed the GTA path and want to ensure all services are using the new path.
97 | POST {{CodeWalkerAPI_HostAddress}}/api/reload-services
98 | Accept: application/json
99 |
100 | ### ✅ Get Service Status
101 | # This request retrieves the current service status including reload version and GTA path.
102 | GET {{CodeWalkerAPI_HostAddress}}/api/service-status
103 | Accept: application/json
104 |
105 | ### ✅ Example: Service Status with Invalid GTA Path
106 | # When the GTA path is invalid, you'll get a response like:
107 | # {
108 | # "gtaPath": "C:\\Invalid\\Path",
109 | # "servicesReady": false,
110 | # "statusMessage": "GTA V directory not found at C:\\Invalid\\Path",
111 | # "reloadVersion": 0,
112 | # "timestamp": "2024-01-01T12:00:00.000Z"
113 | # }
114 |
115 | ### ✅ Example: Service Status with Valid GTA Path
116 | # When the GTA path is valid, you'll get a response like:
117 | # {
118 | # "gtaPath": "C:\\Program Files\\Rockstar Games\\Grand Theft Auto V",
119 | # "servicesReady": true,
120 | # "statusMessage": "Services are ready",
121 | # "reloadVersion": 1,
122 | # "timestamp": "2024-01-01T12:00:00.000Z"
123 | # }
124 |
125 | ### ✅ Example: Error Response When GTA Path is Invalid
126 | # When you try to use an endpoint with an invalid GTA path, you'll get:
127 | # {
128 | # "error": "Service unavailable",
129 | # "message": "GTA V directory not found at C:\\Invalid\\Path. Please use /api/set-config to set a valid GTA path.",
130 | # "solution": "Use /api/set-config to configure a valid GTA path"
131 | # }
132 |
133 | ### ✅ Jenkins Hash Generation (UTF8 default)
134 | GET {{CodeWalkerAPI_HostAddress}}/api/hash/jenkins?input=prop_alien_egg_01
135 | Accept: application/json
136 |
137 | ### ✅ Jenkins Hash Generation (ASCII encoding)
138 | GET {{CodeWalkerAPI_HostAddress}}/api/hash/jenkins?input=prop_alien_egg_01&encoding=1
139 | Accept: application/json
140 |
141 |
--------------------------------------------------------------------------------
/Services/RpfService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using CodeWalker.API.Services;
6 | using CodeWalker.API.Utils;
7 | using CodeWalker.GameFiles;
8 | using Microsoft.Extensions.Logging;
9 |
10 | public class RpfService
11 | {
12 | private RpfManager _rpfManager;
13 | private readonly ILogger _logger;
14 | private readonly ConfigService _configService;
15 |
16 | public RpfService(ILogger logger, ConfigService configService)
17 | {
18 | _logger = logger;
19 | _configService = configService;
20 | InitializeRpfManager();
21 | }
22 |
23 | private void InitializeRpfManager()
24 | {
25 | var cfg = _configService.Get();
26 | string gtaPath = cfg.GTAPath;
27 | bool isGen9 = cfg.Gen9;
28 |
29 | _rpfManager = new RpfManager();
30 | _rpfManager.Init(gtaPath, isGen9, Console.WriteLine, Console.Error.WriteLine);
31 |
32 | _logger.LogInformation("[RpfService] Initialized with Gen9 = {Gen9}, GTA Path = {GtaPath}", isGen9, gtaPath);
33 | }
34 |
35 | public void Reload()
36 | {
37 | _logger.LogInformation("[RpfService] Reloading with new GTA path...");
38 | InitializeRpfManager();
39 | _logger.LogInformation("[RpfService] Reload completed. Entry count: {Count}", _rpfManager.EntryDict.Count);
40 | }
41 |
42 | private static string NormalizePath(string path)
43 | {
44 | return path.Replace('\\', '/').Trim();
45 | }
46 |
47 | public List SearchFile(string filename)
48 | {
49 | var results = new List();
50 | foreach (var entry in _rpfManager.EntryDict.Values)
51 | {
52 | if (entry.Name.Contains(filename, StringComparison.OrdinalIgnoreCase))
53 | {
54 | results.Add(entry.Path);
55 | }
56 | }
57 | return results;
58 | }
59 |
60 | public byte[] ExtractFile(string filename)
61 | {
62 | var entry = _rpfManager.GetEntry(filename);
63 | if (entry is RpfFileEntry fileEntry)
64 | {
65 | _logger.LogDebug("Extracting {FilePath}...", fileEntry.Path);
66 | return fileEntry.File.ExtractFile(fileEntry);
67 | }
68 | throw new FileNotFoundException($"File '{filename}' not found.");
69 | }
70 |
71 | public (byte[] fileBytes, RpfFileEntry entry)? ExtractFileWithEntry(string fullRpfPath)
72 | {
73 | var entry = _rpfManager.GetEntry(fullRpfPath);
74 | if (entry is RpfFileEntry fileEntry)
75 | {
76 | _logger.LogDebug("[MATCH] Found: {FilePath}", fileEntry.Path);
77 | return (fileEntry.File.ExtractFile(fileEntry), fileEntry);
78 | }
79 | _logger.LogWarning("[DEBUG] File not found in RPF: {FullPath}", fullRpfPath);
80 | return null;
81 | }
82 |
83 | public RpfEntry? GetEntry(string path)
84 | {
85 | return _rpfManager.GetEntry(path);
86 | }
87 |
88 | public RpfFile? GetParentRpfFile(RpfEntry entry)
89 | {
90 | return entry?.File;
91 | }
92 |
93 | public RpfDirectoryEntry EnsureDirectoryPath(RpfDirectoryEntry root, string internalPath)
94 | {
95 | var parts = internalPath.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
96 | RpfDirectoryEntry current = root;
97 | foreach (var part in parts)
98 | {
99 | var existing = current.Directories.FirstOrDefault(d => d.Name.Equals(part, StringComparison.OrdinalIgnoreCase));
100 | if (existing != null)
101 | {
102 | current = existing;
103 | continue;
104 | }
105 | _logger.LogInformation("[CREATE] Creating directory '{Dir}' in '{Parent}'", part, current.Path);
106 | var newDir = RpfFile.CreateDirectory(current, part);
107 | current = newDir;
108 | }
109 | return current;
110 | }
111 |
112 | private string ToVirtualPath(string fullPath)
113 | {
114 | var gtaPath = PathUtils.NormalizePath(_configService.Get().GTAPath); // `/`
115 | var normalized = PathUtils.NormalizePath(fullPath); // `/`
116 |
117 | if (normalized.StartsWith(gtaPath, StringComparison.OrdinalIgnoreCase))
118 | normalized = normalized.Substring(gtaPath.Length).TrimStart('/');
119 |
120 | return PathUtils.NormalizePath(normalized, useBackslashes: true); // final lookup version
121 | }
122 |
123 |
124 | public bool TryResolveEntryPath(string fullPath, out RpfFile? rpfFile, out RpfDirectoryEntry? targetDir, out string? fileName, out string? error)
125 | {
126 | rpfFile = null;
127 | targetDir = null;
128 | fileName = null;
129 | error = null;
130 |
131 | try
132 | {
133 | var normalized = NormalizePath(fullPath);
134 | _logger.LogDebug("[TryResolve] Normalized path: {Path}", normalized);
135 |
136 | var virtualPath = ToVirtualPath(fullPath); // now backslash-correct
137 |
138 | // Case 1: It’s a folder
139 | var entry = _rpfManager.GetEntry(virtualPath);
140 | if (entry is RpfDirectoryEntry dirEntry)
141 | {
142 | rpfFile = dirEntry.File;
143 | targetDir = dirEntry;
144 | return true;
145 | }
146 |
147 | // Case 2: It’s an RPF file
148 | if (virtualPath.EndsWith(".rpf", StringComparison.OrdinalIgnoreCase))
149 | {
150 | _logger.LogDebug("[TryResolve] Trying FindRpfFile({Path})", virtualPath);
151 | var archive = _rpfManager.FindRpfFile(virtualPath);
152 | if (archive != null)
153 | {
154 | rpfFile = archive;
155 | targetDir = rpfFile?.Root;
156 | return true;
157 | }
158 | }
159 |
160 | error = $"Entry not found or not a valid target: {entry?.GetType().Name ?? "null"}";
161 | _logger.LogWarning("[TryResolve] {Error}", error);
162 | return false;
163 | }
164 | catch (Exception ex)
165 | {
166 | error = ex.Message;
167 | _logger.LogError(ex, "[TryResolve] Unexpected error");
168 | return false;
169 | }
170 | }
171 |
172 | public int Preheat()
173 | {
174 | return _rpfManager.EntryDict.Count;
175 | }
176 |
177 | public RpfManager Manager => _rpfManager;
178 |
179 | }
180 |
--------------------------------------------------------------------------------
/Controllers/ConfigController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Swashbuckle.AspNetCore.Annotations;
3 | using CodeWalker.API.Models;
4 | using CodeWalker.API.Services;
5 |
6 | namespace CodeWalker.API.Controllers
7 | {
8 | [ApiController]
9 | [Route("api")]
10 | public class ConfigController : ControllerBase
11 | {
12 | private readonly ConfigService _configService;
13 | private readonly ReloadableServiceContainer _serviceContainer;
14 |
15 | public ConfigController(ConfigService configService, ReloadableServiceContainer serviceContainer)
16 | {
17 | _configService = configService;
18 | _serviceContainer = serviceContainer;
19 | }
20 |
21 | [HttpPost("set-config")]
22 | [Consumes("application/json")]
23 | [SwaggerOperation(Summary = "Set config paths (JSON)", Description = "Updates folder paths using a JSON body.")]
24 | public IActionResult SetConfigJson([FromBody] ApiConfig updated)
25 | {
26 | return MergeAndSaveConfig(updated);
27 | }
28 |
29 | [HttpPost("set-config")]
30 | [Consumes("application/x-www-form-urlencoded")]
31 | [ApiExplorerSettings(IgnoreApi = true)]
32 | public IActionResult SetConfigForm(
33 | [FromForm] string? CodewalkerOutputDir,
34 | [FromForm] string? BlenderOutputDir,
35 | [FromForm] string? FivemOutputDir,
36 | [FromForm] string? RpfArchivePath,
37 | [FromForm] string? GTAPath,
38 | [FromForm] int? Port)
39 | {
40 | var current = _configService.Get();
41 | var merged = new ApiConfig
42 | {
43 | CodewalkerOutputDir = CodewalkerOutputDir ?? current.CodewalkerOutputDir,
44 | BlenderOutputDir = BlenderOutputDir ?? current.BlenderOutputDir,
45 | FivemOutputDir = FivemOutputDir ?? current.FivemOutputDir,
46 | RpfArchivePath = RpfArchivePath ?? current.RpfArchivePath,
47 | GTAPath = GTAPath ?? current.GTAPath,
48 | Port = (Port.HasValue && Port != 0) ? Port.Value : current.Port
49 | };
50 |
51 | return MergeAndSaveConfig(merged);
52 | }
53 |
54 | private IActionResult MergeAndSaveConfig(ApiConfig merged)
55 | {
56 | var oldConfig = _configService.Get();
57 | var oldGtaPath = oldConfig.GTAPath;
58 |
59 | _configService.Set(merged);
60 |
61 | // Check if GTA path changed
62 | if (!string.Equals(oldGtaPath, merged.GTAPath, StringComparison.OrdinalIgnoreCase))
63 | {
64 | return Ok(new {
65 | message = "Configuration updated successfully. GTA path changed - services are being reloaded automatically.",
66 | gtaPathChanged = true,
67 | oldGtaPath = oldGtaPath,
68 | newGtaPath = merged.GTAPath,
69 | reloadVersion = _serviceContainer.GetReloadVersion()
70 | });
71 | }
72 |
73 | return Ok(new { message = "Configuration updated successfully." });
74 | }
75 |
76 | [HttpGet("get-config")]
77 | [SwaggerOperation(Summary = "Get config paths", Description = "Retrieves the currently configured folder paths.")]
78 | [SwaggerResponse(200, "Current config", typeof(ApiConfig))]
79 | public IActionResult GetConfig()
80 | {
81 | return Ok(_configService.Get());
82 | }
83 |
84 | [HttpPost("reset-config")]
85 | [SwaggerOperation(Summary = "Reset config paths", Description = "Resets the backend folder configuration to default values.")]
86 | [SwaggerResponse(200, "Configuration reset to defaults")]
87 | public IActionResult ResetConfig()
88 | {
89 | _configService.Set(new ApiConfig());
90 | return Ok(new { message = "Configuration reset to defaults." });
91 | }
92 |
93 | [HttpGet("service-status")]
94 | [SwaggerOperation(Summary = "Get service status", Description = "Retrieves the current service status including reload version and GTA path.")]
95 | [SwaggerResponse(200, "Service status")]
96 | public IActionResult GetServiceStatus()
97 | {
98 | var config = _configService.Get();
99 | var gtaPath = config.GTAPath;
100 |
101 | bool servicesReady = false;
102 | string statusMessage = "";
103 |
104 | if (string.IsNullOrWhiteSpace(gtaPath))
105 | {
106 | statusMessage = "GTA path is not configured";
107 | }
108 | else if (!Directory.Exists(gtaPath))
109 | {
110 | statusMessage = $"GTA V directory not found at {gtaPath}";
111 | }
112 | else
113 | {
114 | try
115 | {
116 | // Try to get a service to see if they're working
117 | var testService = _serviceContainer.GetRpfService(new LoggerFactory().CreateLogger());
118 | servicesReady = true;
119 | statusMessage = "Services are ready";
120 | }
121 | catch (InvalidOperationException ex)
122 | {
123 | statusMessage = ex.Message;
124 | }
125 | }
126 |
127 | return Ok(new {
128 | gtaPath = gtaPath,
129 | servicesReady = servicesReady,
130 | statusMessage = statusMessage,
131 | reloadVersion = _serviceContainer.GetReloadVersion(),
132 | timestamp = DateTime.UtcNow
133 | });
134 | }
135 |
136 | [HttpPost("reload-services")]
137 | [SwaggerOperation(Summary = "Reload services", Description = "Manually triggers a reload of all services (RpfService, GameFileCache, etc.) with the current configuration.")]
138 | [SwaggerResponse(200, "Services reloaded successfully")]
139 | public IActionResult ReloadServices()
140 | {
141 | var config = _configService.Get();
142 | var gtaPath = config.GTAPath;
143 |
144 | _serviceContainer.ReloadServices();
145 |
146 | return Ok(new {
147 | message = "Services reloaded successfully.",
148 | gtaPath = gtaPath,
149 | reloadVersion = _serviceContainer.GetReloadVersion(),
150 | timestamp = DateTime.UtcNow
151 | });
152 | }
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/Controllers/DownloadController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Swashbuckle.AspNetCore.Annotations;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Text;
6 | using CodeWalker.GameFiles;
7 | using CodeWalker.API.Services; // ✅ Needed for ConfigService
8 | using Microsoft.Extensions.Logging;
9 | using CodeWalker.API.Utils;
10 |
11 | namespace CodeWalker.API.Controllers
12 | {
13 | [Route("api")]
14 | [ApiController]
15 | public class DownloadController : ControllerBase
16 | {
17 | private readonly ILogger _logger;
18 | private readonly RpfService _rpfService;
19 | private readonly TextureExtractor _textureExtractor;
20 | private readonly ConfigService _configService;
21 |
22 | public DownloadController(
23 | ILogger logger,
24 | RpfService rpfService,
25 | GameFileCache gameFileCache,
26 | ILogger textureLogger,
27 | ConfigService configService) // ✅ Injected
28 | {
29 | _logger = logger;
30 | _rpfService = rpfService;
31 | _textureExtractor = new TextureExtractor(gameFileCache, textureLogger);
32 | _configService = configService;
33 | }
34 |
35 | [HttpGet("download-files")]
36 | [SwaggerOperation(
37 | Summary = "Downloads and extracts files",
38 | Description = "Extracts textures or converts YDR files to XML before saving them."
39 | )]
40 | [SwaggerResponse(200, "Successful operation", typeof(List