mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-24 16:43:37 +01:00
chore(merge): merge main into develop
Resolves CHANGELOG.md conflict: preserve develop's [Unreleased] block, insert the [9.3.1] section from main, remove the duplicate DisableSaveIntegrityCheck entry that had been in [Unreleased].
This commit is contained in:
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
|||||||
18
.github/workflows/go.yml
vendored
18
.github/workflows/go.yml
vendored
@@ -46,10 +46,10 @@ jobs:
|
|||||||
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
|
|
||||||
@@ -80,10 +80,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ jobs:
|
|||||||
run: env GOOS=linux GOARCH=amd64 go build -v
|
run: env GOOS=linux GOARCH=amd64 go build -v
|
||||||
|
|
||||||
- name: Upload Linux-amd64 artifacts
|
- name: Upload Linux-amd64 artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Linux-amd64
|
name: Linux-amd64
|
||||||
path: |
|
path: |
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
run: env GOOS=windows GOARCH=amd64 go build -v
|
run: env GOOS=windows GOARCH=amd64 go build -v
|
||||||
|
|
||||||
- name: Upload Windows-amd64 artifacts
|
- name: Upload Windows-amd64 artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Windows-amd64
|
name: Windows-amd64
|
||||||
path: |
|
path: |
|
||||||
@@ -125,15 +125,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
|
|
||||||
- name: Run golangci-lint
|
- name: Run golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v7
|
uses: golangci/golangci-lint-action@v9
|
||||||
with:
|
with:
|
||||||
version: v2.10.1
|
version: v2.10.1
|
||||||
args: --timeout=5m
|
args: --timeout=5m
|
||||||
|
|||||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -32,10 +32,10 @@ jobs:
|
|||||||
binary: erupe-ce.exe
|
binary: erupe-ce.exe
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
cd staging && zip -r ../erupe-${{ matrix.os_name }}.zip .
|
cd staging && zip -r ../erupe-${{ matrix.os_name }}.zip .
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os_name }}
|
name: ${{ matrix.os_name }}
|
||||||
path: erupe-${{ matrix.os_name }}.zip
|
path: erupe-${{ matrix.os_name }}.zip
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -108,10 +108,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
pattern: '*-amd64'
|
pattern: '*-amd64'
|
||||||
|
|||||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -17,7 +17,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- `saveutil` admin CLI (`cmd/saveutil/`): `import`, `export`, `grant-import`, and `revoke-import` commands for transferring character save data between server instances without touching the database manually.
|
- `saveutil` admin CLI (`cmd/saveutil/`): `import`, `export`, `grant-import`, and `revoke-import` commands for transferring character save data between server instances without touching the database manually.
|
||||||
- `POST /v2/characters/{id}/import` API endpoint: player-facing save import gated behind a one-time admin-granted token (generated by `saveutil grant-import`). Token expires after a configurable TTL (default 24 h).
|
- `POST /v2/characters/{id}/import` API endpoint: player-facing save import gated behind a one-time admin-granted token (generated by `saveutil grant-import`). Token expires after a configurable TTL (default 24 h).
|
||||||
- Database migration `0013_save_transfer`: adds `savedata_import_token` and `savedata_import_token_expiry` columns to the `characters` table.
|
- Database migration `0013_save_transfer`: adds `savedata_import_token` and `savedata_import_token_expiry` columns to the `characters` table.
|
||||||
- `DisableSaveIntegrityCheck` config flag: when `true`, the SHA-256 savedata integrity check is skipped on load. Intended for cross-server save transfers where the stored hash in the database does not match the imported save blob. Defaults to `false`. Affected characters can alternatively be unblocked per-character with `UPDATE characters SET savedata_hash = NULL WHERE id = <id>`.
|
|
||||||
- Guild scout invitations now use a dedicated `guild_invites` table (migration `0012_guild_invites`), giving each invitation a real serial PK; the scout list response now returns accurate invite IDs and timestamps, and `CancelGuildScout` uses the correct PK instead of the character ID.
|
- Guild scout invitations now use a dedicated `guild_invites` table (migration `0012_guild_invites`), giving each invitation a real serial PK; the scout list response now returns accurate invite IDs and timestamps, and `CancelGuildScout` uses the correct PK instead of the character ID.
|
||||||
- Event Tent (campaign) system: code redemption, stamp tracking, reward claiming, and quest gating for special event quests, backed by 8 new database tables and seeded with community-researched live-game campaign data ([#182](https://github.com/Mezeporta/Erupe/pull/182), by stratick).
|
- Event Tent (campaign) system: code redemption, stamp tracking, reward claiming, and quest gating for special event quests, backed by 8 new database tables and seeded with community-researched live-game campaign data ([#182](https://github.com/Mezeporta/Erupe/pull/182), by stratick).
|
||||||
- Database migration `0010_campaign` (campaigns, campaign_categories, campaign_category_links, campaign_rewards, campaign_rewards_claimed, campaign_state, campaign_codes, campaign_quest).
|
- Database migration `0010_campaign` (campaigns, campaign_categories, campaign_category_links, campaign_rewards, campaign_rewards_claimed, campaign_state, campaign_codes, campaign_quest).
|
||||||
@@ -32,6 +31,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Fixed backup recovery panic: `recoverFromBackups` now rejects decompressed backup data smaller than the minimum save layout size, preventing a slice-bounds panic when nullcomp passes through garbage bytes as "already decompressed" data ([#182](https://github.com/Mezeporta/Erupe/pull/182)).
|
- Fixed backup recovery panic: `recoverFromBackups` now rejects decompressed backup data smaller than the minimum save layout size, preventing a slice-bounds panic when nullcomp passes through garbage bytes as "already decompressed" data ([#182](https://github.com/Mezeporta/Erupe/pull/182)).
|
||||||
|
|
||||||
|
## [9.3.1] - 2026-03-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `DisableSaveIntegrityCheck` config flag: when `true`, the SHA-256 savedata integrity check is skipped on load.
|
||||||
|
Intended for cross-server save transfers where the stored hash in the database does not match the imported save blob.
|
||||||
|
Defaults to `false`.
|
||||||
|
Affected characters can alternatively be unblocked per-character with `UPDATE characters SET savedata_hash = NULL WHERE id = <id>`.
|
||||||
|
|
||||||
## [9.3.0] - 2026-03-19
|
## [9.3.0] - 2026-03-19
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"UploadQuality":100
|
"UploadQuality":100
|
||||||
},
|
},
|
||||||
"DeleteOnSaveCorruption": false,
|
"DeleteOnSaveCorruption": false,
|
||||||
|
"DisableSaveIntegrityCheck": false,
|
||||||
"ClientMode": "ZZ",
|
"ClientMode": "ZZ",
|
||||||
"QuestCacheExpiry": 300,
|
"QuestCacheExpiry": 300,
|
||||||
"CommandPrefix": "!",
|
"CommandPrefix": "!",
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ type Config struct {
|
|||||||
LoginNotices []string // MHFML string of the login notices displayed
|
LoginNotices []string // MHFML string of the login notices displayed
|
||||||
PatchServerManifest string // Manifest patch server override
|
PatchServerManifest string // Manifest patch server override
|
||||||
PatchServerFile string // File patch server override
|
PatchServerFile string // File patch server override
|
||||||
DeleteOnSaveCorruption bool // Attempts to save corrupted data will flag the save for deletion
|
DeleteOnSaveCorruption bool // Attempts to save corrupted data will flag the save for deletion
|
||||||
|
DisableSaveIntegrityCheck bool // Skip SHA-256 hash verification on load (needed for cross-server save transfers)
|
||||||
ClientMode string
|
ClientMode string
|
||||||
RealClientMode Mode
|
RealClientMode Mode
|
||||||
QuestCacheExpiry int // Number of seconds to keep quest data cached
|
QuestCacheExpiry int // Number of seconds to keep quest data cached
|
||||||
|
|||||||
@@ -150,7 +150,8 @@ func TestConfigStruct(t *testing.T) {
|
|||||||
LoginNotices: []string{"Welcome"},
|
LoginNotices: []string{"Welcome"},
|
||||||
PatchServerManifest: "http://patch.example.com/manifest",
|
PatchServerManifest: "http://patch.example.com/manifest",
|
||||||
PatchServerFile: "http://patch.example.com/files",
|
PatchServerFile: "http://patch.example.com/files",
|
||||||
DeleteOnSaveCorruption: false,
|
DeleteOnSaveCorruption: false,
|
||||||
|
DisableSaveIntegrityCheck: false,
|
||||||
ClientMode: "ZZ",
|
ClientMode: "ZZ",
|
||||||
RealClientMode: ZZ,
|
RealClientMode: ZZ,
|
||||||
QuestCacheExpiry: 3600,
|
QuestCacheExpiry: 3600,
|
||||||
|
|||||||
@@ -157,7 +157,8 @@ func TestConfigStructTypes(t *testing.T) {
|
|||||||
LoginNotices: []string{"Notice"},
|
LoginNotices: []string{"Notice"},
|
||||||
PatchServerManifest: "http://patch.example.com",
|
PatchServerManifest: "http://patch.example.com",
|
||||||
PatchServerFile: "http://files.example.com",
|
PatchServerFile: "http://files.example.com",
|
||||||
DeleteOnSaveCorruption: false,
|
DeleteOnSaveCorruption: false,
|
||||||
|
DisableSaveIntegrityCheck: false,
|
||||||
ClientMode: "ZZ",
|
ClientMode: "ZZ",
|
||||||
RealClientMode: ZZ,
|
RealClientMode: ZZ,
|
||||||
QuestCacheExpiry: 3600,
|
QuestCacheExpiry: 3600,
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -107,7 +107,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(fmt.Sprintf("Starting Erupe (9.3.0-%s)", Commit()))
|
logger.Info(fmt.Sprintf("Starting Erupe (9.3.1-%s)", Commit()))
|
||||||
logger.Info(fmt.Sprintf("Client Mode: %s (%d)", config.ClientMode, config.RealClientMode))
|
logger.Info(fmt.Sprintf("Client Mode: %s (%d)", config.ClientMode, config.RealClientMode))
|
||||||
|
|
||||||
if config.Database.Password == "" {
|
if config.Database.Password == "" {
|
||||||
|
|||||||
@@ -153,13 +153,11 @@ func TestClientConnection_GracefulLoginLogout(t *testing.T) {
|
|||||||
RawDataPayload: compressed,
|
RawDataPayload: compressed,
|
||||||
}
|
}
|
||||||
handleMsgMhfSavedata(session, savePkt)
|
handleMsgMhfSavedata(session, savePkt)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Client sends logout packet (graceful)
|
// Client sends logout packet (graceful)
|
||||||
t.Log("Client sending logout packet")
|
t.Log("Client sending logout packet")
|
||||||
logoutPkt := &mhfpacket.MsgSysLogout{}
|
logoutPkt := &mhfpacket.MsgSysLogout{}
|
||||||
handleMsgSysLogout(session, logoutPkt)
|
handleMsgSysLogout(session, logoutPkt)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify connection closed
|
// Verify connection closed
|
||||||
if !mockConn.IsClosed() {
|
if !mockConn.IsClosed() {
|
||||||
@@ -220,13 +218,11 @@ func TestClientConnection_UngracefulDisconnect(t *testing.T) {
|
|||||||
RawDataPayload: compressed,
|
RawDataPayload: compressed,
|
||||||
}
|
}
|
||||||
handleMsgMhfSavedata(session, savePkt)
|
handleMsgMhfSavedata(session, savePkt)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Simulate network failure - connection drops without logout packet
|
// Simulate network failure - connection drops without logout packet
|
||||||
t.Log("Simulating network failure (no logout packet sent)")
|
t.Log("Simulating network failure (no logout packet sent)")
|
||||||
// In real scenario, recvLoop would detect io.EOF and call logoutPlayer
|
// In real scenario, recvLoop would detect io.EOF and call logoutPlayer
|
||||||
logoutPlayer(session)
|
logoutPlayer(session)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify data was saved despite ungraceful disconnect
|
// Verify data was saved despite ungraceful disconnect
|
||||||
var savedCompressed []byte
|
var savedCompressed []byte
|
||||||
@@ -274,7 +270,6 @@ func TestClientConnection_SessionTimeout(t *testing.T) {
|
|||||||
RawDataPayload: compressed,
|
RawDataPayload: compressed,
|
||||||
}
|
}
|
||||||
handleMsgMhfSavedata(session, savePkt)
|
handleMsgMhfSavedata(session, savePkt)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Simulate timeout by setting lastPacket to long ago
|
// Simulate timeout by setting lastPacket to long ago
|
||||||
session.lastPacket = time.Now().Add(-35 * time.Second)
|
session.lastPacket = time.Now().Add(-35 * time.Second)
|
||||||
@@ -283,7 +278,6 @@ func TestClientConnection_SessionTimeout(t *testing.T) {
|
|||||||
// and call logoutPlayer(session)
|
// and call logoutPlayer(session)
|
||||||
t.Log("Session timed out (>30s since last packet)")
|
t.Log("Session timed out (>30s since last packet)")
|
||||||
logoutPlayer(session)
|
logoutPlayer(session)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify data saved
|
// Verify data saved
|
||||||
var savedCompressed []byte
|
var savedCompressed []byte
|
||||||
@@ -346,11 +340,9 @@ func TestClientConnection_MultipleClientsSimultaneous(t *testing.T) {
|
|||||||
RawDataPayload: compressed,
|
RawDataPayload: compressed,
|
||||||
}
|
}
|
||||||
handleMsgMhfSavedata(session, savePkt)
|
handleMsgMhfSavedata(session, savePkt)
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Graceful logout
|
// Graceful logout
|
||||||
logoutPlayer(session)
|
logoutPlayer(session)
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify individual client's data
|
// Verify individual client's data
|
||||||
var savedCompressed []byte
|
var savedCompressed []byte
|
||||||
@@ -416,12 +408,10 @@ func TestClientConnection_SaveDuringCombat(t *testing.T) {
|
|||||||
RawDataPayload: compressed,
|
RawDataPayload: compressed,
|
||||||
}
|
}
|
||||||
handleMsgMhfSavedata(session, savePkt)
|
handleMsgMhfSavedata(session, savePkt)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Disconnect while in stage
|
// Disconnect while in stage
|
||||||
t.Log("Player disconnects during quest")
|
t.Log("Player disconnects during quest")
|
||||||
logoutPlayer(session)
|
logoutPlayer(session)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify data saved even during combat
|
// Verify data saved even during combat
|
||||||
var savedCompressed []byte
|
var savedCompressed []byte
|
||||||
@@ -474,12 +464,10 @@ func TestClientConnection_ReconnectAfterCrash(t *testing.T) {
|
|||||||
RawDataPayload: compressed,
|
RawDataPayload: compressed,
|
||||||
}
|
}
|
||||||
handleMsgMhfSavedata(session1, savePkt)
|
handleMsgMhfSavedata(session1, savePkt)
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Client crashes (ungraceful disconnect)
|
// Client crashes (ungraceful disconnect)
|
||||||
t.Log("Client crashes (no logout packet)")
|
t.Log("Client crashes (no logout packet)")
|
||||||
logoutPlayer(session1)
|
logoutPlayer(session1)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Client reconnects immediately
|
// Client reconnects immediately
|
||||||
t.Log("Client reconnects after crash")
|
t.Log("Client reconnects after crash")
|
||||||
@@ -492,7 +480,6 @@ func TestClientConnection_ReconnectAfterCrash(t *testing.T) {
|
|||||||
AckHandle: 18001,
|
AckHandle: 18001,
|
||||||
}
|
}
|
||||||
handleMsgMhfLoaddata(session2, loadPkt)
|
handleMsgMhfLoaddata(session2, loadPkt)
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify data from before crash
|
// Verify data from before crash
|
||||||
var savedCompressed []byte
|
var savedCompressed []byte
|
||||||
|
|||||||
@@ -55,16 +55,23 @@ func GetCharacterSaveData(s *Session, charID uint32) (*CharacterSaveData, error)
|
|||||||
// Verify integrity checksum if one was stored with this save.
|
// Verify integrity checksum if one was stored with this save.
|
||||||
// A nil hash means the character was saved before checksums were introduced,
|
// A nil hash means the character was saved before checksums were introduced,
|
||||||
// so we skip verification (the next save will compute and store the hash).
|
// so we skip verification (the next save will compute and store the hash).
|
||||||
if storedHash != nil {
|
// DisableSaveIntegrityCheck bypasses this entirely for cross-server save transfers.
|
||||||
|
if storedHash != nil && !s.server.erupeConfig.DisableSaveIntegrityCheck {
|
||||||
computedHash := sha256.Sum256(saveData.decompSave)
|
computedHash := sha256.Sum256(saveData.decompSave)
|
||||||
if !bytes.Equal(storedHash, computedHash[:]) {
|
if !bytes.Equal(storedHash, computedHash[:]) {
|
||||||
s.logger.Error("Savedata integrity check failed: hash mismatch",
|
s.logger.Error("Savedata integrity check failed: hash mismatch — "+
|
||||||
|
"if this character was imported from another server, set DisableSaveIntegrityCheck=true in config.json "+
|
||||||
|
"or run: UPDATE characters SET savedata_hash = NULL WHERE id = <charID>",
|
||||||
zap.Uint32("charID", charID),
|
zap.Uint32("charID", charID),
|
||||||
zap.Binary("stored_hash", storedHash),
|
zap.Binary("stored_hash", storedHash),
|
||||||
zap.Binary("computed_hash", computedHash[:]),
|
zap.Binary("computed_hash", computedHash[:]),
|
||||||
)
|
)
|
||||||
return recoverFromBackups(s, saveData, charID)
|
return recoverFromBackups(s, saveData, charID)
|
||||||
}
|
}
|
||||||
|
} else if storedHash != nil && s.server.erupeConfig.DisableSaveIntegrityCheck {
|
||||||
|
s.logger.Warn("Savedata integrity check skipped (DisableSaveIntegrityCheck=true)",
|
||||||
|
zap.Uint32("charID", charID),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
saveData.updateStructWithSaveData()
|
saveData.updateStructWithSaveData()
|
||||||
|
|||||||
@@ -885,3 +885,68 @@ func TestGetCharacterSaveData_ConfigMode(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGetCharacterSaveData_IntegrityCheck verifies the SHA-256 hash guard and
|
||||||
|
// that DisableSaveIntegrityCheck bypasses it without returning an error.
|
||||||
|
func TestGetCharacterSaveData_IntegrityCheck(t *testing.T) {
|
||||||
|
// Build a minimal valid savedata blob and compress it.
|
||||||
|
rawSave := make([]byte, 150000)
|
||||||
|
copy(rawSave[88:], []byte("TestChar\x00"))
|
||||||
|
compressed, err := nullcomp.Compress(rawSave)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("compress: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hash that deliberately does NOT match rawSave.
|
||||||
|
wrongHash := bytes.Repeat([]byte{0xDE}, 32)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
disable bool
|
||||||
|
hash []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil hash skips check",
|
||||||
|
disable: false,
|
||||||
|
hash: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mismatched hash fails when check enabled",
|
||||||
|
disable: false,
|
||||||
|
hash: wrongHash,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mismatched hash passes when check disabled",
|
||||||
|
disable: true,
|
||||||
|
hash: wrongHash,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
mock := newMockCharacterRepo()
|
||||||
|
mock.loadSaveDataID = 1
|
||||||
|
mock.loadSaveDataData = compressed
|
||||||
|
mock.loadSaveDataName = "TestChar"
|
||||||
|
mock.loadSaveDataHash = tc.hash
|
||||||
|
|
||||||
|
server := createMockServer()
|
||||||
|
server.erupeConfig.RealClientMode = cfg.ZZ
|
||||||
|
server.erupeConfig.DisableSaveIntegrityCheck = tc.disable
|
||||||
|
server.charRepo = mock
|
||||||
|
session := createMockSession(1, server)
|
||||||
|
|
||||||
|
_, err := GetCharacterSaveData(session, 1)
|
||||||
|
if tc.wantErr && err == nil {
|
||||||
|
t.Error("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !tc.wantErr && err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ func TestRengokuData_SaveLoadRoundTrip_AcrossSessions(t *testing.T) {
|
|||||||
|
|
||||||
// Logout session 1
|
// Logout session 1
|
||||||
logoutPlayer(session1)
|
logoutPlayer(session1)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// === SESSION 2: Load data in new session ===
|
// === SESSION 2: Load data in new session ===
|
||||||
session2 := createTestSessionForServerWithChar(server, charID, "RengokuChar2")
|
session2 := createTestSessionForServerWithChar(server, charID, "RengokuChar2")
|
||||||
@@ -348,7 +347,6 @@ func TestRengokuData_SkillRegionPreserved(t *testing.T) {
|
|||||||
handleMsgMhfSaveRengokuData(session1, savePkt)
|
handleMsgMhfSaveRengokuData(session1, savePkt)
|
||||||
drainAck(t, session1)
|
drainAck(t, session1)
|
||||||
logoutPlayer(session1)
|
logoutPlayer(session1)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// === SESSION 2: Load and verify skill region ===
|
// === SESSION 2: Load and verify skill region ===
|
||||||
session2 := createTestSessionForServerWithChar(server, charID, "SkillChar")
|
session2 := createTestSessionForServerWithChar(server, charID, "SkillChar")
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package channelserver
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"erupe-ce/common/mhfitem"
|
"erupe-ce/common/mhfitem"
|
||||||
cfg "erupe-ce/config"
|
cfg "erupe-ce/config"
|
||||||
@@ -617,9 +616,6 @@ func TestPlateDataPersistenceDuringLogout(t *testing.T) {
|
|||||||
t.Log("Triggering logout via logoutPlayer")
|
t.Log("Triggering logout via logoutPlayer")
|
||||||
logoutPlayer(session)
|
logoutPlayer(session)
|
||||||
|
|
||||||
// Give logout time to complete
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// ===== VERIFICATION: Check all plate data was saved =====
|
// ===== VERIFICATION: Check all plate data was saved =====
|
||||||
t.Log("--- Verifying plate data persisted ---")
|
t.Log("--- Verifying plate data persisted ---")
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,13 @@ func IntegrationTest_PacketQueueFlow(t *testing.T) {
|
|||||||
|
|
||||||
done:
|
done:
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= tt.wantPackets {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentPackets := mock.GetSentPackets()
|
sentPackets := mock.GetSentPackets()
|
||||||
if len(sentPackets) != tt.wantPackets {
|
if len(sentPackets) != tt.wantPackets {
|
||||||
@@ -175,7 +181,13 @@ func IntegrationTest_ConcurrentQueueing(t *testing.T) {
|
|||||||
|
|
||||||
done:
|
done:
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= expectedTotal {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentPackets := mock.GetSentPackets()
|
sentPackets := mock.GetSentPackets()
|
||||||
if len(sentPackets) != expectedTotal {
|
if len(sentPackets) != expectedTotal {
|
||||||
@@ -237,9 +249,14 @@ func IntegrationTest_AckPacketFlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for ACKs to be sent
|
// Wait for ACKs to be sent
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= ackCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentPackets := mock.GetSentPackets()
|
sentPackets := mock.GetSentPackets()
|
||||||
if len(sentPackets) != ackCount {
|
if len(sentPackets) != ackCount {
|
||||||
@@ -307,9 +324,14 @@ func IntegrationTest_MixedPacketTypes(t *testing.T) {
|
|||||||
s.QueueSendNonBlocking([]byte{0x00, 0x03, 0xEE})
|
s.QueueSendNonBlocking([]byte{0x00, 0x03, 0xEE})
|
||||||
|
|
||||||
// Wait for all packets
|
// Wait for all packets
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= 4 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentPackets := mock.GetSentPackets()
|
sentPackets := mock.GetSentPackets()
|
||||||
if len(sentPackets) != 4 {
|
if len(sentPackets) != 4 {
|
||||||
@@ -357,9 +379,14 @@ func IntegrationTest_PacketOrderPreservation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for packets
|
// Wait for packets
|
||||||
time.Sleep(300 * time.Millisecond)
|
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= packetCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentPackets := mock.GetSentPackets()
|
sentPackets := mock.GetSentPackets()
|
||||||
if len(sentPackets) != packetCount {
|
if len(sentPackets) != packetCount {
|
||||||
@@ -423,9 +450,14 @@ func IntegrationTest_QueueBackpressure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for processing
|
// Wait for processing
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
// Some packets should have been sent
|
// Some packets should have been sent
|
||||||
sentCount := mock.PacketCount()
|
sentCount := mock.PacketCount()
|
||||||
@@ -502,7 +534,13 @@ func IntegrationTest_GuildEnumerationFlow(t *testing.T) {
|
|||||||
|
|
||||||
done:
|
done:
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= tt.guildCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentPackets := mock.GetSentPackets()
|
sentPackets := mock.GetSentPackets()
|
||||||
if len(sentPackets) != tt.guildCount {
|
if len(sentPackets) != tt.guildCount {
|
||||||
@@ -571,9 +609,21 @@ func IntegrationTest_ConcurrentClientAccess(t *testing.T) {
|
|||||||
s.QueueSend(testData)
|
s.QueueSend(testData)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= tt.packetsPerClient {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline = time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= tt.packetsPerClient {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentCount := mock.PacketCount()
|
sentCount := mock.PacketCount()
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -638,9 +688,21 @@ func IntegrationTest_ClientVersionCompatibility(t *testing.T) {
|
|||||||
testData := []byte{0x00, 0x01, 0xAA, 0xBB}
|
testData := []byte{0x00, 0x01, 0xAA, 0xBB}
|
||||||
s.QueueSend(testData)
|
s.QueueSend(testData)
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline = time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentCount := mock.PacketCount()
|
sentCount := mock.PacketCount()
|
||||||
if (sentCount > 0) != tt.shouldSucceed {
|
if (sentCount > 0) != tt.shouldSucceed {
|
||||||
@@ -674,9 +736,14 @@ func IntegrationTest_PacketPrioritization(t *testing.T) {
|
|||||||
s.QueueSend([]byte{0x00, byte(i), 0xDD})
|
s.QueueSend([]byte{0x00, byte(i), 0xDD})
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= 10 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentPackets := mock.GetSentPackets()
|
sentPackets := mock.GetSentPackets()
|
||||||
if len(sentPackets) < 10 {
|
if len(sentPackets) < 10 {
|
||||||
@@ -732,7 +799,13 @@ func IntegrationTest_DataIntegrityUnderLoad(t *testing.T) {
|
|||||||
|
|
||||||
done:
|
done:
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= packetCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentPackets := mock.GetSentPackets()
|
sentPackets := mock.GetSentPackets()
|
||||||
if len(sentPackets) != packetCount {
|
if len(sentPackets) != packetCount {
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ type mockCharacterRepo struct {
|
|||||||
loadSaveDataData []byte
|
loadSaveDataData []byte
|
||||||
loadSaveDataNew bool
|
loadSaveDataNew bool
|
||||||
loadSaveDataName string
|
loadSaveDataName string
|
||||||
|
loadSaveDataHash []byte
|
||||||
loadSaveDataErr error
|
loadSaveDataErr error
|
||||||
|
|
||||||
// ReadEtcPoints mock fields
|
// ReadEtcPoints mock fields
|
||||||
@@ -245,7 +246,7 @@ func (m *mockCharacterRepo) SaveBackup(_ uint32, _ int, _ []byte) error {
|
|||||||
func (m *mockCharacterRepo) GetLastBackupTime(_ uint32) (time.Time, error) { return time.Time{}, nil }
|
func (m *mockCharacterRepo) GetLastBackupTime(_ uint32) (time.Time, error) { return time.Time{}, nil }
|
||||||
func (m *mockCharacterRepo) SaveCharacterDataAtomic(_ SaveAtomicParams) error { return nil }
|
func (m *mockCharacterRepo) SaveCharacterDataAtomic(_ SaveAtomicParams) error { return nil }
|
||||||
func (m *mockCharacterRepo) LoadSaveDataWithHash(_ uint32) (uint32, []byte, bool, string, []byte, error) {
|
func (m *mockCharacterRepo) LoadSaveDataWithHash(_ uint32) (uint32, []byte, bool, string, []byte, error) {
|
||||||
return m.loadSaveDataID, m.loadSaveDataData, m.loadSaveDataNew, m.loadSaveDataName, nil, m.loadSaveDataErr
|
return m.loadSaveDataID, m.loadSaveDataData, m.loadSaveDataNew, m.loadSaveDataName, m.loadSaveDataHash, m.loadSaveDataErr
|
||||||
}
|
}
|
||||||
func (m *mockCharacterRepo) LoadBackupsByRecency(_ uint32) ([]SavedataBackup, error) {
|
func (m *mockCharacterRepo) LoadBackupsByRecency(_ uint32) ([]SavedataBackup, error) {
|
||||||
return []SavedataBackup{}, nil
|
return []SavedataBackup{}, nil
|
||||||
|
|||||||
@@ -155,13 +155,11 @@ func TestMonitored_SaveHandlerInvocationDuringLogout(t *testing.T) {
|
|||||||
t.Log("Calling handleMsgMhfSavedata during session")
|
t.Log("Calling handleMsgMhfSavedata during session")
|
||||||
handleMsgMhfSavedata(session, savePkt)
|
handleMsgMhfSavedata(session, savePkt)
|
||||||
monitor.RecordSavedata()
|
monitor.RecordSavedata()
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Now trigger logout
|
// Now trigger logout
|
||||||
t.Log("Triggering logout - monitoring if save handlers are called")
|
t.Log("Triggering logout - monitoring if save handlers are called")
|
||||||
monitor.RecordLogout()
|
monitor.RecordLogout()
|
||||||
logoutPlayer(session)
|
logoutPlayer(session)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Report statistics
|
// Report statistics
|
||||||
t.Log(monitor.GetStats())
|
t.Log(monitor.GetStats())
|
||||||
@@ -233,12 +231,10 @@ func TestWithLogging_LogoutFlowAnalysis(t *testing.T) {
|
|||||||
RawDataPayload: compressed,
|
RawDataPayload: compressed,
|
||||||
}
|
}
|
||||||
handleMsgMhfSavedata(session, savePkt)
|
handleMsgMhfSavedata(session, savePkt)
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Trigger logout
|
// Trigger logout
|
||||||
t.Log("Triggering logout with logging enabled")
|
t.Log("Triggering logout with logging enabled")
|
||||||
logoutPlayer(session)
|
logoutPlayer(session)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Analyze logs
|
// Analyze logs
|
||||||
allLogs := logs.All()
|
allLogs := logs.All()
|
||||||
@@ -317,11 +313,9 @@ func TestConcurrent_MultipleSessionsSaving(t *testing.T) {
|
|||||||
RawDataPayload: compressed,
|
RawDataPayload: compressed,
|
||||||
}
|
}
|
||||||
handleMsgMhfSavedata(session, savePkt)
|
handleMsgMhfSavedata(session, savePkt)
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Logout
|
// Logout
|
||||||
logoutPlayer(session)
|
logoutPlayer(session)
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify data saved
|
// Verify data saved
|
||||||
var savedCompressed []byte
|
var savedCompressed []byte
|
||||||
@@ -376,11 +370,9 @@ func TestSequential_RepeatedLogoutLoginCycles(t *testing.T) {
|
|||||||
RawDataPayload: compressed,
|
RawDataPayload: compressed,
|
||||||
}
|
}
|
||||||
handleMsgMhfSavedata(session, savePkt)
|
handleMsgMhfSavedata(session, savePkt)
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Logout
|
// Logout
|
||||||
logoutPlayer(session)
|
logoutPlayer(session)
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify data after each cycle
|
// Verify data after each cycle
|
||||||
var savedCompressed []byte
|
var savedCompressed []byte
|
||||||
@@ -452,7 +444,6 @@ func TestRealtime_SaveDataTimestamps(t *testing.T) {
|
|||||||
events = append(events, SaveEvent{time.Now(), "logout_start"})
|
events = append(events, SaveEvent{time.Now(), "logout_start"})
|
||||||
logoutPlayer(session)
|
logoutPlayer(session)
|
||||||
events = append(events, SaveEvent{time.Now(), "logout_end"})
|
events = append(events, SaveEvent{time.Now(), "logout_end"})
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Print timeline
|
// Print timeline
|
||||||
t.Log("Save event timeline:")
|
t.Log("Save event timeline:")
|
||||||
|
|||||||
@@ -84,16 +84,10 @@ func TestSessionLifecycle_BasicSaveLoadCycle(t *testing.T) {
|
|||||||
t.Log("Sending savedata packet")
|
t.Log("Sending savedata packet")
|
||||||
handleMsgMhfSavedata(session1, savePkt)
|
handleMsgMhfSavedata(session1, savePkt)
|
||||||
|
|
||||||
// Drain ACK
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Now trigger logout via the actual logout flow
|
// Now trigger logout via the actual logout flow
|
||||||
t.Log("Triggering logout via logoutPlayer")
|
t.Log("Triggering logout via logoutPlayer")
|
||||||
logoutPlayer(session1)
|
logoutPlayer(session1)
|
||||||
|
|
||||||
// Give logout time to complete
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// ===== SESSION 2: Login again and verify data =====
|
// ===== SESSION 2: Login again and verify data =====
|
||||||
t.Log("--- Starting Session 2: Login and verify data persists ---")
|
t.Log("--- Starting Session 2: Login and verify data persists ---")
|
||||||
|
|
||||||
@@ -106,8 +100,6 @@ func TestSessionLifecycle_BasicSaveLoadCycle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
handleMsgMhfLoaddata(session2, loadPkt)
|
handleMsgMhfLoaddata(session2, loadPkt)
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify savedata persisted
|
// Verify savedata persisted
|
||||||
var savedCompressed []byte
|
var savedCompressed []byte
|
||||||
err = db.QueryRow("SELECT savedata FROM characters WHERE id = $1", charID).Scan(&savedCompressed)
|
err = db.QueryRow("SELECT savedata FROM characters WHERE id = $1", charID).Scan(&savedCompressed)
|
||||||
@@ -189,7 +181,6 @@ func TestSessionLifecycle_WarehouseDataPersistence(t *testing.T) {
|
|||||||
|
|
||||||
// Logout
|
// Logout
|
||||||
logoutPlayer(session1)
|
logoutPlayer(session1)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// ===== SESSION 2: Verify warehouse contents =====
|
// ===== SESSION 2: Verify warehouse contents =====
|
||||||
session2 := createTestSessionForServerWithChar(server, charID, "WarehouseChar")
|
session2 := createTestSessionForServerWithChar(server, charID, "WarehouseChar")
|
||||||
@@ -240,7 +231,6 @@ func TestSessionLifecycle_KoryoPointsPersistence(t *testing.T) {
|
|||||||
|
|
||||||
t.Logf("Adding %d Koryo points", addPoints)
|
t.Logf("Adding %d Koryo points", addPoints)
|
||||||
handleMsgMhfAddKouryouPoint(session1, pkt)
|
handleMsgMhfAddKouryouPoint(session1, pkt)
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify points were added in session 1
|
// Verify points were added in session 1
|
||||||
var points1 uint32
|
var points1 uint32
|
||||||
@@ -252,7 +242,6 @@ func TestSessionLifecycle_KoryoPointsPersistence(t *testing.T) {
|
|||||||
|
|
||||||
// Logout
|
// Logout
|
||||||
logoutPlayer(session1)
|
logoutPlayer(session1)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// ===== SESSION 2: Verify Koryo points persist =====
|
// ===== SESSION 2: Verify Koryo points persist =====
|
||||||
session2 := createTestSessionForServerWithChar(server, charID, "KoryoChar")
|
session2 := createTestSessionForServerWithChar(server, charID, "KoryoChar")
|
||||||
@@ -341,14 +330,10 @@ func TestSessionLifecycle_MultipleDataTypesPersistence(t *testing.T) {
|
|||||||
}
|
}
|
||||||
handleMsgMhfSavedata(session1, savePkt)
|
handleMsgMhfSavedata(session1, savePkt)
|
||||||
|
|
||||||
// Give handlers time to process
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
t.Log("Modified all data types in session 1")
|
t.Log("Modified all data types in session 1")
|
||||||
|
|
||||||
// Logout
|
// Logout
|
||||||
logoutPlayer(session1)
|
logoutPlayer(session1)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// ===== SESSION 2: Verify all data persists =====
|
// ===== SESSION 2: Verify all data persists =====
|
||||||
session2 := createTestSessionForServerWithChar(server, charID, "MultiChar")
|
session2 := createTestSessionForServerWithChar(server, charID, "MultiChar")
|
||||||
@@ -358,7 +343,6 @@ func TestSessionLifecycle_MultipleDataTypesPersistence(t *testing.T) {
|
|||||||
AckHandle: 5001,
|
AckHandle: 5001,
|
||||||
}
|
}
|
||||||
handleMsgMhfLoaddata(session2, loadPkt)
|
handleMsgMhfLoaddata(session2, loadPkt)
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
allPassed := true
|
allPassed := true
|
||||||
|
|
||||||
@@ -472,13 +456,11 @@ func TestSessionLifecycle_DisconnectWithoutLogout(t *testing.T) {
|
|||||||
RawDataPayload: compressed,
|
RawDataPayload: compressed,
|
||||||
}
|
}
|
||||||
handleMsgMhfSavedata(session1, savePkt)
|
handleMsgMhfSavedata(session1, savePkt)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Simulate disconnect by calling logoutPlayer (which is called by recvLoop on EOF)
|
// Simulate disconnect by calling logoutPlayer (which is called by recvLoop on EOF)
|
||||||
// In real scenario, this is triggered by connection close
|
// In real scenario, this is triggered by connection close
|
||||||
t.Log("Simulating ungraceful disconnect")
|
t.Log("Simulating ungraceful disconnect")
|
||||||
logoutPlayer(session1)
|
logoutPlayer(session1)
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// ===== SESSION 2: Verify data saved despite ungraceful disconnect =====
|
// ===== SESSION 2: Verify data saved despite ungraceful disconnect =====
|
||||||
session2 := createTestSessionForServerWithChar(server, charID, "DisconnectChar")
|
session2 := createTestSessionForServerWithChar(server, charID, "DisconnectChar")
|
||||||
@@ -544,7 +526,6 @@ func TestSessionLifecycle_RapidReconnect(t *testing.T) {
|
|||||||
|
|
||||||
// Logout quickly
|
// Logout quickly
|
||||||
logoutPlayer(session)
|
logoutPlayer(session)
|
||||||
time.Sleep(30 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify points persisted
|
// Verify points persisted
|
||||||
var loadedPoints uint32
|
var loadedPoints uint32
|
||||||
|
|||||||
@@ -279,13 +279,26 @@ func TestBroadcastMHFAllSessions(t *testing.T) {
|
|||||||
testPkt := &mhfpacket.MsgSysNop{}
|
testPkt := &mhfpacket.MsgSysNop{}
|
||||||
server.BroadcastMHF(testPkt, nil)
|
server.BroadcastMHF(testPkt, nil)
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
// Poll until all sessions have received the packet or the deadline is reached.
|
||||||
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
receivedCount := 0
|
||||||
|
for _, sess := range server.sessions {
|
||||||
|
mock := sess.cryptConn.(*MockCryptConn)
|
||||||
|
if mock.PacketCount() > 0 {
|
||||||
|
receivedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if receivedCount == sessionCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
// Stop all sessions
|
// Stop all sessions
|
||||||
for _, sess := range sessions {
|
for _, sess := range sessions {
|
||||||
sess.closed.Store(true)
|
sess.closed.Store(true)
|
||||||
}
|
}
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify all sessions received the packet
|
// Verify all sessions received the packet
|
||||||
receivedCount := 0
|
receivedCount := 0
|
||||||
|
|||||||
@@ -116,11 +116,23 @@ func TestPacketQueueIndividualSending(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for packets to be processed
|
// Wait for packets to be processed
|
||||||
time.Sleep(100 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= tt.wantPackets {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
// Stop the session
|
// Stop the session
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline = time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= tt.wantPackets {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
// Verify packet count
|
// Verify packet count
|
||||||
sentPackets := mock.GetSentPackets()
|
sentPackets := mock.GetSentPackets()
|
||||||
@@ -165,9 +177,21 @@ func TestPacketQueueNoConcatenation(t *testing.T) {
|
|||||||
s.sendPackets <- packet{packet2, true}
|
s.sendPackets <- packet{packet2, true}
|
||||||
s.sendPackets <- packet{packet3, true}
|
s.sendPackets <- packet{packet3, true}
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= 3 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline = time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= 3 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentPackets := mock.GetSentPackets()
|
sentPackets := mock.GetSentPackets()
|
||||||
|
|
||||||
@@ -204,7 +228,7 @@ func TestQueueSendUsesQueue(t *testing.T) {
|
|||||||
s.QueueSend(testData)
|
s.QueueSend(testData)
|
||||||
|
|
||||||
// Give it a moment
|
// Give it a moment
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
|
||||||
// WITHOUT sendLoop running, packets should NOT be sent yet
|
// WITHOUT sendLoop running, packets should NOT be sent yet
|
||||||
if mock.PacketCount() > 0 {
|
if mock.PacketCount() > 0 {
|
||||||
@@ -218,7 +242,13 @@ func TestQueueSendUsesQueue(t *testing.T) {
|
|||||||
|
|
||||||
// Now start sendLoop and verify it gets sent
|
// Now start sendLoop and verify it gets sent
|
||||||
go s.sendLoop()
|
go s.sendLoop()
|
||||||
time.Sleep(100 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
if mock.PacketCount() != 1 {
|
if mock.PacketCount() != 1 {
|
||||||
t.Errorf("expected 1 packet sent after sendLoop, got %d", mock.PacketCount())
|
t.Errorf("expected 1 packet sent after sendLoop, got %d", mock.PacketCount())
|
||||||
@@ -237,9 +267,21 @@ func TestPacketTerminatorFormat(t *testing.T) {
|
|||||||
testData := []byte{0x00, 0x01, 0xAA, 0xBB}
|
testData := []byte{0x00, 0x01, 0xAA, 0xBB}
|
||||||
s.sendPackets <- packet{testData, true}
|
s.sendPackets <- packet{testData, true}
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline = time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentPackets := mock.GetSentPackets()
|
sentPackets := mock.GetSentPackets()
|
||||||
if len(sentPackets) != 1 {
|
if len(sentPackets) != 1 {
|
||||||
@@ -313,9 +355,21 @@ func TestPacketQueueAckFormat(t *testing.T) {
|
|||||||
ackData := []byte{0xAA, 0xBB, 0xCC, 0xDD}
|
ackData := []byte{0xAA, 0xBB, 0xCC, 0xDD}
|
||||||
s.QueueAck(ackHandle, ackData)
|
s.QueueAck(ackHandle, ackData)
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
s.closed.Store(true)
|
s.closed.Store(true)
|
||||||
time.Sleep(50 * time.Millisecond)
|
deadline = time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if mock.PacketCount() >= 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
sentPackets := mock.GetSentPackets()
|
sentPackets := mock.GetSentPackets()
|
||||||
if len(sentPackets) != 1 {
|
if len(sentPackets) != 1 {
|
||||||
|
|||||||
Reference in New Issue
Block a user