package dnfbridge import ( "encoding/json" "os" "path/filepath" "strconv" "strings" "time" dnfauction "longheng.io/server/internal/modules/dnf/auction" ) type currentServerFeatureSettings struct { RaidReviveSeconds int `json:"raidReviveSeconds"` AllDeadReturnSeconds int `json:"allDeadReturnSeconds"` AllowFreeRevive *bool `json:"allowFreeRevive"` HellOrbChance int `json:"hellOrbChance"` HellDropChanceDenominator int `json:"hellDropChanceDenominator"` EquipmentRollMultiplier int `json:"equipmentRollMultiplier"` EpicPieceRollMultiplier int `json:"epicPieceRollMultiplier"` AntonRaidAttackSeconds int `json:"antonRaidAttackSeconds"` LukeEnergyRecoveryPercent int `json:"lukeEnergyRecoveryPercent"` FiendWarSquadCount int `json:"fiendWarSquadCount"` RoomFatigueCost int `json:"roomFatigueCost"` DailyFatigueResetHour int `json:"dailyFatigueResetHour"` AdventureGroupBonusPercent int `json:"adventureGroupBonusPercent"` RentalMaxReinforce int `json:"rentalMaxReinforce"` RentalMaxForge int `json:"rentalMaxForge"` ReviveByDungeon map[string]int `json:"reviveByDungeon"` AntonBlackFogRecoverySeconds int `json:"antonBlackFogRecoverySeconds"` AntonBlackFogActiveSeconds int `json:"antonBlackFogActiveSeconds"` AntonQuakeRecoverySeconds int `json:"antonQuakeRecoverySeconds"` AntonQuakeActiveSeconds int `json:"antonQuakeActiveSeconds"` AntonNavalOpenDelaySeconds int `json:"antonNavalOpenDelaySeconds"` AntonNavalRecoverySeconds int `json:"antonNavalRecoverySeconds"` AntonNavalActiveSeconds int `json:"antonNavalActiveSeconds"` LukeEnergyRecovery0Seconds int `json:"lukeEnergyRecovery0Seconds"` LukeEnergyRecovery1Seconds int `json:"lukeEnergyRecovery1Seconds"` LukeEnergyRecovery2Seconds int `json:"lukeEnergyRecovery2Seconds"` LukePhaseTimeoutPercent int `json:"lukePhaseTimeoutPercent"` FiendWarAreaLimitPercent int `json:"fiendWarAreaLimitPercent"` FiendWarDieImprisonPercent int `json:"fiendWarDieImprisonPercent"` SoloTeleportCooldownSeconds int `json:"soloTeleportCooldownSeconds"` SoloTeleportCooldownIndices string `json:"soloTeleportCooldownIndices"` AuctionStandardListingLimit int `json:"auctionStandardListingLimit"` AuctionPremiumListingLimit int `json:"auctionPremiumListingLimit"` AuctionStandardDurationHours int `json:"auctionStandardDurationHours"` AuctionPremiumDurationHours int `json:"auctionPremiumDurationHours"` AntonMemberMax int `json:"antonMemberMax"` AntonStartMinimumMembers int `json:"antonStartMinimumMembers"` AntonSingleStartMinimumMembers int `json:"antonSingleStartMinimumMembers"` AntonStartDelaySeconds int `json:"antonStartDelaySeconds"` AntonPhaseBreakSeconds int `json:"antonPhaseBreakSeconds"` AntonEntryCostItem int `json:"antonEntryCostItem"` AntonEntryCostCount int `json:"antonEntryCostCount"` AntonWeeklyEnterCountA int `json:"antonWeeklyEnterCountA"` AntonWeeklyEnterCountB int `json:"antonWeeklyEnterCountB"` AntonPhaseTimeoutSeconds string `json:"antonPhaseTimeoutSeconds"` LukeDailyAccountEnterLimit int `json:"lukeDailyAccountEnterLimit"` LukeSanctumTimerCount int `json:"lukeSanctumTimerCount"` LukeEntryCostItem int `json:"lukeEntryCostItem"` LukeEntryCostCount int `json:"lukeEntryCostCount"` FiendWarMemberMax int `json:"fiendWarMemberMax"` FiendWarStartMinimumMembers int `json:"fiendWarStartMinimumMembers"` FiendWarDisconnectWaitSeconds int `json:"fiendWarDisconnectWaitSeconds"` FiendWarEntryCostItem int `json:"fiendWarEntryCostItem"` FiendWarEntryCostCount int `json:"fiendWarEntryCostCount"` FiendWarRetry *bool `json:"fiendWarRetry"` FiendWarDailyEnterLimit int `json:"fiendWarDailyEnterLimit"` AuctionStandardFeePercent int `json:"auctionStandardFeePercent"` AuctionPremiumFeePercent int `json:"auctionPremiumFeePercent"` } func GMSaveServerFeatures(data []byte) error { var values map[string]json.RawMessage if err := json.Unmarshal(data, &values); err != nil { return err } path := currentServerConfigPath("server-features.json") tmp := path + ".tmp" if err := os.WriteFile(tmp, data, 0o644); err != nil { return err } return os.Rename(tmp, path) } func currentServerConfigPath(name string) string { candidates := []string{filepath.Join("config", name)} if executable, err := os.Executable(); err == nil { candidates = append(candidates, filepath.Join(filepath.Dir(executable), "..", "config", name), filepath.Join(filepath.Dir(executable), "config", name), ) } for _, candidate := range candidates { if _, err := os.Stat(candidate); err == nil { return candidate } } return candidates[0] } func currentServerFeatures() currentServerFeatureSettings { data, err := os.ReadFile(currentServerConfigPath("server-features.json")) if err != nil { return currentServerFeatureSettings{} } var settings currentServerFeatureSettings if err := json.Unmarshal(data, &settings); err != nil { return currentServerFeatureSettings{} } return settings } func currentActivityEnabled(key string) bool { data, err := os.ReadFile(currentServerConfigPath("activity.json")) if err != nil { return true } var values map[string]bool if err := json.Unmarshal(data, &values); err != nil { return true } enabled, found := values[key] if !found { return true } return enabled } func currentConfiguredAllDeadReturnDelay() time.Duration { const fallback = currentPartyDungeonAllDeadReturnDelay settings := currentServerFeatures() if settings.AllDeadReturnSeconds < 1 || settings.AllDeadReturnSeconds > 60 { return fallback } return time.Duration(settings.AllDeadReturnSeconds) * time.Second } func currentConfiguredAllowFreeRevive() bool { settings := currentServerFeatures() if settings.AllowFreeRevive == nil { return currentDungeonUseCoinAllowFreeRevive } return *settings.AllowFreeRevive } func currentConfiguredHellOrbChance() int { settings := currentServerFeatures() if settings.HellOrbChance < 0 || settings.HellOrbChance > currentDungeonHellOrbRollDenominator { return currentDungeonHellOrbChance } return settings.HellOrbChance } func currentConfiguredHellDropChanceDenominator() int { settings := currentServerFeatures() if settings.HellDropChanceDenominator < 1 || settings.HellDropChanceDenominator > 1_000_000 { return currentHellPartyDropChanceDenominator } return settings.HellDropChanceDenominator } func currentConfiguredAntonRaidAttackSeconds() int { settings := currentServerFeatures() if settings.AntonRaidAttackSeconds < 60 || settings.AntonRaidAttackSeconds > 24*60*60 { return antonRaidAttackSeconds } return settings.AntonRaidAttackSeconds } func currentConfiguredLukeEnergyRecoveryPercent() int { settings := currentServerFeatures() if settings.LukeEnergyRecoveryPercent < 1 || settings.LukeEnergyRecoveryPercent > 1000 { return 100 } return settings.LukeEnergyRecoveryPercent } func currentConfiguredFiendWarSquadCount() int { settings := currentServerFeatures() if settings.FiendWarSquadCount < 1 || settings.FiendWarSquadCount > 8 { return currentFiendWarSquadCount } return settings.FiendWarSquadCount } func currentConfiguredRoomFatigueCost() int64 { settings := currentServerFeatures() if settings.RoomFatigueCost < 0 || settings.RoomFatigueCost > 100 { return currentDungeonRoomFatigueCost } return int64(settings.RoomFatigueCost) } func currentConfiguredDailyFatigueResetHour() int { settings := currentServerFeatures() if settings.DailyFatigueResetHour < 0 || settings.DailyFatigueResetHour > 23 { return 6 } return settings.DailyFatigueResetHour } func currentConfiguredAdventureGroupBonusPercent() int { settings := currentServerFeatures() if settings.AdventureGroupBonusPercent < 0 || settings.AdventureGroupBonusPercent > 1000 { return currentDungeonAdventureGroupDefaultClearBonusPercent } return settings.AdventureGroupBonusPercent } func currentConfiguredRentalMaxReinforce() int { settings := currentServerFeatures() if settings.RentalMaxReinforce < 0 || settings.RentalMaxReinforce > 31 { return currentRentalMaxReinforce } return settings.RentalMaxReinforce } func currentConfiguredRentalMaxForge() int { settings := currentServerFeatures() if settings.RentalMaxForge < 0 || settings.RentalMaxForge > 255 { return currentRentalMaxForge } return settings.RentalMaxForge } func currentConfiguredDungeonReviveSeconds(dungeonID int64) (int, bool) { if dungeonID <= 0 { return 0, false } settings := currentServerFeatures() if settings.ReviveByDungeon == nil { return 0, false } value, found := settings.ReviveByDungeon[strconv.FormatInt(dungeonID, 10)] if !found || value < 1 || value > 86400 { return 0, false } return value, true } func currentConfiguredDungeonReviveDelay(dungeonID int64) time.Duration { seconds, found := currentConfiguredDungeonReviveSeconds(dungeonID) if !found { return currentConfiguredRaidReviveDelay() } return time.Duration(seconds) * time.Second } func currentAntonDuration(configured int, fallback time.Duration) time.Duration { if configured < 1 || configured > 86400 { return fallback } return time.Duration(configured) * time.Second } func currentAntonBlackFogRecovery() time.Duration { return currentAntonDuration(currentServerFeatures().AntonBlackFogRecoverySeconds, antonBlackFogRecovery) } func currentAntonBlackFogActive() time.Duration { return currentAntonDuration(currentServerFeatures().AntonBlackFogActiveSeconds, antonBlackFogActive) } func currentAntonQuakeRecovery() time.Duration { return currentAntonDuration(currentServerFeatures().AntonQuakeRecoverySeconds, antonQuakeRecovery) } func currentAntonQuakeActive() time.Duration { return currentAntonDuration(currentServerFeatures().AntonQuakeActiveSeconds, antonQuakeActive) } func currentAntonNavalOpenDelay() time.Duration { return currentAntonDuration(currentServerFeatures().AntonNavalOpenDelaySeconds, antonNavalOpenDelay) } func currentAntonNavalRecovery() time.Duration { return currentAntonDuration(currentServerFeatures().AntonNavalRecoverySeconds, antonNavalRecovery) } func currentAntonNavalActive() time.Duration { return currentAntonDuration(currentServerFeatures().AntonNavalActiveSeconds, antonNavalActive) } func currentLukeEnergyRecovery(tier int) time.Duration { if tier < 0 { tier = 0 } if tier >= len(lukeEnergyControlRecovery) { tier = len(lukeEnergyControlRecovery) - 1 } settings := currentServerFeatures() values := [...]int{ settings.LukeEnergyRecovery0Seconds, settings.LukeEnergyRecovery1Seconds, settings.LukeEnergyRecovery2Seconds, } if values[tier] >= 1 && values[tier] <= 86400 { return time.Duration(values[tier]) * time.Second } percent := settings.LukeEnergyRecoveryPercent if percent < 1 || percent > 1000 { percent = 100 } return time.Duration(int64(lukeEnergyControlRecovery[tier]) * int64(percent) / 100) } func currentLukePhaseTimeoutPercent() int { percent := currentServerFeatures().LukePhaseTimeoutPercent if percent < 1 || percent > 1000 { return 100 } return percent } func currentFiendWarAreaLimitPercent() int { percent := currentServerFeatures().FiendWarAreaLimitPercent if percent < 1 || percent > 1000 { return 100 } return percent } func currentFiendWarDieImprisonPercent() int { percent := currentServerFeatures().FiendWarDieImprisonPercent if percent < 1 || percent > 1000 { return 100 } return percent } func currentConfiguredSoloTeleportCooldownSeconds() uint32 { seconds := currentServerFeatures().SoloTeleportCooldownSeconds if seconds < 1 || seconds > 86400 { return currentSoloTeleportCooldownSeconds } return uint32(seconds) } func currentConfiguredSoloTeleportIndices() []byte { raw := strings.TrimSpace(currentServerFeatures().SoloTeleportCooldownIndices) if raw == "" { return nil } if strings.EqualFold(raw, "off") || strings.EqualFold(raw, "none") { return []byte{} } var indices []byte for _, field := range strings.Split(raw, ",") { value, err := strconv.ParseUint(strings.TrimSpace(field), 10, 8) if err == nil && currentSoloTeleportCooldownIndexAccepted(byte(value)) { indices = append(indices, byte(value)) } } return indices } func currentConfiguredAuctionListingLimit(premium bool) int { settings := currentServerFeatures() value := settings.AuctionStandardListingLimit if premium { value = settings.AuctionPremiumListingLimit } if value < 1 || value > 1000 { if premium { return currentAuctionPremiumListingLimit } return currentAuctionStandardListingLimit } return value } func currentConfiguredAuctionDuration(premium bool) time.Duration { settings := currentServerFeatures() hours := settings.AuctionStandardDurationHours if premium { hours = settings.AuctionPremiumDurationHours } if hours < 1 || hours > 720 { if premium { return currentAuctionPremiumDuration } return currentAuctionStandardDuration } return time.Duration(hours) * time.Hour } func currentConfiguredAuctionFeePercent(premium bool) int64 { settings := currentServerFeatures() value := settings.AuctionStandardFeePercent if premium { value = settings.AuctionPremiumFeePercent } if value <= 0 || value > 100 { return dnfauction.SaleFeePercentFor(premium) } return int64(value) } // currentConfiguredRaidReviveDelay reads the optional runtime/config/ // server-features.json written by the standalone server tool. It only changes // the server-side death watchdog; the current EXE still owns its ten-second // revive prompt. func currentConfiguredRaidReviveDelay() time.Duration { const fallback = currentDungeonDeathReturnDelay settings := currentServerFeatures() if settings.RaidReviveSeconds < 1 || settings.RaidReviveSeconds > 60 { return fallback } return time.Duration(settings.RaidReviveSeconds) * time.Second } // currentConfiguredEquipmentRollMultiplier keeps the local test-end special // drop tuning (2x equipment rolls) configurable by the standalone server tool. func currentConfiguredEquipmentRollMultiplier() int { settings := currentServerFeatures() if settings.EquipmentRollMultiplier < 1 || settings.EquipmentRollMultiplier > 100 { return 1 } return settings.EquipmentRollMultiplier } // currentConfiguredEpicPieceRollMultiplier keeps the local test-end special // drop tuning (3x epic fragment rolls) configurable by the standalone tool. func currentConfiguredEpicPieceRollMultiplier() int { settings := currentServerFeatures() if settings.EpicPieceRollMultiplier < 1 || settings.EpicPieceRollMultiplier > 100 { return 1 } return settings.EpicPieceRollMultiplier }