From 790506fc7a2c62f5280782661c3cff370769273e Mon Sep 17 00:00:00 2001 From: sindoring Date: Thu, 23 Jul 2026 01:14:05 +0400 Subject: [PATCH] Generate companion summon spells --- CUSTOM_CREATURES.md | 25 +- custom-creatures.json | 5 + custom-creatures.lock.json | 5 +- modules/moonwell/host/CustomSpellProvider.cpp | 264 ++++++++++++++++++ tool/src/creatures.rs | 237 ++++++++++++++-- tool/src/main.rs | 2 + 6 files changed, 510 insertions(+), 28 deletions(-) create mode 100644 modules/moonwell/host/CustomSpellProvider.cpp diff --git a/CUSTOM_CREATURES.md b/CUSTOM_CREATURES.md index d91528f..617f58a 100644 --- a/CUSTOM_CREATURES.md +++ b/CUSTOM_CREATURES.md @@ -31,7 +31,11 @@ 2. генерирует `WXLFileData.csv` для WarcraftXL; 3. дописывает модели в `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc`; 4. сохраняет стабильные ID в `custom-creatures.lock.json`; -5. кладёт серверные копии DBC и таблицу ID в `build/server-dbc`. +5. для `pets` создаёт companion summon spell и строку skill line `778`; +6. кладёт серверные DBC, SQL и таблицу ID в `build/server-dbc`. + +Полный `Spell.dbc` в патч не копируется. WarcraftXL читает компактный +`WXLSpellOverrides.tsv` и при открытии DBC добавляет новые spell-записи к штатному файлу клиента. Перестановка моделей в списке не меняет уже выданные ID. Lock-файл необходимо хранить в Git. @@ -43,6 +47,8 @@ { "folder": "assets/creatures/my_mount", "key": "my_mount", + "name": "My Mount", + "name_ru": "Мой транспорт", "archive": "Creature/MyMount", "model_file": "my_mount.m2", "template_display_id": 2404, @@ -59,6 +65,17 @@ компаньон-кот с display ID `5448`. Шаблон задаёт физические размеры, звук и прочие поля DBC; параметры объекта позволяют их переопределить. -Сгенерированные DBC сами по себе не создают серверное существо или заклинание. Сервер должен -загрузить DBC из `build/server-dbc`, а полученный `displayInfoID` нужно назначить записи питомца, -транспорта или соответствующему spell. +## Установка на сервер + +Для каждого элемента `pets` сборщик автоматически выдаёт `creatureEntry`, `summonSpellID` и +`skillLineAbilityID`. Значения находятся в `build/server-dbc/custom-creatures.csv`. + +1. Скопировать `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc` из `build/server-dbc` в + серверный каталог `data/dbc`. +2. Применить `build/server-dbc/custom-creatures.sql` к `acore_world`. +3. Перезапустить worldserver: DBC и таблицы `spell_dbc` загружаются только при старте. +4. Для проверки изучить выданный spell командой `.learn `. + +SQL создаёт `creature_template`, `creature_template_model`, companion spell и строку skill line. +Генерация полноценного mount spell для секции `mounts` остаётся отдельным этапом: mount требует +другой spell-шаблон с аурами скорости и `SPELL_AURA_MOUNTED`. diff --git a/custom-creatures.json b/custom-creatures.json index d7b18b4..46f46d8 100644 --- a/custom-creatures.json +++ b/custom-creatures.json @@ -2,6 +2,9 @@ "base_dbc_dir": "assets/dbc/3.3.5a", "model_data_id_start": 50000, "display_info_id_start": 50000, + "creature_entry_id_start": 5000000, + "summon_spell_id_start": 110000, + "skill_line_ability_id_start": 30000, "defaults": { "mount_template_display_id": 2404, "pet_template_display_id": 5448 @@ -10,6 +13,8 @@ "pets": [ { "folder": "src/Data/patch-Z/Creature/Catslime", + "name": "Cat Slime", + "name_ru": "Котослизень", "display_scale": 1.0 } ] diff --git a/custom-creatures.lock.json b/custom-creatures.lock.json index 634473d..502398f 100644 --- a/custom-creatures.lock.json +++ b/custom-creatures.lock.json @@ -2,7 +2,10 @@ "models": { "catslime": { "model_data_id": 50000, - "display_info_id": 50000 + "display_info_id": 50000, + "creature_entry": 5000000, + "summon_spell_id": 110000, + "skill_line_ability_id": 30000 } } } diff --git a/modules/moonwell/host/CustomSpellProvider.cpp b/modules/moonwell/host/CustomSpellProvider.cpp new file mode 100644 index 0000000..dc78ba1 --- /dev/null +++ b/modules/moonwell/host/CustomSpellProvider.cpp @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Appends compact MoonWell spell overrides to the stock 3.3.5 DBCs at open time. + +#include "Host.hpp" +#include "core/Logger.hpp" +#include "mpq/MpqStore.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace moonwell::host +{ + namespace + { + constexpr std::string_view kOverridesPath = "WXLSpellOverrides.tsv"; + constexpr std::string_view kSpellPath = "DBFilesClient\\Spell.dbc"; + constexpr std::string_view kSkillPath = "DBFilesClient\\SkillLineAbility.dbc"; + constexpr uint32_t kSpellFields = 234; + constexpr uint32_t kSpellRecordSize = kSpellFields * 4; + constexpr uint32_t kSkillFields = 14; + constexpr uint32_t kSkillRecordSize = kSkillFields * 4; + + struct Override + { + uint32_t spellId = 0; + uint32_t templateSpellId = 0; + uint32_t creatureEntry = 0; + uint32_t skillLineAbilityId = 0; + std::string name; + }; + + std::once_flag g_loadOnce; + bool g_ready = false; + std::vector g_spellDbc; + std::vector g_skillDbc; + + uint32_t ReadU32(const uint8_t* data) + { + uint32_t value = 0; + std::memcpy(&value, data, sizeof(value)); + return value; + } + + void WriteU32(uint8_t* data, uint32_t value) + { + std::memcpy(data, &value, sizeof(value)); + } + + std::string_view Trim(std::string_view value) + { + while (!value.empty() && std::isspace(static_cast(value.front()))) + value.remove_prefix(1); + while (!value.empty() && std::isspace(static_cast(value.back()))) + value.remove_suffix(1); + return value; + } + + bool ParseU32(std::string_view text, uint32_t& value) + { + text = Trim(text); + const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value); + return parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size(); + } + + bool SamePath(std::string_view left, std::string_view right) + { + if (left.size() != right.size()) return false; + for (size_t i = 0; i < left.size(); ++i) + { + const unsigned char a = static_cast(left[i] == '/' ? '\\' : left[i]); + const unsigned char b = static_cast(right[i] == '/' ? '\\' : right[i]); + if (std::tolower(a) != std::tolower(b)) return false; + } + return true; + } + + bool ParseOverrides(const std::vector& bytes, std::vector& out) + { + const std::string_view text(reinterpret_cast(bytes.data()), bytes.size()); + size_t lineStart = 0; + while (lineStart < text.size()) + { + size_t lineEnd = text.find('\n', lineStart); + if (lineEnd == std::string_view::npos) lineEnd = text.size(); + std::string_view line = Trim(text.substr(lineStart, lineEnd - lineStart)); + lineStart = lineEnd + 1; + if (line.empty() || line.front() == '#') continue; + + std::string_view fields[5]; + size_t fieldStart = 0; + bool valid = true; + for (size_t i = 0; i < 4; ++i) + { + const size_t tab = line.find('\t', fieldStart); + if (tab == std::string_view::npos) { valid = false; break; } + fields[i] = line.substr(fieldStart, tab - fieldStart); + fieldStart = tab + 1; + } + if (!valid) return false; + fields[4] = Trim(line.substr(fieldStart)); + + Override item; + if (!ParseU32(fields[0], item.spellId) || + !ParseU32(fields[1], item.templateSpellId) || + !ParseU32(fields[2], item.creatureEntry) || + !ParseU32(fields[3], item.skillLineAbilityId) || fields[4].empty()) + return false; + item.name.assign(fields[4]); + out.push_back(std::move(item)); + } + return !out.empty(); + } + + bool ValidateWdbc(const std::vector& bytes, uint32_t fields, uint32_t recordSize, + uint32_t& count, uint32_t& stringSize, size_t& recordsEnd) + { + if (bytes.size() < 20 || std::memcmp(bytes.data(), "WDBC", 4) != 0) return false; + count = ReadU32(bytes.data() + 4); + if (ReadU32(bytes.data() + 8) != fields || ReadU32(bytes.data() + 12) != recordSize) + return false; + stringSize = ReadU32(bytes.data() + 16); + recordsEnd = 20ull + static_cast(count) * recordSize; + return recordsEnd <= bytes.size() && recordsEnd + stringSize == bytes.size(); + } + + const uint8_t* FindSpell(const std::vector& bytes, uint32_t count, uint32_t id) + { + for (uint32_t i = 0; i < count; ++i) + { + const uint8_t* row = bytes.data() + 20 + static_cast(i) * kSpellRecordSize; + if (ReadU32(row) == id) return row; + } + return nullptr; + } + + const uint8_t* FindSkillRow(const std::vector& bytes, uint32_t count, uint32_t spellId) + { + for (uint32_t i = 0; i < count; ++i) + { + const uint8_t* row = bytes.data() + 20 + static_cast(i) * kSkillRecordSize; + if (ReadU32(row + 4) == 778 && ReadU32(row + 8) == spellId) return row; + } + return nullptr; + } + + bool BuildSpellDbc(const std::vector& base, const std::vector& overrides, + std::vector& out) + { + uint32_t count = 0, stringSize = 0; + size_t recordsEnd = 0; + if (!ValidateWdbc(base, kSpellFields, kSpellRecordSize, count, stringSize, recordsEnd)) + return false; + + std::vector records; + records.reserve(overrides.size() * kSpellRecordSize); + std::vector strings; + for (const Override& item : overrides) + { + const uint8_t* source = FindSpell(base, count, item.templateSpellId); + if (!source) return false; + const size_t recordStart = records.size(); + records.insert(records.end(), source, source + kSpellRecordSize); + uint8_t* row = records.data() + recordStart; + WriteU32(row, item.spellId); + WriteU32(row + 110 * 4, item.creatureEntry); + + const uint32_t nameOffset = stringSize + static_cast(strings.size()); + strings.insert(strings.end(), item.name.begin(), item.name.end()); + strings.push_back(0); + const std::string description = "Призывает или отпускает спутника: " + item.name + "."; + const uint32_t descriptionOffset = stringSize + static_cast(strings.size()); + strings.insert(strings.end(), description.begin(), description.end()); + strings.push_back(0); + for (uint32_t field = 136; field <= 151; ++field) WriteU32(row + field * 4, nameOffset); + for (uint32_t field = 170; field <= 185; ++field) WriteU32(row + field * 4, descriptionOffset); + for (uint32_t field = 187; field <= 202; ++field) WriteU32(row + field * 4, 0); + } + + out.reserve(base.size() + records.size() + strings.size()); + out.insert(out.end(), base.begin(), base.begin() + 20); + WriteU32(out.data() + 4, count + static_cast(overrides.size())); + WriteU32(out.data() + 16, stringSize + static_cast(strings.size())); + out.insert(out.end(), base.begin() + 20, base.begin() + recordsEnd); + out.insert(out.end(), records.begin(), records.end()); + out.insert(out.end(), base.begin() + recordsEnd, base.end()); + out.insert(out.end(), strings.begin(), strings.end()); + return true; + } + + bool BuildSkillDbc(const std::vector& base, const std::vector& overrides, + std::vector& out) + { + uint32_t count = 0, stringSize = 0; + size_t recordsEnd = 0; + if (!ValidateWdbc(base, kSkillFields, kSkillRecordSize, count, stringSize, recordsEnd)) + return false; + + std::vector records; + records.reserve(overrides.size() * kSkillRecordSize); + for (const Override& item : overrides) + { + const uint8_t* source = FindSkillRow(base, count, item.templateSpellId); + if (!source) return false; + const size_t recordStart = records.size(); + records.insert(records.end(), source, source + kSkillRecordSize); + uint8_t* row = records.data() + recordStart; + WriteU32(row, item.skillLineAbilityId); + WriteU32(row + 8, item.spellId); + } + + out.reserve(base.size() + records.size()); + out.insert(out.end(), base.begin(), base.begin() + 20); + WriteU32(out.data() + 4, count + static_cast(overrides.size())); + out.insert(out.end(), base.begin() + 20, base.begin() + recordsEnd); + out.insert(out.end(), records.begin(), records.end()); + out.insert(out.end(), base.begin() + recordsEnd, base.end()); + return true; + } + + void Load() + { + const std::string root = wxl::host::ClientRoot(); + wxl::host::mpq::MpqStore store; + std::vector overrideBytes, spellBase, skillBase; + if (root.empty() || !store.Mount(root) || !store.ReadAll(kOverridesPath, overrideBytes)) + return; + + std::vector overrides; + if (!ParseOverrides(overrideBytes, overrides) || + !store.ReadAll(kSpellPath, spellBase) || !store.ReadAll(kSkillPath, skillBase) || + !BuildSpellDbc(spellBase, overrides, g_spellDbc) || + !BuildSkillDbc(skillBase, overrides, g_skillDbc)) + { + WLOG_ERROR("moonwell-spells: failed to build custom companion DBCs"); + return; + } + g_ready = true; + WLOG_INFO("moonwell-spells: appended %zu companion spell(s)", overrides.size()); + } + + bool Provide(std::string_view name, std::vector& out) + { + if (!SamePath(name, kSpellPath) && !SamePath(name, kSkillPath)) return false; + std::call_once(g_loadOnce, &Load); + if (!g_ready) return false; + out = SamePath(name, kSpellPath) ? g_spellDbc : g_skillDbc; + return true; + } + + struct Registrar + { + Registrar() { wxl::host::RegisterProvider("moonwell-companion-spells", &Provide); } + }; + + Registrar g_registrar; + } +} diff --git a/tool/src/creatures.rs b/tool/src/creatures.rs index 50019fd..c0dee84 100644 --- a/tool/src/creatures.rs +++ b/tool/src/creatures.rs @@ -17,6 +17,12 @@ pub struct Registry { pub model_data_id_start: u32, #[serde(default = "default_display_id")] pub display_info_id_start: u32, + #[serde(default = "default_creature_entry")] + pub creature_entry_id_start: u32, + #[serde(default = "default_summon_spell_id")] + pub summon_spell_id_start: u32, + #[serde(default = "default_skill_line_ability_id")] + pub skill_line_ability_id_start: u32, pub defaults: Defaults, #[serde(default)] pub mounts: Vec, @@ -30,6 +36,15 @@ fn default_model_id() -> u32 { fn default_display_id() -> u32 { 50_000 } +fn default_creature_entry() -> u32 { + 5_000_000 +} +fn default_summon_spell_id() -> u32 { + 110_000 +} +fn default_skill_line_ability_id() -> u32 { + 30_000 +} #[derive(Debug, Deserialize)] pub struct Defaults { @@ -57,6 +72,8 @@ pub struct EntryOptions { pub collision_height: Option, pub mount_height: Option, pub flags: Option, + pub name: Option, + pub name_ru: Option, } impl EntrySpec { @@ -78,6 +95,8 @@ impl EntrySpec { collision_height: options.collision_height, mount_height: options.mount_height, flags: options.flags, + name: options.name.clone(), + name_ru: options.name_ru.clone(), }, } } @@ -93,6 +112,12 @@ struct LockFile { struct LockedIds { model_data_id: u32, display_info_id: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + creature_entry: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + summon_spell_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + skill_line_ability_id: Option, } #[derive(Debug)] @@ -106,6 +131,9 @@ struct PreparedEntry { template_display_id: u32, ids: LockedIds, options: EntryOptions, + summon: bool, + name: String, + name_ru: String, } pub struct GeneratedCreatures { @@ -113,6 +141,7 @@ pub struct GeneratedCreatures { pub file_data_csv: Vec, pub creature_model_data: Vec, pub creature_display_info: Vec, + pub spell_overrides: Vec, } #[derive(Clone)] @@ -318,36 +347,73 @@ fn collect_manifest_files(value: &Value, out: &mut Vec<(u32, String)>) { } } -fn allocate_ids(lock: &mut LockFile, key: &str, model_start: u32, display_start: u32) -> LockedIds { - if let Some(ids) = lock.models.get(key) { - return ids.clone(); +fn next_free(start: u32, used: &HashSet) -> u32 { + let mut value = start; + while used.contains(&value) { + value = value.checked_add(1).expect("custom ID range exhausted"); } + value +} + +fn allocate_ids(lock: &mut LockFile, key: &str, registry: &Registry, summon: bool) -> LockedIds { let used_models: HashSet = lock.models.values().map(|ids| ids.model_data_id).collect(); let used_displays: HashSet = lock .models .values() .map(|ids| ids.display_info_id) .collect(); - let mut model = model_start; - while used_models.contains(&model) { - model += 1; + let used_creatures: HashSet = lock + .models + .values() + .filter_map(|ids| ids.creature_entry) + .collect(); + let used_spells: HashSet = lock + .models + .values() + .filter_map(|ids| ids.summon_spell_id) + .collect(); + let used_skill_rows: HashSet = lock + .models + .values() + .filter_map(|ids| ids.skill_line_ability_id) + .collect(); + let mut ids = lock.models.get(key).cloned().unwrap_or(LockedIds { + model_data_id: next_free(registry.model_data_id_start, &used_models), + display_info_id: next_free(registry.display_info_id_start, &used_displays), + creature_entry: None, + summon_spell_id: None, + skill_line_ability_id: None, + }); + if summon { + if ids.creature_entry.is_none() { + ids.creature_entry = Some(next_free(registry.creature_entry_id_start, &used_creatures)); + } + if ids.summon_spell_id.is_none() { + ids.summon_spell_id = Some(next_free(registry.summon_spell_id_start, &used_spells)); + } + if ids.skill_line_ability_id.is_none() { + ids.skill_line_ability_id = Some(next_free( + registry.skill_line_ability_id_start, + &used_skill_rows, + )); + } } - let mut display = display_start; - while used_displays.contains(&display) { - display += 1; - } - let ids = LockedIds { - model_data_id: model, - display_info_id: display, - }; lock.models.insert(key.to_string(), ids.clone()); ids } +fn validate_text(value: &str, field: &str, key: &str) -> Result<(), Box> { + if value.contains(['\t', '\r', '\n']) { + return Err(format!("{} for '{}' contains a tab or newline", field, key).into()); + }; + Ok(()) +} + fn prepare_entry( project_root: &Path, spec: &EntrySpec, template_default: u32, + summon: bool, lock: &mut LockFile, registry: &Registry, ) -> Result> { @@ -357,6 +423,10 @@ fn prepare_entry( return Err(format!("creature folder not found: {}", folder.display()).into()); } let key = options.key.clone().unwrap_or(default_key(&folder)?); + let name = options.name.clone().unwrap_or_else(|| key.clone()); + let name_ru = options.name_ru.clone().unwrap_or_else(|| name.clone()); + validate_text(&name, "name", &key)?; + validate_text(&name_ru, "name_ru", &key)?; let archive = normalized_archive( options .archive @@ -386,12 +456,7 @@ fn prepare_entry( file_data.push((file_data_id, format!("{archive}/{file}"))); assets.push((source, format!("{archive}/{file}"))); } - let ids = allocate_ids( - lock, - &key, - registry.model_data_id_start, - registry.display_info_id_start, - ); + let ids = allocate_ids(lock, &key, registry, summon); Ok(PreparedEntry { key, folder, @@ -402,6 +467,9 @@ fn prepare_entry( template_display_id: options.template_display_id.unwrap_or(template_default), ids, options, + summon, + name, + name_ru, }) } @@ -489,6 +557,67 @@ fn generate_dbc( Ok((models.build(), displays.build())) } +fn sql_string(value: &str) -> String { + format!("'{}'", value.replace('\\', "\\\\").replace('\'', "''")) +} + +fn generate_server_sql(entries: &[PreparedEntry]) -> Result> { + let mut sql = String::from( + "-- Generated by MoonWell custom creature builder.\n\ + -- Apply to acore_world, then restart ac-worldserver.\n\ + START TRANSACTION;\n\n", + ); + for entry in entries.iter().filter(|entry| entry.summon) { + let creature = entry + .ids + .creature_entry + .ok_or("pet has no creature entry")?; + let spell = entry + .ids + .summon_spell_id + .ok_or("pet has no summon spell ID")?; + let skill_row = entry + .ids + .skill_line_ability_id + .ok_or("pet has no skill-line row ID")?; + let name_en = sql_string(&entry.name); + let name_ru = sql_string(&entry.name_ru); + let description_en = sql_string(&format!( + "Summons or dismisses your companion: {}.", + entry.name + )); + let description_ru = sql_string(&format!( + "Призывает или отпускает спутника: {}.", + entry.name_ru + )); + + sql.push_str(&format!( + "-- {}\n\ + INSERT INTO creature_template\n\ + (entry,name,minlevel,maxlevel,faction,speed_walk,speed_run,scale,unit_class,unit_flags,unit_flags2,type,MovementType,RegenHealth,VerifiedBuild)\n\ + VALUES ({creature},{name_en},1,1,188,1,1.14286,1,1,0,2048,12,0,1,12340)\n\ + ON DUPLICATE KEY UPDATE name=VALUES(name),faction=VALUES(faction),type=VALUES(type),unit_flags2=VALUES(unit_flags2);\n\ + INSERT INTO creature_template_model\n\ + (CreatureID,Idx,CreatureDisplayID,DisplayScale,Probability,VerifiedBuild)\n\ + VALUES ({creature},0,{},1,1,12340)\n\ + ON DUPLICATE KEY UPDATE CreatureDisplayID=VALUES(CreatureDisplayID),DisplayScale=VALUES(DisplayScale),Probability=VALUES(Probability);\n\n", + entry.key, entry.ids.display_info_id + )); + sql.push_str(&format!( + "INSERT INTO spell_dbc\n\ + (ID,Attributes,AttributesEx3,CastingTimeIndex,InterruptFlags,ProcChance,DurationIndex,RangeIndex,EquippedItemClass,Effect_1,EffectDieSides_1,EffectBasePoints_1,ImplicitTargetA_1,EffectMultipleValue_1,EffectMiscValue_1,EffectMiscValueB_1,SpellVisualID_1,SpellIconID,Name_Lang_enUS,Name_Lang_ruRU,Description_Lang_enUS,Description_Lang_ruRU,StartRecoveryCategory,StartRecoveryTime,EffectChainAmplitude_1,EffectChainAmplitude_2,EffectChainAmplitude_3,SchoolMask)\n\ + VALUES ({spell},262416,536870912,1,31,101,21,1,-1,28,1,1,32,1000,{creature},41,353,2686,{name_en},{name_ru},{description_en},{description_ru},133,1500,1,1,1,1)\n\ + ON DUPLICATE KEY UPDATE EffectMiscValue_1=VALUES(EffectMiscValue_1),Name_Lang_enUS=VALUES(Name_Lang_enUS),Name_Lang_ruRU=VALUES(Name_Lang_ruRU),Description_Lang_enUS=VALUES(Description_Lang_enUS),Description_Lang_ruRU=VALUES(Description_Lang_ruRU);\n\ + INSERT INTO skilllineability_dbc\n\ + (ID,SkillLine,Spell,RaceMask,ClassMask,ExcludeRace,ExcludeClass,MinSkillLineRank,SupercededBySpell,AcquireMethod,TrivialSkillLineRankHigh,TrivialSkillLineRankLow,CharacterPoints_1,CharacterPoints_2)\n\ + VALUES ({skill_row},778,{spell},0,0,0,0,1,0,0,0,0,0,0)\n\ + ON DUPLICATE KEY UPDATE SkillLine=VALUES(SkillLine),Spell=VALUES(Spell),MinSkillLineRank=VALUES(MinSkillLineRank);\n\n" + )); + } + sql.push_str("COMMIT;\n"); + Ok(sql) +} + pub fn prepare(project_root: &Path) -> Result, Box> { let registry_path = project_root.join("custom-creatures.json"); if !registry_path.is_file() { @@ -510,6 +639,7 @@ pub fn prepare(project_root: &Path) -> Result, Box Result, Box Result, Box Result, Box Result, Box Result, Box Result, Box Result, Box