diff --git a/changelog.d/20261023-time-is-time-settings.md b/changelog.d/20261023-time-is-time-settings.md new file mode 100644 index 0000000..38a41d9 --- /dev/null +++ b/changelog.d/20261023-time-is-time-settings.md @@ -0,0 +1,7 @@ +--- +area: manager +type: added +audience: admins +title: Configure the day and night cycle from the module settings +--- +TimeIsTime now has a module card, an on/off switch and settings translated into all six Manager languages. diff --git a/crates/coa-core/src/modules.rs b/crates/coa-core/src/modules.rs index 3b63b9b..7add8c0 100644 --- a/crates/coa-core/src/modules.rs +++ b/crates/coa-core/src/modules.rs @@ -522,6 +522,18 @@ fn save_settings_in( "Use the module switch to turn this module on or off.".into(), )); } + if id == "time-is-time" { + let valid = match key.as_str() { + "TimeIsTime.Announce" => matches!(value, "0" | "1"), + "TimeIsTime.SpeedRate" => value.parse::().is_ok_and(|v| v.is_finite() && (0.0..=60.0).contains(&v)), + "TimeIsTime.TimeStart" => value.parse::().is_ok_and(|v| v >= 0), + "TimeIsTime.HourOffset" => value.parse::().is_ok_and(|v| v.is_finite() && (-24.0..=24.0).contains(&v)), + _ => true, + }; + if !valid { + return Err(Error::Invalid(format!("Invalid value for {key}."))); + } + } if id == "content-scaling" && key == "CoAContentScaling.Difficulty.DamageMultiplier" { let multiplier = value.parse::().map_err(|_| { Error::Invalid("Enemy damage must be a number from 0.25 to 2.0.".into()) @@ -611,6 +623,53 @@ fn save_settings_in( mod tests { use super::*; + #[test] + fn time_is_time_card_settings_and_backups_match_the_module() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("server"); + let meta = temp.path().join("metadata"); + fs::create_dir_all(dir(&root)).unwrap(); + fs::create_dir_all(&meta).unwrap(); + let cat = catalog(); + assert!(!list_in(&cat, &root).iter().find(|m| m.id == "time-is-time").unwrap().installed); + let defaults = "# Keep module documentation\n[worldserver]\nTimeIsTime.Enable = 1\nTimeIsTime.Announce = 1\nTimeIsTime.SpeedRate = 1.0\nTimeIsTime.TimeStart = 0\nTimeIsTime.HourOffset = 0.0\n"; + let dist = dir(&root).join("mod-time_is_time.conf.dist"); + let active = dir(&root).join("mod-time_is_time.conf"); + fs::write(&dist, defaults).unwrap(); + let view = list_in(&cat, &root).into_iter().find(|m| m.id == "time-is-time").unwrap(); + assert!(view.installed && view.enabled && view.switchable && view.has_settings); + for locale in ["en", "ru", "de", "fr", "es", "zh"] { + assert!(!view.description[locale].is_empty()); + } + assert_eq!(settings_in(&cat, &root, "time-is-time").unwrap().len(), 5); + let changes = BTreeMap::from([ + ("TimeIsTime.SpeedRate".into(), "7.5".into()), + ("TimeIsTime.TimeStart".into(), "1213963200".into()), + ("TimeIsTime.HourOffset".into(), "5.5".into()), + ("TimeIsTime.Announce".into(), "0".into()), + ]); + assert_eq!(save_settings_in(&cat, &root, &meta, "time-is-time", &changes).unwrap().len(), 4); + let saved = fs::read(&active).unwrap(); + assert!(String::from_utf8_lossy(&saved).contains("# Keep module documentation")); + assert_eq!(fs::read_to_string(&dist).unwrap(), defaults); + assert!(save_settings_in(&cat, &root, &meta, "time-is-time", &changes).unwrap().is_empty()); + for (key, value) in [ + ("TimeIsTime.Enable", "0"), ("TimeIsTime.SpeedRate", "NaN"), + ("TimeIsTime.SpeedRate", "-1"), ("TimeIsTime.SpeedRate", "61"), + ("TimeIsTime.HourOffset", "inf"), ("TimeIsTime.HourOffset", "25"), + ("TimeIsTime.TimeStart", "-1"), ("TimeIsTime.TimeStart", "2147483648"), + ("TimeIsTime.TimeStart", "1.5"), ("TimeIsTime.Announce", "2"), + ] { + assert!(save_settings_in(&cat, &root, &meta, "time-is-time", &BTreeMap::from([(key.into(), value.into())])).is_err(), "{key}={value}"); + assert_eq!(fs::read(&active).unwrap(), saved); + } + set_enabled_in(&cat, &root, &meta, "time-is-time", false).unwrap(); + assert_eq!(read(&active).unwrap().get("TimeIsTime.Enable"), Some("0")); + set_enabled_in(&cat, &root, &meta, "time-is-time", true).unwrap(); + assert_eq!(read(&active).unwrap().get("TimeIsTime.SpeedRate"), Some("7.5")); + assert!(meta.join("backups").exists()); + } + fn test_catalog() -> Vec { let one = |id: &str, name: &str, conf: &str, key: &str| Entry { id: id.into(), @@ -802,8 +861,8 @@ mod tests { // it may be empty (modules are added to it one by one); whatever is in it must be complete for e in &c { assert!( - e.description.contains_key("en") && e.description.len() == 5, - "{} has all five descriptions", + ["en", "ru", "de", "fr", "es"].iter().all(|locale| e.description.get(*locale).is_some_and(|text| !text.is_empty())), + "{} has the existing catalog languages (additional translations are allowed)", e.id ); assert!( diff --git a/modules/catalog.json b/modules/catalog.json index 4a1bfd7..b47039d 100644 --- a/modules/catalog.json +++ b/modules/catalog.json @@ -139,6 +139,24 @@ "enable_key": "CoACustomRaces.Enable", "default_on": false }, + { + "id": "time-is-time", + "name": "TimeIsTime", + "description": { + "en": "Adjust the day and night cycle speed and the starting game time.", + "ru": "Настройка скорости смены дня и ночи и начального игрового времени.", + "de": "Geschwindigkeit des Tag-Nacht-Zyklus und Startzeit im Spiel anpassen.", + "fr": "Réglez la vitesse du cycle jour-nuit et l’heure de départ en jeu.", + "es": "Ajusta la velocidad del ciclo de día y noche y la hora inicial del juego.", + "zh": "调整昼夜循环速度和游戏初始时间。" + }, + "repo": "https://github.com/dunjeon/mod-TimeIsTime", + "conf": "mod-time_is_time.conf", + "enable_key": "TimeIsTime.Enable", + "default_on": true, + "status": "early", + "icon": "clock" + }, { "id": "client-compat", "name": "Client compatibility", diff --git a/src/dev/mock.ts b/src/dev/mock.ts index 5806768..a8423b5 100644 --- a/src/dev/mock.ts +++ b/src/dev/mock.ts @@ -1,5 +1,6 @@ // Dev-only: lets the UI run in a plain browser for visual review. Never bundled in production builds. import { auctionCategories, auctionQualities } from "../lib/moduleSettings"; +import moduleCatalog from "../../modules/catalog.json"; import botsSchema from "../../schemas/bots.json"; import serverSchema from "../../schemas/server.json"; import botsPresets from "../../schemas/presets/bots.json"; @@ -69,13 +70,14 @@ export {}; if(cmd==="realmlist_save"){const w=window as any;const rl=w.__rl;if(args.profileId){const p=rl.profiles.find((x: any)=>x.id===args.profileId);p.name=args.name;p.data=args.data;return p;}if(rl.profiles.some((x: any)=>x.name.toLowerCase()===String(args.name).toLowerCase()))throw {human:{code:"unknown",title:"",message:"",actions:[]},technical:"A realmlist with that name already exists."};const p={id:String(args.name).toLowerCase().replace(/[^a-z0-9]+/g,"-"),name:args.name,data:(/\s/.test(args.data.trim())?args.data.trim():"set realmlist "+args.data.trim())+"\r\n"};rl.profiles.push(p);return p;} if(cmd==="realmlist_delete"){const rl=(window as any).__rl;rl.profiles=rl.profiles.filter((x: any)=>x.id!==args.profileId);return null;} if(cmd==="account_delete") return null; - if(cmd==="modules_list") return [{id:"companions",name:"CoA Companions",description:{en:"Companion bots for groups, dungeons and your guild.",ru:"Боты-компаньоны для групп, подземелий и гильдии."},repo:"https://github.com/Corfirean/mod-coa-playerbots",installed:true,enabled:modState["companions"],switchable:true,status:"beta",icon:"bot",page:"bots"},{id:"content-scaling",name:"Content Scaling",description:{en:"Scales dungeons and raids to your group, with solo modes.",ru:"Подстраивает подземелья и рейды под вашу группу, есть соло-режимы."},repo:"https://github.com/Corfirean/mod-coa-content-scaling",installed:true,enabled:modState["content-scaling"],switchable:true,status:"early",icon:"scaling",page:null},{id:"tbc-content",name:"TBC Content",description:{en:"Outland dungeons and raids, scaled to your level range.",ru:"Подземелья и рейды Запределья, подогнанные под ваши уровни."},repo:"https://github.com/Corfirean/mod-coa-content-scaling/tree/main/content-packs/mod-coa-tbc-content",installed:true,enabled:modState["tbc-content"],switchable:true,status:"early",icon:"tbc",page:null},{id:"wotlk-content",name:"WotLK Content",description:{en:"Northrend dungeons and raids, scaled to your level range.",ru:"Подземелья и рейды Нордскола, подогнанные под ваши уровни."},repo:"https://github.com/Corfirean/mod-coa-content-scaling/tree/main/content-packs/mod-coa-wotlk-content",installed:true,enabled:modState["wotlk-content"],switchable:true,status:"early",icon:"wotlk",page:null},{id:"ah-bot",name:"Auction House Bot",description:{en:"Fills the auction house with realistic, adjustable listings.",ru:"Наполняет аукцион реалистичными настраиваемыми лотами."},repo:"https://github.com/Corfirean/azerothcore-wotlk-coa",installed:true,enabled:modState["ah-bot"] ?? false,switchable:true,status:"early",icon:"gavel",page:null,hidden:false},{id:"npc-enchanter",name:"NPC Enchanter",description:{en:"An NPC that enchants your gear for you.",ru:"НПС, который зачаровывает ваше снаряжение."},repo:"https://github.com/azerothcore/mod-npc-enchanter",installed:true,enabled:modState["npc-enchanter"],switchable:true,status:"early",icon:"sparkles",page:null,hidden:false},{id:"playerbots",name:"SQUID's Playerbots",description:{en:"An alternative bot system with random bots and full AI.",ru:"Альтернативная система ботов со случайными ботами и полным ИИ."},repo:"https://github.com/Zyth45/mod-playerbots",installed:true,enabled:modState["playerbots"] ?? false,switchable:true,status:"early",icon:"swords",page:"bots",hidden:false},{id:"client-compat",name:"Client compatibility",description:{en:"Client-server link and appearance unlocks.",ru:"Связь клиента с сервером и разблокировка внешних видов."},repo:"https://github.com/Corfirean/azerothcore-wotlk-coa",installed:true,enabled:modState["client-compat"],switchable:false,status:"release",icon:"plug",page:null,hidden:true}].map(m => ({...m, has_settings: !["tbc-content", "wotlk-content"].includes(m.id), compatibility: realm !== "coa" ? (["companions", "playerbots"].includes(m.id) ? "unsupported" : m.id === "client-compat" ? "compatible" : "experimental") : "compatible", switchable: realm !== "coa" && ["companions", "playerbots"].includes(m.id) ? false : m.switchable})); + if(cmd==="modules_list") return [{id:"companions",name:"CoA Companions",description:{en:"Companion bots for groups, dungeons and your guild.",ru:"Боты-компаньоны для групп, подземелий и гильдии."},repo:"https://github.com/Corfirean/mod-coa-playerbots",installed:true,enabled:modState["companions"],switchable:true,status:"beta",icon:"bot",page:"bots"},{id:"content-scaling",name:"Content Scaling",description:{en:"Scales dungeons and raids to your group, with solo modes.",ru:"Подстраивает подземелья и рейды под вашу группу, есть соло-режимы."},repo:"https://github.com/Corfirean/mod-coa-content-scaling",installed:true,enabled:modState["content-scaling"],switchable:true,status:"early",icon:"scaling",page:null},{id:"tbc-content",name:"TBC Content",description:{en:"Outland dungeons and raids, scaled to your level range.",ru:"Подземелья и рейды Запределья, подогнанные под ваши уровни."},repo:"https://github.com/Corfirean/mod-coa-content-scaling/tree/main/content-packs/mod-coa-tbc-content",installed:true,enabled:modState["tbc-content"],switchable:true,status:"early",icon:"tbc",page:null},{id:"wotlk-content",name:"WotLK Content",description:{en:"Northrend dungeons and raids, scaled to your level range.",ru:"Подземелья и рейды Нордскола, подогнанные под ваши уровни."},repo:"https://github.com/Corfirean/mod-coa-content-scaling/tree/main/content-packs/mod-coa-wotlk-content",installed:true,enabled:modState["wotlk-content"],switchable:true,status:"early",icon:"wotlk",page:null},{id:"ah-bot",name:"Auction House Bot",description:{en:"Fills the auction house with realistic, adjustable listings.",ru:"Наполняет аукцион реалистичными настраиваемыми лотами."},repo:"https://github.com/Corfirean/azerothcore-wotlk-coa",installed:true,enabled:modState["ah-bot"] ?? false,switchable:true,status:"early",icon:"gavel",page:null,hidden:false},{id:"npc-enchanter",name:"NPC Enchanter",description:{en:"An NPC that enchants your gear for you.",ru:"НПС, который зачаровывает ваше снаряжение."},repo:"https://github.com/azerothcore/mod-npc-enchanter",installed:true,enabled:modState["npc-enchanter"],switchable:true,status:"early",icon:"sparkles",page:null,hidden:false},{id:"playerbots",name:"SQUID's Playerbots",description:{en:"An alternative bot system with random bots and full AI.",ru:"Альтернативная система ботов со случайными ботами и полным ИИ."},repo:"https://github.com/Zyth45/mod-playerbots",installed:true,enabled:modState["playerbots"] ?? false,switchable:true,status:"early",icon:"swords",page:"bots",hidden:false},{id:"client-compat",name:"Client compatibility",description:{en:"Client-server link and appearance unlocks.",ru:"Связь клиента с сервером и разблокировка внешних видов."},repo:"https://github.com/Corfirean/azerothcore-wotlk-coa",installed:true,enabled:modState["client-compat"],switchable:false,status:"release",icon:"plug",page:null,hidden:true}, ...moduleCatalog.modules.filter(m => m.id === "time-is-time").map(m => ({...m, installed:true, enabled:modState[m.id] ?? true, switchable:true, page:null, hidden:false}))].map(m => ({...m, has_settings: !["tbc-content", "wotlk-content"].includes(m.id), compatibility: realm !== "coa" ? (["companions", "playerbots"].includes(m.id) ? "unsupported" : m.id === "client-compat" ? "compatible" : "experimental") : "compatible", switchable: realm !== "coa" && ["companions", "playerbots"].includes(m.id) ? false : m.switchable})); if(cmd==="module_set_enabled"){ if(args.enabled && ["companions","playerbots"].includes(args.module) && modState[args.module === "companions" ? "playerbots" : "companions"]) throw {human:{code:"bot_modules_conflict",title:"",message:"",actions:[]},technical:"Only one bot system may be enabled."}; modState[args.module]=args.enabled; return null; } if(cmd==="all_settings") return [{key:"Visibility.Distance.Continents",value:"100",default:"90",doc:"Description: Visibility distance on continents. Range: 45-250 Default: 90",changed:true},{key:"Death.SicknessLevel",value:"11",default:"11",doc:"Description: Character level up to which characters get resurrection sickness. Default: 11",changed:false},{key:"Rate.Drop.Item.Epic",value:"3",default:"1",doc:"Description: Drop rates for money and items based on quality. Default: 1",changed:true},{key:"Instance.ResetTimeHour",value:"4",default:"4",doc:"Description: Hour of the day when the global instance reset occurs. Range: 0-23 Default: 4 - (04:00 AM)",changed:false},{key:"PlayerLimit",value:"1000",default:"1000",doc:"Description: Maximum number of players in the world.",changed:false}]; if(cmd==="all_settings_save") return Object.keys(args&&args.changes||{}); if(cmd==="module_settings" && args.module==="client-compat") return [{key:"CoA.UnlockLocalAppearanceCatalog",value:"1",default:"1",doc:""},{key:"CoA.UnlockAllVanity",value:"1",default:"1",doc:""},{key:"CoA.AutoCollectAppearances",value:"0",default:"1",doc:""},{key:"CoA.LearnOwnedCompanions",value:"1",default:"1",doc:""},{key:"CoA.MaxRidingFromStart",value:"1",default:"1",doc:""}]; if(cmd==="module_settings"){ const S:Record={ + "time-is-time": [{key:"TimeIsTime.Enable",value:"1",default:"1",doc:""},{key:"TimeIsTime.Announce",value:"1",default:"1",doc:""},{key:"TimeIsTime.SpeedRate",value:"1.0",default:"1.0",doc:""},{key:"TimeIsTime.TimeStart",value:"0",default:"0",doc:""},{key:"TimeIsTime.HourOffset",value:"0.0",default:"0.0",doc:""}], "npc-enchanter":[{key:"Enchanter.Enable",value:"1",default:"1",doc:"Enable the module."},{key:"Enchanter.Announce",value:"0",default:"0",doc:"Announce the module when the player logs in."},{key:"Enchanter.CostCopper",value:"0",default:"0",doc:"What one enchantment costs, in copper (10000 = 1 gold). 0 = free."},{key:"Enchanter.MessageTimer",value:"60000",default:"60000",doc:"How often the NPC emotes, in milliseconds."}], "playerbots":[{"key": "AiPlayerbot.RandomBotAutologin", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.MinRandomBots", "value": "500", "default": "500", "doc": ""}, {"key": "AiPlayerbot.MaxRandomBots", "value": "500", "default": "500", "doc": ""}, {"key": "AiPlayerbot.RandomBotAccountCount", "value": "0", "default": "0", "doc": ""}, {"key": "AiPlayerbot.RandomBotMinLevel", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.RandomBotMaxLevel", "value": "60", "default": "60", "doc": ""}, {"key": "AiPlayerbot.SyncLevelWithPlayers", "value": "0", "default": "0", "doc": ""}, {"key": "AiPlayerbot.RandomBotInvitePlayer", "value": "0", "default": "0", "doc": ""}, {"key": "AiPlayerbot.RandomBotJoinLfg", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.RandomBotJoinBG", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.RandomBotAutoJoinBG", "value": "0", "default": "0", "doc": ""}, {"key": "AiPlayerbot.RandomBotTalk", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.RandomBotEmote", "value": "0", "default": "0", "doc": ""}, {"key": "AiPlayerbot.RandomBotSuggestDungeons", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.CoaClassesOnly", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.CoaSmartHeal", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.CoaSmartTank", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.CoaSpecRotations", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.CoaInterruptCoordination", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.MaxAddedBots", "value": "40", "default": "40", "doc": ""}, {"key": "AiPlayerbot.BotAutologin", "value": "0", "default": "0", "doc": ""}, {"key": "AiPlayerbot.DisabledWithoutRealPlayer", "value": "0", "default": "0", "doc": ""}, {"key": "AiPlayerbot.AddClassCommand", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.KeepAltsInGroup", "value": "0", "default": "0", "doc": ""}, {"key": "AiPlayerbot.CoaRecruitSameFaction", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.CoaLfgBots", "value": "1", "default": "1", "doc": ""}, {"key": "AiPlayerbot.RandomBotXPRate", "value": "1.0", "default": "1.0", "doc": ""}, {"key": "AiPlayerbot.FollowDistance", "value": "1.5", "default": "1.5", "doc": ""}, {"key": "AiPlayerbot.Enabled", "value": "0", "default": "0", "doc": ""}, {"key": "AiPlayerbot.RandomBotAccountPrefix", "value": "\"rndbot\"", "default": "\"rndbot\"", "doc": ""}, {"key": "AiPlayerbot.RandomBotUpdateInterval", "value": "20", "default": "20", "doc": ""}], "companions":[{key:"CoaBots.Enable",value:"1",default:"1",doc:"Master switch of the companion bots."},{key:"CoaBots.RandomSpawn.DefaultCount",value:"10",default:"10",doc:""}], diff --git a/src/lib/moduleSettings.ts b/src/lib/moduleSettings.ts index 3a6492f..3f1f854 100644 --- a/src/lib/moduleSettings.ts +++ b/src/lib/moduleSettings.ts @@ -17,6 +17,11 @@ const fields = moduleSettingDefinitions; function add(key: string, type: string, title: string[], description: string[], extra: Partial = {}) { fields.push({ key, type, title, description, ...extra }); } +add("TimeIsTime.Enable", "bool", ["Day and night cycle", "Смена дня и ночи", "Tag-Nacht-Zyklus", "Cycle jour-nuit", "Ciclo de día y noche", "昼夜循环"], ["Enable custom game time.", "Включает настройку игрового времени.", "Angepasste Spielzeit aktivieren.", "Active l’heure de jeu personnalisée.", "Activa la hora de juego personalizada.", "启用自定义游戏时间。"]); +add("TimeIsTime.Announce", "bool", ["Announce on login", "Сообщение при входе", "Hinweis beim Anmelden", "Annonce à la connexion", "Aviso al entrar", "登录公告"], ["Show players a module announcement when they log in.", "Показывает игрокам сообщение о модуле при входе.", "Spielern beim Anmelden einen Modulhinweis anzeigen.", "Affiche un message sur le module à la connexion.", "Muestra un aviso sobre el módulo al entrar.", "玩家登录时显示模块公告。"]); +add("TimeIsTime.SpeedRate", "float", ["Game time speed", "Скорость игрового времени", "Geschwindigkeit der Spielzeit", "Vitesse du temps en jeu", "Velocidad del tiempo de juego", "游戏时间速度"], ["1 = a 24-hour day; 7.5 = 3 hours 12 minutes; 60 = 24 minutes. 0 pauses the clock. Applies when players log in again.", "1 — сутки за 24 часа; 7.5 — за 3 часа 12 минут; 60 — за 24 минуты. 0 останавливает часы. Применяется после повторного входа игроков.", "1 = ein Tag in 24 Stunden; 7.5 = 3 Stunden 12 Minuten; 60 = 24 Minuten. 0 hält die Uhr an. Gilt nach erneuter Anmeldung.", "1 = une journée en 24 heures ; 7.5 = 3 h 12 ; 60 = 24 minutes. 0 arrête l’horloge. S’applique à la reconnexion des joueurs.", "1 = un día en 24 horas; 7.5 = 3 horas y 12 minutos; 60 = 24 minutos. 0 detiene el reloj. Se aplica cuando los jugadores vuelven a entrar.", "1表示一天为24小时;7.5为3小时12分钟;60为24分钟。0暂停时钟。玩家重新登录后生效。"], { min: 0, max: 60 }); +add("TimeIsTime.TimeStart", "int", ["Starting game time (Unix timestamp)", "Начальное игровое время (Unix timestamp)", "Spielstartzeit (Unix-Zeitstempel)", "Heure de départ (horodatage Unix)", "Hora inicial (marca de tiempo Unix)", "游戏初始时间(Unix时间戳)"], ["0 uses the server clock. A positive Unix timestamp sets the same starting date and time at every login. Applies when players log in again.", "0 — использовать часы сервера. Положительная метка Unix задаёт одинаковые начальные дату и время при каждом входе. Применяется после повторного входа игроков.", "0 nutzt die Serveruhr. Ein positiver Unix-Zeitstempel setzt bei jeder Anmeldung dieselbe Startzeit. Gilt nach erneuter Anmeldung.", "0 utilise l’horloge du serveur. Un horodatage Unix positif fixe la même date et heure à chaque connexion. S’applique à la reconnexion.", "0 usa el reloj del servidor. Una marca Unix positiva fija la misma fecha y hora inicial en cada entrada. Se aplica al volver a entrar.", "0使用服务器时钟。正Unix时间戳使每次登录都从同一日期和时间开始。重新登录后生效。"], { min: 0, max: 2147483647, advanced: true }); +add("TimeIsTime.HourOffset", "float", ["Starting time offset (hours)", "Смещение начального времени (часы)", "Versatz der Startzeit (Stunden)", "Décalage de départ (heures)", "Desfase de hora inicial (horas)", "初始时间偏移(小时)"], ["Added only when a positive starting timestamp is set. Fractional hours are allowed, for example 5.5. Applies when players log in again.", "Добавляется только при положительной начальной метке времени. Допустимы дробные часы, например 5.5. Применяется после повторного входа игроков.", "Wird nur bei positivem Startzeitstempel addiert. Bruchteile sind erlaubt, etwa 5.5 Stunden. Gilt nach erneuter Anmeldung.", "Ajouté uniquement avec un horodatage de départ positif. Les fractions sont acceptées, par exemple 5.5 heures. S’applique à la reconnexion.", "Se suma solo si la marca de tiempo inicial es positiva. Admite fracciones, por ejemplo 5.5 horas. Se aplica al volver a entrar.", "仅在初始时间戳为正时叠加。支持小数小时,例如5.5。重新登录后生效。"], { min: -24, max: 24, advanced: true }); add("AiPlayerbot.RandomBotAutologin", "bool", ["Populate the world automatically", "Автоматически населять мир", "Welt automatisch bevölkern", "Peupler automatiquement le monde", "Poblar el mundo automáticamente", "自动填充世界"], ["Log random bots into the world when the server starts.", "Вводит случайных ботов в мир при запуске сервера.", "Meldet Zufallsbots beim Serverstart in der Welt an.", "Connecte les bots aléatoires au démarrage du serveur.", "Conecta bots aleatorios al iniciar el servidor.", "服务器启动时让随机机器人登录世界。"]); add("AiPlayerbot.MinRandomBots", "int", ["Minimum online bots", "Минимум ботов онлайн", "Mindestens aktive Bots", "Minimum de bots connectés", "Mínimo de bots conectados", "最少在线机器人"], ["Lower limit of the automatically selected online population. Must not exceed the maximum.", "Нижний предел автоматически выбираемого числа ботов онлайн. Не должен превышать максимум.", "Untergrenze der automatisch gewählten Bot-Zahl. Darf das Maximum nicht überschreiten.", "Limite basse du nombre de bots connectés. Ne doit pas dépasser le maximum.", "Límite inferior de bots conectados. No debe superar el máximo.", "自动选择的在线机器人数量下限,不得超过上限。"], { min: 0 }); add("AiPlayerbot.MaxRandomBots", "int", ["Maximum online bots", "Максимум ботов онлайн", "Maximal aktive Bots", "Maximum de bots connectés", "Máximo de bots conectados", "最多在线机器人"], ["Upper limit of the random bot population; more bots use more server resources.", "Верхний предел числа случайных ботов онлайн. Больше ботов — выше нагрузка на сервер.", "Obergrenze der Zufallsbots; mehr Bots benötigen mehr Serverressourcen.", "Limite haute des bots aléatoires ; plus de bots utilisent plus de ressources.", "Límite superior de bots aleatorios; más bots requieren más recursos del servidor.", "随机机器人在线数量上限,数量越多越占用服务器资源。"], { min: 0 }); diff --git a/src/screens/ModulesPage.tsx b/src/screens/ModulesPage.tsx index 475ff0c..322b6e5 100644 --- a/src/screens/ModulesPage.tsx +++ b/src/screens/ModulesPage.tsx @@ -1,6 +1,6 @@ import { Fragment, useCallback, useEffect, useRef, useState, type ComponentType, type ReactNode } from "react"; import { listen } from "@tauri-apps/api/event"; -import { Bot, Flame, Gavel, Loader2, Plug, Puzzle, Scaling, Settings2, Snowflake, Sparkles, Swords, Users, type LucideProps } from "lucide-react"; +import { Bot, Clock, Flame, Gavel, Loader2, Plug, Puzzle, Scaling, Settings2, Snowflake, Sparkles, Swords, Users, type LucideProps } from "lucide-react"; import { api, asUiError, type CustomRacesClientStatus, type CustomRacesProgress, type ModuleSetting, type ModuleView, type UiError } from "@/lib/api"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; @@ -21,6 +21,7 @@ function GithubMark(props: { className?: string }) { /** Icon and colours of a module's square tile, by the name the catalog gives it. */ const TILES: Record; from: string; to: string }> = { + clock: { icon: Clock, from: "from-indigo-400", to: "to-violet-800" }, bot: { icon: Bot, from: "from-emerald-500", to: "to-teal-700" }, scaling: { icon: Scaling, from: "from-violet-500", to: "to-indigo-700" }, plug: { icon: Plug, from: "from-amber-500", to: "to-orange-700" }, diff --git a/tools/check-module-settings.mjs b/tools/check-module-settings.mjs index 0194eb4..0a6cd55 100644 --- a/tools/check-module-settings.mjs +++ b/tools/check-module-settings.mjs @@ -10,6 +10,12 @@ source = source.replace(/import (\w+) from "(\.\.\/\.\.\/[^"\n]+\.json)";/g, (_, const compiled = stripTypeScriptTypes(source); const { moduleSettingDefinitions, moduleField, canonicalModuleValue, auctionTypeChanges, auctionCategorySettings, auctionCategories, auctionQualities } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString("base64")}`); const ids = new Set(); +for (const [key, type] of [["Announce", "bool"], ["SpeedRate", "float"], ["TimeStart", "int"], ["HourOffset", "float"]]) { + for (const locale of ["en", "ru", "de", "fr", "es", "zh"]) { + const field = moduleField(`TimeIsTime.${key}`, locale); + if (field?.type !== type || !field.title || !field.description) throw new Error(`Missing TimeIsTime control: ${key}/${locale}`); + } +} for (const field of moduleSettingDefinitions) { if (ids.has(field.key)) throw new Error(`Duplicate module setting: ${field.key}`); ids.add(field.key);