Files
Erupe/server/channelserver/repo_interfaces.go
Houmgaor 2bd92c9ae7 feat(diva): implement Diva Defense (UD) system
Add full Diva Defense / United Defense system: schema, repo layer,
i18n bead names, and RE-verified packet handler implementations.

Schema (0011_diva.sql): diva_beads, diva_beads_assignment,
diva_beads_points, diva_prizes tables; interception_maps/points
columns on guilds and guild_characters.

Seed (DivaDefaults.sql): 26 prize milestones for personal and
guild reward tracks (item_type=26 diva coins).

Repo (DivaRepo): 11 new methods covering bead assignment, point
accumulation, interception point tracking, prize queries, and
cleanup. Mocks wired in test_helpers_test.go.

i18n: Bead struct with EN/JP names for all 18 bead types (IDs
1–25). Session tracks currentBeadIndex (-1 = none assigned).

Packet handlers corrected against mhfo-hd.dll RE findings:
- GetKijuInfo: u8 count, 512-byte desc, color_id+bead_type per entry
- SetKiju: 1-byte ACK; persists bead assignment to DB
- GetUdMyPoint: 8×18-byte entries, no count prefix
- GetUdTotalPointInfo: u8 error + u64[64] + u8[64] + u64 (~585 B)
- GetUdSelectedColorInfo: u8 error + u8[8] = 9 bytes
- GetUdDailyPresentList: correct u16 count format (was wrong hex)
- GetUdNormaPresentList: correct u16 count format (was wrong hex)
- GetUdRankingRewardList: correct u16 count with u32 item_id/qty
- GetRewardSong: 22-byte layout with 0xFFFFFFFF prayer_end sentinel
- AddRewardSongCount: parse implemented (was NOT IMPLEMENTED stub)
2026-03-20 17:52:01 +01:00

383 lines
18 KiB
Go

