diff --git a/.github/workflows/controller.yml b/.github/workflows/controller.yml
index 7e73f429c60..1eeebf5d317 100644
--- a/.github/workflows/controller.yml
+++ b/.github/workflows/controller.yml
@@ -59,3 +59,7 @@ jobs:
needs: build
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/macos_m1.yml
+ windows:
+ needs: build
+ if: ${{ !failure() && !cancelled() }}
+ uses: ./.github/workflows/windows.yml
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
new file mode 100644
index 00000000000..7669990bf90
--- /dev/null
+++ b/.github/workflows/windows.yml
@@ -0,0 +1,63 @@
+name: windows
+
+on: workflow_call
+
+env:
+ MYSQL_DATABASE: 'ragnarok'
+ MYSQL_USER: 'ragnarok'
+ MYSQL_PASSWORD: 'ragnarok'
+ MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
+
+jobs:
+ build:
+ runs-on: windows-latest
+ timeout-minutes: 60
+ strategy:
+ matrix:
+ CC: [msbuild]
+ RENEWAL: ["", "DISABLE_RENEWAL"]
+ CLIENT_TYPE: ["", "ENABLE_PACKETVER_RE", "ENABLE_PACKETVER_ZERO"]
+ PACKET_VERSION: ["PACKETVER=20221024", "PACKETVER=20130724"]
+ exclude:
+ - PACKET_VERSION: "PACKETVER=20130724"
+ CLIENT_TYPE: "ENABLE_PACKETVER_ZERO"
+
+ # github.head_ref will stop previous runs in the same PR (if in a PR)
+ # github.run_id is a fallback when outside a PR (e.g. every merge in master will run, and previous won't stop)
+ concurrency:
+ group: msbuild-${{ github.head_ref || github.run_id }}_${{ matrix.CC }}_${{ matrix.RENEWAL }}_${{ matrix.CLIENT_TYPE }}_${{ matrix.PACKET_VERSION}}
+ cancel-in-progress: true
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Add msbuild to PATH
+ uses: microsoft/setup-msbuild@v2
+
+ - uses: shogo82148/actions-setup-mysql@v1
+ with:
+ distribution: "mariadb"
+ mysql-version: "11.1"
+
+ - name: init database
+ run: |
+ mysql -u root -e "CREATE DATABASE IF NOT EXISTS ${{ env.MYSQL_DATABASE }};"
+ mysql -u root -e "CREATE USER IF NOT EXISTS '${{ env.MYSQL_USER }}'@'localhost' IDENTIFIED BY '${{ env.MYSQL_PASSWORD }}';"
+ mysql -u root -e "GRANT SELECT,INSERT,UPDATE,DELETE ON ${{ env.MYSQL_DATABASE }}.* TO '${{ env.MYSQL_USER }}'@'localhost';"
+ Get-Content sql-files/main.sql | mysql -u root ${{ env.MYSQL_DATABASE }}
+ Get-Content sql-files/logs.sql | mysql -u root ${{ env.MYSQL_DATABASE }}
+
+ - name: build
+ run: |
+ .\tools\ci\windows.ps1 build ${{ matrix.CLIENT_TYPE }} ${{ matrix.PACKET_VERSION}} ${{ matrix.RENEWAL }}
+
+ - name: run
+ run: |
+ .\tools\ci\windows.ps1 run
+
+ - name: test
+ if: matrix.PACKET_VERSION != 'PACKETVER=20130724'
+ run: |
+ .\tools\ci\windows.ps1 test
diff --git a/tools/ci/windows.ps1 b/tools/ci/windows.ps1
new file mode 100644
index 00000000000..a1dd853f34c
--- /dev/null
+++ b/tools/ci/windows.ps1
@@ -0,0 +1,104 @@
+# This file is part of Hercules.
+# http://herc.ws - http://github.com/HerculesWS/Hercules
+#
+# Copyright (C) 2024 Hercules Dev Team
+#
+# Hercules is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# Helper script for the Windows CI build
+
+function CreatePluginProject {
+ param([string]$pluginName)
+
+ Write-Host "Creating project for plugin $pluginName"
+
+ $pluginFileSample = "vcproj\\plugin-sample.vcxproj"
+ $pluginFile = "vcproj\\plugin-$pluginName.vcxproj"
+
+ Copy-Item -Path $pluginFileSample -Destination $pluginFile
+ (Get-Content $pluginFile) -replace "sample", $pluginName | Set-Content $pluginFile
+}
+
+function WritePropsFile {
+ param([string[]]$definitions)
+
+ Write-Host "Writing props file with definitions: $definitions"
+
+ # Add preprocessor definitions for msbuild using a props file
+ # https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-cpp-builds?view=vs-2022
+ $propsFile = Join-Path -Path (Get-Location) -ChildPath "vcproj\AdditionalDefinitions.props"
+@"
+
+
+
+
+ %(PreprocessorDefinitions);$($definitions -join ";")
+
+
+
+"@ | Out-File -FilePath $propsFile -Encoding utf8
+ return $propsFile
+}
+
+function CatchProcessErrors {
+ param([string]$programName, [string]$additionalArgs)
+
+ Write-Host "Running server $programName with arguments: $additionalArgs"
+
+ $process = Start-Process -FilePath "$programName" -ArgumentList $additionalArgs -NoNewWindow -PassThru -RedirectStandardError "error_$programName.txt"
+ $process.WaitForExit()
+
+ $errorText = Get-Content "error_$programName.txt" -TotalCount 10000
+ if ($null -ne $errorText -and $errorText -ne "") {
+ Write-Host "Errors found in running server $programName"
+ Write-Host "$errorText"
+ exit 1
+ }
+
+ if ($process.ExitCode -ne 0) {
+ Write-Host "$programName failed with exit code $($process.ExitCode)"
+ exit 1
+ }
+}
+
+Write-Host "Arguments: $args"
+if ($args[0] -eq "build") {
+ $propsFile = WritePropsFile $args[1..($args.Length - 1)]
+ CatchProcessErrors msbuild "-m Hercules.sln /p:ForceImportBeforeCppTargets=$propsFile"
+
+ foreach ($plugin in @("httpsample", "constdb2doc", "db2sql", "dbghelpplug", "generate-translations", "mapcache", "script_mapquit")) {
+ CreatePluginProject $plugin
+ CatchProcessErrors msbuild "-m vcproj\\plugin-$plugin.vcxproj /p:ForceImportBeforeCppTargets=$propsFile"
+ }
+}
+elseif ($args[0] -eq "run") {
+ $serverArgs = "--run-once"
+ CatchProcessErrors "api-server.exe" $serverArgs
+ CatchProcessErrors "login-server.exe" $serverArgs
+ CatchProcessErrors "char-server.exe" $serverArgs
+ CatchProcessErrors "map-server.exe" $serverArgs
+}
+elseif ($args[0] -eq "test") {
+ $serverArgs = "--run-once"
+ foreach ($plugin in @("HPMHooking", "httpsample", "constdb2doc", "db2sql", "dbghelpplug", "generate-translations", "mapcache", "script_mapquit")) {
+ $serverArgs += " --load-plugin $plugin"
+ }
+ CatchProcessErrors "map-server.exe" "$serverArgs --load-script npc/dev/test.txt --load-script npc/dev/ci_test.txt"
+ CatchProcessErrors "map-server.exe" "$serverArgs --constdb2doc"
+ CatchProcessErrors "map-server.exe" "$serverArgs --db2sql"
+ CatchProcessErrors "map-server.exe" "$serverArgs --itemdb2sql"
+ CatchProcessErrors "map-server.exe" "$serverArgs --mobdb2sql"
+ CatchProcessErrors "map-server.exe" "$serverArgs --generate-translations"
+ CatchProcessErrors "map-server.exe" "$serverArgs --fix-md5"
+}