package repo import ( "crypto/sha256" "encoding/hex" "sort" "strings" "browser.local/platform/domain" ) func normalizeStoreSnapshot(snapshot StoreSnapshot) StoreSnapshot { plugins, replacements := latestGamePlugins(snapshot.GamePlugins) snapshot.GamePlugins = plugins snapshot.ServerInstances = rewriteSnapshotServerInstances(snapshot.ServerInstances, replacements) snapshot.RuntimeBindings = rewriteSnapshotRuntimeBindings(snapshot.RuntimeBindings, replacements) snapshot.PluginLifecycles = rewriteSnapshotPluginLifecycles(snapshot.PluginLifecycles, replacements) snapshot.PluginDataRecords = retainedSnapshotPluginData(snapshot.PluginDataRecords, replacements) return snapshot } func latestGamePlugins(values []domain.GamePlugin) ([]domain.GamePlugin, map[string]domain.GamePlugin) { byIdentity := map[string]domain.GamePlugin{} for _, plugin := range values { key := domain.GamePluginIdentityKey(plugin) if current, exists := byIdentity[key]; !exists || domain.GamePluginIsNewer(plugin, current) { byIdentity[key] = plugin } } replacements := map[string]domain.GamePlugin{} out := make([]domain.GamePlugin, 0, len(byIdentity)) for _, plugin := range byIdentity { out = append(out, domain.CopyGamePlugin(plugin)) } sort.SliceStable(out, func(left, right int) bool { return out[left].ID < out[right].ID }) for _, plugin := range values { kept, ok := byIdentity[domain.GamePluginIdentityKey(plugin)] if ok && plugin.ID != kept.ID { replacements[plugin.ID] = kept } } return out, replacements } func rewriteSnapshotServerInstances(values []domain.ServerInstance, replacements map[string]domain.GamePlugin) []domain.ServerInstance { out := make([]domain.ServerInstance, len(values)) for index, value := range values { if kept, ok := replacements[value.PluginID]; ok { value.PluginID = kept.ID value.PluginVersion = kept.Version } out[index] = domain.CopyServerInstance(value) } return out } func rewriteSnapshotRuntimeBindings(values []domain.RuntimeBinding, replacements map[string]domain.GamePlugin) []domain.RuntimeBinding { out := make([]domain.RuntimeBinding, len(values)) for index, value := range values { if kept, ok := replacements[value.PluginID]; ok { value.PluginID = kept.ID value.PluginVersion = kept.Version } out[index] = domain.CopyRuntimeBinding(value) } return out } func rewriteSnapshotPluginLifecycles(values []domain.PluginLifecycleInstallation, replacements map[string]domain.GamePlugin) []domain.PluginLifecycleInstallation { out := make([]domain.PluginLifecycleInstallation, len(values)) for index, value := range values { if kept, ok := replacements[value.PluginID]; ok { value.PluginID = kept.ID if value.TargetVersion != "" { value.TargetVersion = kept.Version } } out[index] = domain.CopyPluginLifecycleInstallation(value) } return out } func retainedSnapshotPluginData(values []domain.PluginDataRecord, replacements map[string]domain.GamePlugin) []domain.PluginDataRecord { byID := map[string]domain.PluginDataRecord{} for _, value := range values { if isLegacySCUMPluginData(value.PluginID, value.Collection) { continue } if kept, ok := replacements[value.PluginID]; ok { value.PluginID = kept.ID value.ID = snapshotPluginDataID(value.ServerInstanceID, kept.ID, value.Collection, value.Key) } if current, exists := byID[value.ID]; !exists || value.UpdatedAt.After(current.UpdatedAt) || (value.UpdatedAt.Equal(current.UpdatedAt) && value.CreatedAt.After(current.CreatedAt)) { byID[value.ID] = domain.CopyPluginDataRecord(value) } } ids := make([]string, 0, len(byID)) for id := range byID { ids = append(ids, id) } sort.Strings(ids) out := make([]domain.PluginDataRecord, 0, len(ids)) for _, id := range ids { out = append(out, domain.CopyPluginDataRecord(byID[id])) } return out } func isLegacySCUMPluginData(pluginID string, collection string) bool { if !isSCUMPluginID(pluginID) { return false } switch strings.ToLower(strings.TrimSpace(collection)) { case "scum_users", "scum_players", "scum_trajectories", "scum_user_trajectories", "scum_vehicles", "scum_vehicle_trajectories", "scum_vehicle_locks": return true default: return false } } func isSCUMPluginID(pluginID string) bool { canonical := domain.CanonicalGamePluginID(pluginID) return canonical == "game.scum" || canonical == "server.scum" } func snapshotPluginDataID(serverID, pluginID, collection, key string) string { sum := sha256.Sum256([]byte(serverID + "\x00" + pluginID + "\x00" + collection + "\x00" + key)) return "plugin-data-" + hex.EncodeToString(sum[:])[:24] }