package channelserver
import (
"time"
)
// Repository interfaces decouple handlers from concrete PostgreSQL implementations,
// enabling mock/stub injection for unit tests and alternative storage backends.
// CharacterRepo defines the contract for character data access.
type CharacterRepo interface {
LoadColumn(charID uint32, column string) ([]byte, error)
SaveColumn(charID uint32, column string, data []byte) error
ReadInt(charID uint32, column string) (int, error)
AdjustInt(charID uint32, column string, delta int) (int, error)
GetName(charID uint32) (string, error)
GetUserID(charID uint32) (uint32, error)
UpdateLastLogin(charID uint32, timestamp int64) error
UpdateTimePlayed(charID uint32, timePlayed int) error
GetCharIDsByUserID(userID uint32) ([]uint32, error)
ReadTime(charID uint32, column string, defaultVal time.Time) (time.Time, error)
SaveTime(charID uint32, column string, value time.Time) error
SaveInt(charID uint32, column string, value int) error
SaveBool(charID uint32, column string, value bool) error
SaveString(charID uint32, column string, value string) error
ReadBool(charID uint32, column string) (bool, error)
ReadString(charID uint32, column string) (string, error)
LoadColumnWithDefault(charID uint32, column string, defaultVal []byte) ([]byte, error)
SetDeleted(charID uint32) error
UpdateDailyCafe(charID uint32, dailyTime time.Time, bonusQuests, dailyQuests uint32) error
ResetDailyQuests(charID uint32) error
ReadEtcPoints(charID uint32) (bonusQuests, dailyQuests, promoPoints uint32, err error)
ResetCafeTime(charID uint32, cafeReset time.Time) error
UpdateGuildPostChecked(charID uint32) error
ReadGuildPostChecked(charID uint32) (time.Time, error)
SaveMercenary(charID uint32, data []byte, rastaID uint32) error
UpdateGCPAndPact(charID uint32, gcp uint32, pactID uint32) error
FindByRastaID(rastaID int) (charID uint32, name string, err error)
SaveCharacterData(charID uint32, compSave []byte, hr, gr uint16, isFemale bool, weaponType uint8, weaponID uint16) error
SaveHouseData(charID uint32, houseTier []byte, houseData, bookshelf, gallery, tore, garden []byte) error
LoadSaveData(charID uint32) (uint32, []byte, bool, string, error)
SaveBackup(charID uint32, slot int, data []byte) error
GetLastBackupTime(charID uint32) (time.Time, error)
// SaveCharacterDataAtomic performs all save-related writes in a single
// database transaction: character data, house data, checksum, and
// optionally a backup snapshot. If any step fails, everything is rolled back.
SaveCharacterDataAtomic(params SaveAtomicParams) error
// LoadSaveDataWithHash loads savedata along with its stored SHA-256 hash.
// The hash may be nil for characters saved before checksums were introduced.
LoadSaveDataWithHash(charID uint32) (id uint32, savedata []byte, isNew bool, name string, hash []byte, err error)
// LoadBackupsByRecency returns all backup slots for a character ordered
// most-recent first. Returns an empty slice if no backups exist.
LoadBackupsByRecency(charID uint32) ([]SavedataBackup, error)
}
// GuildRepo defines the contract for guild data access.
type GuildRepo interface {
GetByID(guildID uint32) (*Guild, error)
GetByCharID(charID uint32) (*Guild, error)
ListAll() ([]*Guild, error)
Create(leaderCharID uint32, guildName string) (int32, error)
Save(guild *Guild) error
Disband(guildID uint32) error
RemoveCharacter(charID uint32) error
AcceptApplication(guildID, charID uint32) error
CreateApplication(guildID, charID, actorID uint32, appType GuildApplicationType) error
CreateApplicationWithMail(guildID, charID, actorID uint32, appType GuildApplicationType, mailSenderID, mailRecipientID uint32, mailSubject, mailBody string) error
CancelInvitation(guildID, charID uint32) error
RejectApplication(guildID, charID uint32) error
ArrangeCharacters(charIDs []uint32) error
GetApplication(guildID, charID uint32, appType GuildApplicationType) (*GuildApplication, error)
HasApplication(guildID, charID uint32) (bool, error)
GetItemBox(guildID uint32) ([]byte, error)
SaveItemBox(guildID uint32, data []byte) error
GetMembers(guildID uint32, applicants bool) ([]*GuildMember, error)
GetCharacterMembership(charID uint32) (*GuildMember, error)
SaveMember(member *GuildMember) error
SetRecruiting(guildID uint32, recruiting bool) error
SetPugiOutfits(guildID uint32, outfits uint32) error
SetRecruiter(charID uint32, allowed bool) error
AddMemberDailyRP(charID uint32, amount uint16) error
ExchangeEventRP(guildID uint32, amount uint16) (uint32, error)
AddRankRP(guildID uint32, amount uint16) error
AddEventRP(guildID uint32, amount uint16) error
GetRoomRP(guildID uint32) (uint16, error)
SetRoomRP(guildID uint32, rp uint16) error
AddRoomRP(guildID uint32, amount uint16) error
SetRoomExpiry(guildID uint32, expiry time.Time) error
ListPosts(guildID uint32, postType int) ([]*MessageBoardPost, error)
CreatePost(guildID, authorID, stampID uint32, postType int, title, body string, maxPosts int) error
DeletePost(postID uint32) error
UpdatePost(postID uint32, title, body string) error
UpdatePostStamp(postID, stampID uint32) error
GetPostLikedBy(postID uint32) (string, error)
SetPostLikedBy(postID uint32, likedBy string) error
CountNewPosts(guildID uint32, since time.Time) (int, error)
GetAllianceByID(allianceID uint32) (*GuildAlliance, error)
ListAlliances() ([]*GuildAlliance, error)
CreateAlliance(name string, parentGuildID uint32) error
DeleteAlliance(allianceID uint32) error
RemoveGuildFromAlliance(allianceID, guildID, subGuild1ID, subGuild2ID uint32) error
SetAllianceRecruiting(allianceID uint32, recruiting bool) error
ListAdventures(guildID uint32) ([]*GuildAdventure, error)
CreateAdventure(guildID, destination uint32, depart, returnTime int64) error
CreateAdventureWithCharge(guildID, destination, charge uint32, depart, returnTime int64) error
CollectAdventure(adventureID uint32, charID uint32) error
ChargeAdventure(adventureID uint32, amount uint32) error
GetPendingHunt(charID uint32) (*TreasureHunt, error)
ListGuildHunts(guildID, charID uint32) ([]*TreasureHunt, error)
CreateHunt(guildID, hostID, destination, level uint32, huntData []byte, catsUsed string) error
AcquireHunt(huntID uint32) error
RegisterHuntReport(huntID, charID uint32) error
CollectHunt(huntID uint32) error
ClaimHuntReward(huntID, charID uint32) error
ListMeals(guildID uint32) ([]*GuildMeal, error)
CreateMeal(guildID, mealID, level uint32, createdAt time.Time) (uint32, error)
UpdateMeal(mealID, newMealID, level uint32, createdAt time.Time) error
ClaimHuntBox(charID uint32, claimedAt time.Time) error
ListGuildKills(guildID, charID uint32) ([]*GuildKill, error)
CountGuildKills(guildID, charID uint32) (int, error)
ClearTreasureHunt(charID uint32) error
InsertKillLog(charID uint32, monster int, quantity uint8, timestamp time.Time) error
ListInvitedCharacters(guildID uint32) ([]*ScoutedCharacter, error)
RolloverDailyRP(guildID uint32, noon time.Time) error
AddWeeklyBonusUsers(guildID uint32, numUsers uint8) error
}
// UserRepo defines the contract for user account data access.
type UserRepo interface {
GetGachaPoints(userID uint32) (fp, premium, trial uint32, err error)
GetTrialCoins(userID uint32) (uint16, error)
DeductTrialCoins(userID uint32, amount uint32) error
DeductPremiumCoins(userID uint32, amount uint32) error
AddPremiumCoins(userID uint32, amount uint32) error
AddTrialCoins(userID uint32, amount uint32) error
DeductFrontierPoints(userID uint32, amount uint32) error
AddFrontierPoints(userID uint32, amount uint32) error
AdjustFrontierPointsDeduct(userID uint32, amount int) (uint32, error)
AdjustFrontierPointsCredit(userID uint32, amount int) (uint32, error)
AddFrontierPointsFromGacha(userID uint32, gachaID uint32, entryType uint8) error
GetRights(userID uint32) (uint32, error)
SetRights(userID uint32, rights uint32) error
IsOp(userID uint32) (bool, error)
SetLastCharacter(userID uint32, charID uint32) error
GetTimer(userID uint32) (bool, error)
SetTimer(userID uint32, value bool) error
CountByPSNID(psnID string) (int, error)
SetPSNID(userID uint32, psnID string) error
GetDiscordToken(userID uint32) (string, error)
SetDiscordToken(userID uint32, token string) error
GetItemBox(userID uint32) ([]byte, error)
SetItemBox(userID uint32, data []byte) error
LinkDiscord(discordID string, token string) (string, error)
SetPasswordByDiscordID(discordID string, hash []byte) error
GetByIDAndUsername(charID uint32) (userID uint32, username string, err error)
BanUser(userID uint32, expires *time.Time) error
}
// GachaRepo defines the contract for gacha system data access.
type GachaRepo interface {
GetEntryForTransaction(gachaID uint32, rollID uint8) (itemType uint8, itemNumber uint16, rolls int, err error)
GetRewardPool(gachaID uint32) ([]GachaEntry, error)
GetItemsForEntry(entryID uint32) ([]GachaItem, error)
GetGuaranteedItems(rollType uint8, gachaID uint32) ([]GachaItem, error)
GetStepupStep(gachaID uint32, charID uint32) (uint8, error)
GetStepupWithTime(gachaID uint32, charID uint32) (uint8, time.Time, error)
HasEntryType(gachaID uint32, entryType uint8) (bool, error)
DeleteStepup(gachaID uint32, charID uint32) error
InsertStepup(gachaID uint32, step uint8, charID uint32) error
GetBoxEntryIDs(gachaID uint32, charID uint32) ([]uint32, error)
InsertBoxEntry(gachaID uint32, entryID uint32, charID uint32) error
DeleteBoxEntries(gachaID uint32, charID uint32) error
ListShop() ([]Gacha, error)
GetShopType(shopID uint32) (int, error)
GetAllEntries(gachaID uint32) ([]GachaEntry, error)
GetWeightDivisor(gachaID uint32) (float64, error)
}
// HouseRepo defines the contract for house/housing data access.
type HouseRepo interface {
UpdateInterior(charID uint32, data []byte) error
GetHouseByCharID(charID uint32) (HouseData, error)
SearchHousesByName(name string) ([]HouseData, error)
UpdateHouseState(charID uint32, state uint8, password string) error
GetHouseAccess(charID uint32) (state uint8, password string, err error)
GetHouseContents(charID uint32) (houseTier, houseData, houseFurniture, bookshelf, gallery, tore, garden []byte, err error)
GetMission(charID uint32) ([]byte, error)
UpdateMission(charID uint32, data []byte) error
InitializeWarehouse(charID uint32) error
GetWarehouseNames(charID uint32) (itemNames, equipNames [10]string, err error)
RenameWarehouseBox(charID uint32, boxType uint8, boxIndex uint8, name string) error
GetWarehouseItemData(charID uint32, index uint8) ([]byte, error)
SetWarehouseItemData(charID uint32, index uint8, data []byte) error
GetWarehouseEquipData(charID uint32, index uint8) ([]byte, error)
SetWarehouseEquipData(charID uint32, index uint8, data []byte) error
GetTitles(charID uint32) ([]Title, error)
AcquireTitle(titleID uint16, charID uint32) error
}
// FestaRepo defines the contract for festa event data access.
type FestaRepo interface {
CleanupAll() error
InsertEvent(startTime uint32) error
GetFestaEvents() ([]FestaEvent, error)
GetTeamSouls(team string) (uint32, error)
GetTrialsWithMonopoly() ([]FestaTrial, error)
GetTopGuildForTrial(trialType uint16) (FestaGuildRanking, error)
GetTopGuildInWindow(start, end uint32) (FestaGuildRanking, error)
GetCharSouls(charID uint32) (uint32, error)
HasClaimedMainPrize(charID uint32) bool
VoteTrial(charID uint32, trialID uint32) error
RegisterGuild(guildID uint32, team string) error
SubmitSouls(charID, guildID uint32, souls []uint16) error
ClaimPrize(prizeID uint32, charID uint32) error
ListPrizes(charID uint32, prizeType string) ([]Prize, error)
}
// TowerRepo defines the contract for tower/tenrouirai data access.
type TowerRepo interface {
GetTowerData(charID uint32) (TowerData, error)
GetSkills(charID uint32) (string, error)
UpdateSkills(charID uint32, skills string, cost int32) error
UpdateProgress(charID uint32, tr, trp, cost, block1 int32) error
GetGems(charID uint32) (string, error)
UpdateGems(charID uint32, gems string) error
GetTenrouiraiProgress(guildID uint32) (TenrouiraiProgressData, error)
GetTenrouiraiMissionScores(guildID uint32, missionIndex uint8) ([]TenrouiraiCharScore, error)
GetGuildTowerRP(guildID uint32) (uint32, error)
GetGuildTowerPageAndRP(guildID uint32) (page int, donated int, err error)
AdvanceTenrouiraiPage(guildID uint32) error
DonateGuildTowerRP(guildID uint32, rp uint16) error
}
// RengokuRepo defines the contract for rengoku score/ranking data access.
type RengokuRepo interface {
UpsertScore(charID uint32, maxStagesMp, maxPointsMp, maxStagesSp, maxPointsSp uint32) error
GetRanking(leaderboard uint32, guildID uint32) ([]RengokuScore, error)
}
// MailRepo defines the contract for in-game mail data access.
type MailRepo interface {
SendMail(senderID, recipientID uint32, subject, body string, itemID, itemAmount uint16, isGuildInvite, isSystemMessage bool) error
GetListForCharacter(charID uint32) ([]Mail, error)
GetByID(id int) (*Mail, error)
MarkRead(id int) error
MarkDeleted(id int) error
SetLocked(id int, locked bool) error
MarkItemReceived(id int) error
}
// StampRepo defines the contract for stamp card data access.
type StampRepo interface {
GetChecked(charID uint32, stampType string) (time.Time, error)
Init(charID uint32, now time.Time) error
SetChecked(charID uint32, stampType string, now time.Time) error
IncrementTotal(charID uint32, stampType string) error
GetTotals(charID uint32, stampType string) (total, redeemed uint16, err error)
ExchangeYearly(charID uint32) (total, redeemed uint16, err error)
Exchange(charID uint32, stampType string) (total, redeemed uint16, err error)
GetMonthlyClaimed(charID uint32, monthlyType string) (time.Time, error)
SetMonthlyClaimed(charID uint32, monthlyType string, now time.Time) error
}
// DistributionRepo defines the contract for distribution/event item data access.
type DistributionRepo interface {
List(charID uint32, distType uint8) ([]Distribution, error)
GetItems(distributionID uint32) ([]DistributionItem, error)
RecordAccepted(distributionID, charID uint32) error
GetDescription(distributionID uint32) (string, error)
}
// SessionRepo defines the contract for session/login token data access.
type SessionRepo interface {
ValidateLoginToken(token string, sessionID uint32, charID uint32) error
BindSession(token string, serverID uint16, charID uint32) error
ClearSession(token string) error
UpdatePlayerCount(serverID uint16, count int) error
}
// EventRepo defines the contract for event/login boost data access.
type EventRepo interface {
GetFeatureWeapon(startTime time.Time) (activeFeature, error)
InsertFeatureWeapon(startTime time.Time, features uint32) error
GetLoginBoosts(charID uint32) ([]loginBoost, error)
InsertLoginBoost(charID uint32, weekReq uint8, expiration, reset time.Time) error
UpdateLoginBoost(charID uint32, weekReq uint8, expiration, reset time.Time) error
GetEventQuests() ([]EventQuest, error)
UpdateEventQuestStartTimes(updates []EventQuestUpdate) error
}
// AchievementRepo defines the contract for achievement data access.
type AchievementRepo interface {
EnsureExists(charID uint32) error
GetAllScores(charID uint32) ([33]int32, error)
IncrementScore(charID uint32, achievementID uint8) error
GetDisplayedLevels(charID uint32) ([]byte, error)
SaveDisplayedLevels(charID uint32, levels []byte) error
}
// ShopRepo defines the contract for shop data access.
type ShopRepo interface {
GetShopItems(shopType uint8, shopID uint32, charID uint32) ([]ShopItem, error)
RecordPurchase(charID, shopItemID, quantity uint32) error
GetFpointItem(tradeID uint32) (quantity, fpoints int, err error)
GetFpointExchangeList() ([]FPointExchange, error)
}
// CafeRepo defines the contract for cafe bonus data access.
type CafeRepo interface {
ResetAccepted(charID uint32) error
GetBonuses(charID uint32) ([]CafeBonus, error)
GetClaimable(charID uint32, elapsedSec int64) ([]CafeBonus, error)
GetBonusItem(bonusID uint32) (itemType, quantity uint32, err error)
AcceptBonus(bonusID, charID uint32) error
}
// GoocooRepo defines the contract for goocoo (pet) data access.
type GoocooRepo interface {
EnsureExists(charID uint32) error
GetSlot(charID uint32, slot uint32) ([]byte, error)
ClearSlot(charID uint32, slot uint32) error
SaveSlot(charID uint32, slot uint32, data []byte) error
}
// DivaPrize represents a single reward milestone for the personal or guild track.
type DivaPrize struct {
ID int
Type string
PointsReq int
ItemType int
ItemID int
Quantity int
GR bool
Repeatable bool
}
// DivaRepo defines the contract for diva event data access.
type DivaRepo interface {
DeleteEvents() error
InsertEvent(startEpoch uint32) error
GetEvents() ([]DivaEvent, error)
AddPoints(charID uint32, eventID uint32, questPoints, bonusPoints uint32) error
GetPoints(charID uint32, eventID uint32) (questPoints, bonusPoints int64, err error)
GetTotalPoints(eventID uint32) (questPoints, bonusPoints int64, err error)
// Bead management
GetBeads() ([]int, error)
AssignBead(characterID uint32, beadIndex int, expiry time.Time) error
AddBeadPoints(characterID uint32, beadIndex int, points int) error
GetCharacterBeadPoints(characterID uint32) (map[int]int, error)
GetTotalBeadPoints() (int64, error)
GetTopBeadPerDay(day int) (int, error)
CleanupBeads() error
// Prize rewards
GetPersonalPrizes() ([]DivaPrize, error)
GetGuildPrizes() ([]DivaPrize, error)
// Interception points (guild_characters.interception_points JSON)
GetCharacterInterceptionPoints(characterID uint32) (map[string]int, error)
AddInterceptionPoints(characterID uint32, questFileID int, points int) error
}
// MiscRepo defines the contract for miscellaneous data access.
type MiscRepo interface {
GetTrendWeapons(weaponType uint8) ([]uint16, error)
UpsertTrendWeapon(weaponID uint16, weaponType uint8) error
}
// ScenarioRepo defines the contract for scenario counter data access.
type ScenarioRepo interface {
GetCounters() ([]Scenario, error)
}
// MercenaryRepo defines the contract for mercenary/rasta data access.
type MercenaryRepo interface {
NextRastaID() (uint32, error)
NextAirouID() (uint32, error)
GetMercenaryLoans(charID uint32) ([]MercenaryLoan, error)
GetGuildHuntCatsUsed(charID uint32) ([]GuildHuntCatUsage, error)
GetGuildAirou(guildID uint32) ([][]byte, error)
}