diff --git a/.gitattributes b/.gitattributes index 46526f6..9b6b61b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,21 @@ * text=auto eol=lf -*.png binary +*.cs text eol=lf +*.csproj text eol=lf +*.json text eol=lf +*.md text eol=lf +*.py text eol=lf +*.xaml text eol=lf +*.xml text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.ps1 text eol=crlf +*.cmd text eol=crlf +*.7z binary +*.dll binary +*.exe binary +*.ico binary *.jpg binary +*.png binary +*.pdb binary +*.webp binary +*.zip binary diff --git a/.github/workflows/public-source.yml b/.github/workflows/public-source.yml new file mode 100644 index 0000000..bfbc968 --- /dev/null +++ b/.github/workflows/public-source.yml @@ -0,0 +1,36 @@ +name: Validação pública + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + validate: + name: Código e repositório + runs-on: windows-latest + timeout-minutes: 15 + + steps: + - name: Obter o repositório + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Configurar Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.10' + + - name: Configurar .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: '8.0.x' + + - name: Validar conteúdo, testes e interface + shell: pwsh + run: .\tools\Test-PublicSource.ps1 diff --git a/.gitignore b/.gitignore index b9f8c62..ae35b8e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,27 @@ -# Pacotes e binários de distribuição permanecem fora do repositório. +# Compilação, caches e ambientes locais. +**/bin/ +**/obj/ +**/__pycache__/ +*.pyc +.tools/ +.vs/ +.idea/ + +# Pacotes, binários e artefatos de distribuição. *.7z *.dll *.exe *.pdb *.zip +dist/ +installer/NIKKEPTBR.Installer/Payload/ -# Artefatos locais e privados. +# Evidências e dados privados do workspace. private/ work/ -# Arquivos gerados pelo sistema operacional. +# Temporários e arquivos do sistema operacional. +*.log +*.tmp .DS_Store Thumbs.db diff --git a/README.md b/README.md index 4c412ca..a5118bf 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@

GODDESS OF VICTORY: NIKKE PT-BR / PC

+

+ Validação pública +

+

TRADUÇÃO COMUNITÁRIA / BETA / WINDOWS X64

@@ -53,7 +57,8 @@ O projeto é criado, dirigido, mantido e validado por **Kacks**. Não possui afi | Apontamentos editoriais preservados | **1.689** | | Pacote transacional | **18/18 verificações** | | Executável final | **11/11 verificações** | -| Suíte atual do projeto | **116/116 testes** | +| Suíte da versão publicada | **116/116 testes** | +| Testes públicos do instalador | **23/23 aprovados** | Esses números descrevem cobertura e integridade técnica. Eles não significam que 429.498 frases receberam revisão humana individual. Os apontamentos editoriais permanecem registrados para uma etapa futura, sem invalidar o funcionamento da primeira versão. @@ -113,22 +118,31 @@ Nenhum mod pode prometer compatibilidade absoluta com versões futuras ainda des ## 07 / TRADUÇÃO EM JOGO -As imagens abaixo são capturas reais do cliente com a integração PT-BR ativa. +As imagens abaixo são capturas reais do cliente com a integração PT-BR ativa. Elas demonstram lobby, personagem, narrativa, escolhas, campanha, progressão e inventário. + +| Lobby e personagem | Diálogo e cena de história | +| --- | --- | +| ![Lobby, navegação e objetivos em português brasileiro](./assets/screenshots/01-lobby-ptbr.webp) | ![Diálogo narrativo em português brasileiro](./assets/screenshots/03-dialogo-narrativo-ptbr.webp) | +| ![Perfil de personagem, atributos e ações em português brasileiro](./assets/screenshots/02-perfil-personagem-ptbr.webp) | ![Cena de história traduzida para português brasileiro](./assets/screenshots/04-cena-historia-ptbr.webp) | -| Identificação de personagem | Diálogo narrativo | +| Escolhas e campanha | Progressão e inventário | | --- | --- | -| ![Andersen identificado como Vice-Chefe do Comando do Governo Central](./assets/screenshots/01-dialogo-ptbr.png) | ![Diálogo em português brasileiro com Andersen](./assets/screenshots/02-dialogo-ptbr.png) | +| ![Escolhas de diálogo em português brasileiro](./assets/screenshots/05-escolhas-dialogo-ptbr.webp) | ![Progressão e recompensas em português brasileiro](./assets/screenshots/07-progressao-recompensas-ptbr.webp) | +| ![Campanha, formação e preparação de batalha](./assets/screenshots/06-campanha-formacao-ptbr.webp) | ![Inventário e descrição de item em português brasileiro](./assets/screenshots/08-inventario-item-ptbr.webp) | -## 08 / CONTEÚDO PÚBLICO E VERIFICAÇÃO +## 08 / CÓDIGO PÚBLICO E VERIFICAÇÃO -| Área | Conteúdo | +| Área | Conteúdo público | | --- | --- | +| [`installer`](./installer) | Interface WPF e núcleo transacional usados pelo instalador gráfico. | +| [`tools`](./tools) | Contrato do pacote, bootstrap determinístico e auditoria do repositório. | +| [`tests`](./tests) | Testes automatizados do pacote, das transações e da interface com o núcleo. | | [`docs`](./docs) | Arquitetura, compatibilidade, instalação, status, autoria e roadmap. | | [`assets`](./assets) | Identidade visual e capturas aprovadas para a página do projeto. | | [`release`](./release) | Manifesto público sem textos nem arquivos proprietários do jogo. | | [`CHANGELOG.md`](./CHANGELOG.md) | Histórico das versões distribuídas. | -Cada Release inclui o instalador, manifesto JSON e arquivo SHA-256. O repositório não publica bancos extraídos, textos integrais, chaves, executáveis ou outros arquivos proprietários do cliente. +Cada Release inclui o instalador, manifesto JSON e arquivo SHA-256. O repositório publica o código autoral necessário para auditar a interface e a segurança transacional, mas não distribui o catálogo, a carga útil, bancos extraídos, textos integrais, chaves, executáveis ou arquivos proprietários do cliente. Consulte [Compilação e limites do código público](./docs/COMPILACAO.md). ## 09 / AUTORIA E TRANSPARÊNCIA diff --git a/assets/screenshots/01-dialogo-ptbr.png b/assets/screenshots/01-dialogo-ptbr.png deleted file mode 100644 index e37b44c..0000000 Binary files a/assets/screenshots/01-dialogo-ptbr.png and /dev/null differ diff --git a/assets/screenshots/01-lobby-ptbr.webp b/assets/screenshots/01-lobby-ptbr.webp new file mode 100644 index 0000000..c393adb Binary files /dev/null and b/assets/screenshots/01-lobby-ptbr.webp differ diff --git a/assets/screenshots/02-dialogo-ptbr.png b/assets/screenshots/02-dialogo-ptbr.png deleted file mode 100644 index 7d23f23..0000000 Binary files a/assets/screenshots/02-dialogo-ptbr.png and /dev/null differ diff --git a/assets/screenshots/02-perfil-personagem-ptbr.webp b/assets/screenshots/02-perfil-personagem-ptbr.webp new file mode 100644 index 0000000..f27b0e5 Binary files /dev/null and b/assets/screenshots/02-perfil-personagem-ptbr.webp differ diff --git a/assets/screenshots/03-dialogo-narrativo-ptbr.webp b/assets/screenshots/03-dialogo-narrativo-ptbr.webp new file mode 100644 index 0000000..951fb76 Binary files /dev/null and b/assets/screenshots/03-dialogo-narrativo-ptbr.webp differ diff --git a/assets/screenshots/04-cena-historia-ptbr.webp b/assets/screenshots/04-cena-historia-ptbr.webp new file mode 100644 index 0000000..b61f854 Binary files /dev/null and b/assets/screenshots/04-cena-historia-ptbr.webp differ diff --git a/assets/screenshots/05-escolhas-dialogo-ptbr.webp b/assets/screenshots/05-escolhas-dialogo-ptbr.webp new file mode 100644 index 0000000..69b484a Binary files /dev/null and b/assets/screenshots/05-escolhas-dialogo-ptbr.webp differ diff --git a/assets/screenshots/06-campanha-formacao-ptbr.webp b/assets/screenshots/06-campanha-formacao-ptbr.webp new file mode 100644 index 0000000..94be1bd Binary files /dev/null and b/assets/screenshots/06-campanha-formacao-ptbr.webp differ diff --git a/assets/screenshots/07-progressao-recompensas-ptbr.webp b/assets/screenshots/07-progressao-recompensas-ptbr.webp new file mode 100644 index 0000000..3084910 Binary files /dev/null and b/assets/screenshots/07-progressao-recompensas-ptbr.webp differ diff --git a/assets/screenshots/08-inventario-item-ptbr.webp b/assets/screenshots/08-inventario-item-ptbr.webp new file mode 100644 index 0000000..364ef5f Binary files /dev/null and b/assets/screenshots/08-inventario-item-ptbr.webp differ diff --git a/docs/AUTORIA-E-PROCESSO.md b/docs/AUTORIA-E-PROCESSO.md index 246c216..c3df552 100644 --- a/docs/AUTORIA-E-PROCESSO.md +++ b/docs/AUTORIA-E-PROCESSO.md @@ -39,6 +39,6 @@ A versão `0.1.0 beta` é uma primeira versão funcional de ampla cobertura. A v ## Política de transparência -O repositório publica métricas agregadas, documentação, manifesto e limitações. Bancos extraídos, textos integrais, chaves, dumps e arquivos proprietários do jogo não são distribuídos. +O repositório publica métricas agregadas, documentação, manifesto, limitações e o código autoral necessário para auditar a interface e a segurança transacional do instalador. Bancos extraídos, catálogo, carga útil, textos integrais, chaves, dumps, binários e arquivos proprietários do jogo não são distribuídos. Cada versão declara o cliente validado e não promete compatibilidade com atualizações futuras desconhecidas. diff --git a/docs/COMPILACAO.md b/docs/COMPILACAO.md new file mode 100644 index 0000000..7604285 --- /dev/null +++ b/docs/COMPILACAO.md @@ -0,0 +1,32 @@ +# Compilação e limites do código público + +O repositório disponibiliza o código autoral da interface WPF, do núcleo transacional e das ferramentas que validam o pacote do NIKKE PT-BR. Esse material permite auditar as regras de instalação, verificação, reparo, remoção, recuperação e integridade. + +## Conteúdo intencionalmente ausente + +O catálogo de tradução, o índice compilado, os binários do runtime, o bootstrap incorporado e qualquer dado obtido do cliente não fazem parte do repositório. Por esse motivo, compilar apenas o código público produz a interface verificável do instalador, mas não uma distribuição funcional do mod. + +O instalador pronto e aprovado deve ser obtido exclusivamente na [Release oficial](https://github.com/kacksdev/nikke-ptbr/releases/latest). Não aceite compilações de terceiros como equivalentes à versão publicada. + +## Requisitos para validação + +- Windows x64; +- Python 3.10 ou posterior; +- .NET SDK 8 ou posterior; +- acesso à internet apenas para restaurar o pacote de referências do .NET na primeira compilação. + +## Validação local + +Na raiz do repositório, execute: + +```powershell +.\tools\Test-PublicSource.ps1 +``` + +O script executa a auditoria do conteúdo público, os testes do núcleo transacional e a compilação determinística da interface WPF sem carga útil. A mesma sequência é executada pelo GitHub Actions. + +## Construção oficial + +A construção de uma distribuição funcional exige uma carga útil privada previamente aprovada, composta somente pelos três componentes pertencentes ao projeto. O bootstrap é criado com ordem e timestamps fixos, tem cada arquivo registrado por SHA-256 e é incorporado ao instalador somente depois da verificação do contrato. + +Esse processo deliberadamente não é transformado em um download alternativo pelo repositório. A separação protege os direitos dos titulares, impede a publicação acidental de dados do jogo e mantém uma única origem oficial para o binário testado. diff --git a/docs/FERRAMENTAS.md b/docs/FERRAMENTAS.md index 638aaec..f99061e 100644 --- a/docs/FERRAMENTAS.md +++ b/docs/FERRAMENTAS.md @@ -42,3 +42,9 @@ O instalador não baixa componentes nem envia telemetria durante a operação. | OpenAI Codex | Tradução em escala, engenharia, auditoria, testes e documentação sob direção do mantenedor. | | Git e GitHub | Versionamento, Issues, segurança, manifesto e Releases. | | GameBanana | Distribuição alternativa e feedback da comunidade. | + +## Código público + +O repositório público contém a interface WPF do instalador, o núcleo transacional em Python, o empacotador determinístico, testes automatizados e a validação contínua. O catálogo PT-BR, a carga útil compilada, binários e dados obtidos do cliente não são publicados. + +Consulte [Compilação e limites do código público](./COMPILACAO.md) para reproduzir a validação disponível sem material privado. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7a36c48..5ed7f78 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -53,13 +53,13 @@ O roadmap separa cobertura técnica, revisão editorial, integração, instalaç ## Fase 5: primeira publicação -**Estado: em preparação** +**Estado: concluída em 11 de setembro de 2026** -- documentação pública atualizada; -- manifesto e checksums preparados; -- Release do GitHub preparada em rascunho; -- página do GameBanana mantida privada até revisão final; -- publicação simultânea somente após autorização explícita. +- documentação pública revisada; +- Release `v0.1.0` publicada no GitHub com instalador, manifesto e checksum; +- distribuição alternativa publicada no GameBanana; +- hashes dos downloads públicos conferidos contra os artefatos aprovados; +- código autoral do instalador e do núcleo transacional disponibilizado para auditoria. ## Fase 6: manutenção e maturidade editorial diff --git a/docs/STATUS.md b/docs/STATUS.md index dcb5d22..4b53db6 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,8 +1,8 @@ # Status técnico -Atualizado em 10 de setembro de 2026. +Atualizado em 11 de setembro de 2026. -## Versão preparada +## Versão publicada | Item | Estado | | --- | --- | @@ -37,7 +37,8 @@ Nenhum arquivo oficial é substituído. O runtime valida o cliente e falha de fo | --- | --- | | Pacote transacional | 18/18 | | Executável final em réplica | 11/11 | -| Suíte atual | 116/116 | +| Suíte da versão publicada | 116/116 | +| Testes públicos do instalador | 23/23 | | Construções finais consecutivas | Idênticas byte a byte | | Instalação real | Aprovada | | Inicialização pelo launcher | Aprovada | @@ -62,4 +63,6 @@ A carga útil exata da versão final foi aceita no cliente real. Depois dessa pr ## Publicação -Os artefatos preparados para a versão incluem instalador, SHA-256, manifesto e documentação. A página pública deve sempre indicar o cliente validado e separar cobertura técnica de revisão editorial. +A versão `0.1.0` está disponível na [Release oficial do GitHub](https://github.com/kacksdev/nikke-ptbr/releases/tag/v0.1.0) e na [página oficial do GameBanana](https://gamebanana.com/mods/715729). Os dois downloads públicos foram conferidos por SHA-256 contra os artefatos aprovados. + +O código autoral da interface do instalador, do núcleo transacional e dos respectivos testes está publicado para auditoria. Catálogo, carga útil, binários compilados e materiais proprietários do cliente permanecem fora do repositório. diff --git a/installer/NIKKEPTBR.Installer/App.xaml b/installer/NIKKEPTBR.Installer/App.xaml new file mode 100644 index 0000000..568b2c7 --- /dev/null +++ b/installer/NIKKEPTBR.Installer/App.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + diff --git a/installer/NIKKEPTBR.Installer/App.xaml.cs b/installer/NIKKEPTBR.Installer/App.xaml.cs new file mode 100644 index 0000000..a3b2bf0 --- /dev/null +++ b/installer/NIKKEPTBR.Installer/App.xaml.cs @@ -0,0 +1,110 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web.Script.Serialization; +using System.Windows; + +namespace NIKKEPTBR.Installer +{ + public partial class App : Application + { + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + if (e.Args.Any(arg => string.Equals(arg, "--headless", StringComparison.OrdinalIgnoreCase))) + { + RunHeadless(e.Args); + return; + } + + var requested = ReadOption(e.Args, "--game-root"); + var window = new MainWindow(requested); + MainWindow = window; + ShutdownMode = ShutdownMode.OnMainWindowClose; + window.Show(); + } + + private void RunHeadless(string[] args) + { + CoreResponse response; + HeadlessOptions options = null; + try + { + options = HeadlessOptions.Parse(args); + var bridge = new CoreBridge(options.CacheRoot, options.StateRoot); + response = bridge.ExecuteAsync( + options.Action, + options.GameRoot, + new InlineProgress(_ => { })).GetAwaiter().GetResult(); + } + catch (Exception error) + { + response = new CoreResponse + { + Success = false, + Action = options?.Action.ToString().ToLowerInvariant() ?? "unknown", + Message = error.Message, + Technical = error.ToString() + }; + } + + var reportPath = options?.ReportPath ?? ReadOption(args, "--report"); + if (!string.IsNullOrWhiteSpace(reportPath)) + { + WriteJsonAtomic(Path.GetFullPath(reportPath), response); + } + Environment.ExitCode = response.Success ? 0 : 1; + Shutdown(Environment.ExitCode); + } + + private static string ReadOption(string[] args, string name) + { + for (var i = 0; i + 1 < args.Length; i++) + { + if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + return null; + } + + private static void WriteJsonAtomic(string path, CoreResponse response) + { + var parent = Path.GetDirectoryName(path); + if (string.IsNullOrWhiteSpace(parent)) + { + throw new InvalidOperationException("Caminho de relatório inválido."); + } + Directory.CreateDirectory(parent); + var temporary = path + ".tmp-" + Guid.NewGuid().ToString("N"); + var serializer = new JavaScriptSerializer { MaxJsonLength = int.MaxValue }; + File.WriteAllText(temporary, serializer.Serialize(response), new UTF8Encoding(false)); + if (File.Exists(path)) + { + File.Replace(temporary, path, null); + } + else + { + File.Move(temporary, path); + } + } + } + + internal sealed class InlineProgress : IProgress + { + private readonly Action _callback; + + public InlineProgress(Action callback) + { + _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + } + + public void Report(T value) + { + _callback(value); + } + } +} diff --git a/installer/NIKKEPTBR.Installer/Assets/THIRD-PARTY-NOTICES.txt b/installer/NIKKEPTBR.Installer/Assets/THIRD-PARTY-NOTICES.txt new file mode 100644 index 0000000..28f0e38 --- /dev/null +++ b/installer/NIKKEPTBR.Installer/Assets/THIRD-PARTY-NOTICES.txt @@ -0,0 +1,51 @@ +NIKKE PT-BR installer third-party notices + +Python 3.10 +Copyright (c) 2001-2026 Python Software Foundation. +Distributed under the Python Software Foundation License Version 2. +https://www.python.org/psf/license/ + +PyInstaller 6.22.2 +Copyright (c) 2010-2026 PyInstaller Development Team. +Distributed under GPL-2.0-or-later with the PyInstaller bootloader exception. +https://pyinstaller.org/ + +MinHook 1.3.4 +Commit c3fcafdc10146beb5919319d0683e44e3c30d537. +Copyright (C) 2009-2017 Tsuda Kageyu. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +Portions of MinHook include Hacker Disassembler Engine 32/64 C. +Copyright (c) 2008-2009, Vyacheslav Patkov. +All rights reserved. These portions use the same BSD 2-Clause conditions and +disclaimer reproduced above. + +MinHook source and complete license: +https://github.com/TsudaKageyu/minhook/tree/v1.3.4 + +The installer interface uses Microsoft .NET Framework components supplied by Windows. + +GODDESS OF VICTORY: NIKKE and its artwork belong to their respective rights holders. +This free community translation is independent and is not affiliated with or endorsed by +SHIFT UP, Level Infinite, Proxima Beta, or the official game publishers. diff --git a/installer/NIKKEPTBR.Installer/Assets/installer-background.png b/installer/NIKKEPTBR.Installer/Assets/installer-background.png new file mode 100644 index 0000000..110009d Binary files /dev/null and b/installer/NIKKEPTBR.Installer/Assets/installer-background.png differ diff --git a/installer/NIKKEPTBR.Installer/Assets/kacks-installer.ico b/installer/NIKKEPTBR.Installer/Assets/kacks-installer.ico new file mode 100644 index 0000000..cf9ceaf Binary files /dev/null and b/installer/NIKKEPTBR.Installer/Assets/kacks-installer.ico differ diff --git a/installer/NIKKEPTBR.Installer/BootstrapMetadata.cs b/installer/NIKKEPTBR.Installer/BootstrapMetadata.cs new file mode 100644 index 0000000..02fba4e --- /dev/null +++ b/installer/NIKKEPTBR.Installer/BootstrapMetadata.cs @@ -0,0 +1,12 @@ +namespace NIKKEPTBR.Installer +{ + internal static class BootstrapMetadata + { + public const string ResourceName = "NIKKEPTBR.Bootstrap.zip"; + public const string ExpectedSha256 = "ED14ECD3139F8D4FDC78A906E1AB88C0E3382F5F949A44A1B7BF863556DEEB89"; + public const long ExpectedSize = 41878741; + public const string BuildId = "CB9793DE936A2DF96CDC84A2"; + public const string CoreRelativePath = "core/NIKKEPTBR-Core.exe"; + public const string PackageRelativePath = "package"; + } +} diff --git a/installer/NIKKEPTBR.Installer/CoreBridge.cs b/installer/NIKKEPTBR.Installer/CoreBridge.cs new file mode 100644 index 0000000..bfbe5c6 --- /dev/null +++ b/installer/NIKKEPTBR.Installer/CoreBridge.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using System.Web.Script.Serialization; + +namespace NIKKEPTBR.Installer +{ + public sealed class CoreBridge + { + private readonly string _cacheOverride; + private readonly string _stateOverride; + private readonly object _bootstrapLock = new object(); + private BootstrapContext _context; + + public CoreBridge(string cacheOverride = null, string stateOverride = null) + { + _cacheOverride = cacheOverride; + _stateOverride = stateOverride; + } + + public Task ExecuteAsync( + InstallerAction action, + string gameRoot, + IProgress progress) + { + if (string.IsNullOrWhiteSpace(gameRoot)) + { + throw new ArgumentException("A pasta do jogo não foi informada.", nameof(gameRoot)); + } + return Task.Run(() => Execute(action, Path.GetFullPath(gameRoot), progress)); + } + + private CoreResponse Execute( + InstallerAction action, + string gameRoot, + IProgress progress) + { + var context = EnsureBootstrap(progress); + var stateRoot = string.IsNullOrWhiteSpace(_stateOverride) + ? DefaultStateRoot(gameRoot) + : Path.GetFullPath(_stateOverride); + var allowedParent = Directory.GetParent(gameRoot)?.FullName; + if (string.IsNullOrWhiteSpace(allowedParent)) + { + throw new InvalidOperationException("A pasta do cliente não possui um diretório pai seguro."); + } + + var arguments = new List + { + "--action", ActionName(action), + "--package-root", context.PackageRoot, + "--target-root", gameRoot, + "--state-root", stateRoot, + "--allowed-target-parent", allowedParent + }; + var start = new ProcessStartInfo + { + FileName = context.CoreExecutable, + Arguments = JoinArguments(arguments), + WorkingDirectory = Path.GetDirectoryName(context.CoreExecutable), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + StandardOutputEncoding = new UTF8Encoding(false), + StandardErrorEncoding = new UTF8Encoding(false) + }; + start.EnvironmentVariables["PYTHONUTF8"] = "1"; + start.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8"; + + CoreResponse final = null; + var serializer = new JavaScriptSerializer { MaxJsonLength = int.MaxValue }; + using (var process = new Process { StartInfo = start }) + { + if (!process.Start()) + { + throw new InvalidOperationException("Não foi possível iniciar o núcleo seguro do instalador."); + } + var errorRead = process.StandardError.ReadToEndAsync(); + string line; + while ((line = process.StandardOutput.ReadLine()) != null) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + Dictionary payload; + try + { + payload = serializer.Deserialize>(line); + } + catch (Exception parseError) + { + throw new InvalidDataException("Resposta interna inválida do instalador.", parseError); + } + + object typeValue; + var type = payload.TryGetValue("type", out typeValue) + ? Convert.ToString(typeValue) + : string.Empty; + if (string.Equals(type, "progress", StringComparison.OrdinalIgnoreCase)) + { + var corePercent = GetInt(payload, "percent"); + progress?.Report(new InstallerProgress + { + Percent = 20 + (int)Math.Round(corePercent * 0.80), + Phase = GetString(payload, "phase"), + Message = GetString(payload, "message") + }); + } + else if (string.Equals(type, "result", StringComparison.OrdinalIgnoreCase)) + { + final = ParseResponse(payload); + } + } + process.WaitForExit(); + var standardError = errorRead.GetAwaiter().GetResult(); + if (final == null) + { + throw new InvalidOperationException( + "O núcleo do instalador terminou sem um relatório válido. " + standardError.Trim()); + } + if (!final.Success || process.ExitCode != 0) + { + if (string.IsNullOrWhiteSpace(final.Technical)) + { + final.Technical = standardError.Trim(); + } + return final; + } + } + return final; + } + + private BootstrapContext EnsureBootstrap(IProgress progress) + { + lock (_bootstrapLock) + { + if (_context == null) + { + _context = new PackageBootstrap(_cacheOverride).EnsureReady(progress); + } + return _context; + } + } + + private static CoreResponse ParseResponse(Dictionary payload) + { + object dataValue; + var data = payload.TryGetValue("data", out dataValue) + ? dataValue as Dictionary + : null; + return new CoreResponse + { + Success = GetBool(payload, "success"), + Action = GetString(payload, "action"), + Message = GetString(payload, "message"), + Technical = GetString(payload, "technical"), + PackageId = GetString(payload, "package_id"), + PackageIdentity = GetString(payload, "package_identity_sha256"), + ModVersion = GetString(payload, "mod_version"), + ClientVersion = GetString(payload, "client_version"), + Data = data ?? new Dictionary() + }; + } + + private static string DefaultStateRoot(string gameRoot) + { + var baseRoot = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Kacksdev", + "NIKKEPTBR", + "state"); + var normalized = Path.GetFullPath(gameRoot).TrimEnd(Path.DirectorySeparatorChar).ToUpperInvariant(); + string key; + using (var sha = SHA256.Create()) + { + key = BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(normalized))) + .Replace("-", string.Empty) + .Substring(0, 20); + } + return Path.Combine(baseRoot, key); + } + + private static string ActionName(InstallerAction action) + { + return action.ToString().ToLowerInvariant(); + } + + private static string GetString(IDictionary payload, string key) + { + object value; + return payload.TryGetValue(key, out value) && value != null + ? Convert.ToString(value, CultureInfo.InvariantCulture) + : string.Empty; + } + + private static int GetInt(IDictionary payload, string key) + { + object value; + return payload.TryGetValue(key, out value) && value != null + ? Convert.ToInt32(value, CultureInfo.InvariantCulture) + : 0; + } + + private static bool GetBool(IDictionary payload, string key) + { + object value; + return payload.TryGetValue(key, out value) && value != null && + Convert.ToBoolean(value, CultureInfo.InvariantCulture); + } + + private static string JoinArguments(IEnumerable values) + { + var result = new StringBuilder(); + foreach (var value in values) + { + if (result.Length > 0) + { + result.Append(' '); + } + result.Append(QuoteArgument(value ?? string.Empty)); + } + return result.ToString(); + } + + private static string QuoteArgument(string value) + { + if (value.Length > 0 && value.IndexOfAny(new[] { ' ', '\t', '\n', '\v', '"' }) < 0) + { + return value; + } + var result = new StringBuilder(); + result.Append('"'); + var backslashes = 0; + foreach (var character in value) + { + if (character == '\\') + { + backslashes++; + continue; + } + if (character == '"') + { + result.Append('\\', backslashes * 2 + 1); + result.Append('"'); + backslashes = 0; + continue; + } + result.Append('\\', backslashes); + backslashes = 0; + result.Append(character); + } + result.Append('\\', backslashes * 2); + result.Append('"'); + return result.ToString(); + } + } +} diff --git a/installer/NIKKEPTBR.Installer/GameDetector.cs b/installer/NIKKEPTBR.Installer/GameDetector.cs new file mode 100644 index 0000000..9e0bbc8 --- /dev/null +++ b/installer/NIKKEPTBR.Installer/GameDetector.cs @@ -0,0 +1,183 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace NIKKEPTBR.Installer +{ + public static class GameDetector + { + private static readonly string[] RequiredFiles = + { + "nikke.exe", + "UnityPlayer.dll", + "GameAssembly.dll" + }; + + public static string Detect() + { + foreach (var candidate in CandidateRoots()) + { + var normalized = Normalize(candidate); + if (!string.IsNullOrWhiteSpace(normalized)) + { + return normalized; + } + } + return null; + } + + public static string Normalize(string requested) + { + if (string.IsNullOrWhiteSpace(requested)) + { + return null; + } + try + { + var basePath = File.Exists(requested) + ? Path.GetDirectoryName(Path.GetFullPath(requested)) + : Path.GetFullPath(requested); + var variants = new[] + { + basePath, + Path.Combine(basePath, "game"), + Path.Combine(basePath, "NIKKE", "game"), + Path.Combine(basePath, "NIKKE", "NIKKE", "game") + }; + return variants.FirstOrDefault(IsClientRoot); + } + catch + { + return null; + } + } + + public static bool IsClientRoot(string path) + { + return !string.IsNullOrWhiteSpace(path) && + Directory.Exists(path) && + RequiredFiles.All(name => File.Exists(Path.Combine(path, name))); + } + + private static IEnumerable CandidateRoots() + { + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var candidate in RawCandidates()) + { + if (string.IsNullOrWhiteSpace(candidate)) + { + continue; + } + string full; + try + { + full = Path.GetFullPath(candidate); + } + catch + { + continue; + } + if (seen.Add(full)) + { + yield return full; + } + } + } + + private static IEnumerable RawCandidates() + { + var environment = Environment.GetEnvironmentVariable("NIKKE_GAME_ROOT"); + if (!string.IsNullOrWhiteSpace(environment)) + { + yield return environment; + } + yield return AppDomain.CurrentDomain.BaseDirectory; + yield return Environment.CurrentDirectory; + + foreach (var processName in new[] { "nikke", "nikke_launcher" }) + { + foreach (var process in Process.GetProcessesByName(processName)) + { + using (process) + { + string executable = null; + try + { + executable = process.MainModule?.FileName; + } + catch + { + } + if (!string.IsNullOrWhiteSpace(executable)) + { + yield return Path.GetDirectoryName(executable); + } + } + } + } + + foreach (var location in RegistryLocations()) + { + yield return location; + } + + foreach (var drive in DriveInfo.GetDrives().Where(item => item.IsReady && item.DriveType == DriveType.Fixed)) + { + var root = drive.RootDirectory.FullName; + yield return Path.Combine(root, "Games", "NIKKE", "NIKKE", "game"); + yield return Path.Combine(root, "Games", "NIKKE"); + yield return Path.Combine(root, "NIKKE", "NIKKE", "game"); + yield return Path.Combine(root, "NIKKE"); + yield return Path.Combine(root, "Level Infinite", "NIKKE"); + yield return Path.Combine(root, "Program Files", "NIKKE"); + yield return Path.Combine(root, "Program Files (x86)", "NIKKE"); + } + } + + private static IEnumerable RegistryLocations() + { + var locations = new List(); + foreach (var hive in new[] { RegistryHive.CurrentUser, RegistryHive.LocalMachine }) + { + foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 }) + { + try + { + using (var root = RegistryKey.OpenBaseKey(hive, view)) + using (var uninstall = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")) + { + if (uninstall == null) + { + continue; + } + foreach (var name in uninstall.GetSubKeyNames()) + { + using (var key = uninstall.OpenSubKey(name)) + { + var display = Convert.ToString(key?.GetValue("DisplayName")); + if (display?.IndexOf("NIKKE", StringComparison.OrdinalIgnoreCase) < 0 && + display?.IndexOf("GODDESS OF VICTORY", StringComparison.OrdinalIgnoreCase) < 0) + { + continue; + } + var install = Convert.ToString(key?.GetValue("InstallLocation")); + if (!string.IsNullOrWhiteSpace(install)) + { + locations.Add(install.Trim('"')); + } + } + } + } + } + catch + { + } + } + } + return locations; + } + } +} diff --git a/installer/NIKKEPTBR.Installer/MainWindow.xaml b/installer/NIKKEPTBR.Installer/MainWindow.xaml new file mode 100644 index 0000000..ee59021 --- /dev/null +++ b/installer/NIKKEPTBR.Installer/MainWindow.xaml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +