diff --git a/CMakeLists.txt b/CMakeLists.txt index 68501de21..f5354e088 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -439,7 +439,23 @@ set(SOURCES src/gameplay/classes/mob_classes_info.cpp src/gameplay/classes/recalc_mob_params_by_vnum.cpp) +# Loot tables module sources (requires yaml-cpp via HAVE_YAML option) +set(LOOT_TABLES_SOURCES + src/gameplay/mechanics/loot_tables/loot_table_types.cpp + src/gameplay/mechanics/loot_tables/loot_table.cpp + src/gameplay/mechanics/loot_tables/loot_registry.cpp + src/gameplay/mechanics/loot_tables/loot_loader.cpp + src/gameplay/mechanics/loot_tables/loot_logger.cpp + src/engine/ui/cmd_god/do_loottable.cpp) +set(LOOT_TABLES_HEADERS + src/gameplay/mechanics/loot_tables/loot_table_types.h + src/gameplay/mechanics/loot_tables/loot_table.h + src/gameplay/mechanics/loot_tables/loot_registry.h + src/gameplay/mechanics/loot_tables/loot_loader.h + src/gameplay/mechanics/loot_tables/loot_logger.h + src/gameplay/mechanics/loot_tables/loot_tables.h + src/engine/ui/cmd_god/do_loottable.h) set(HEADERS src/engine/ui/cmd/do_flee.h @@ -1144,6 +1160,19 @@ else () message(STATUS "ZLib is turned off. Circle will NOT support MCCP.") endif () +# YAML support (for loot tables and world data) +option(HAVE_YAML "Enable YAML support for loot tables configuration." OFF) +if (HAVE_YAML) + find_package(yaml-cpp REQUIRED) + add_definitions(-DHAVE_YAML) + include_directories(${YAML_CPP_INCLUDE_DIRS}) + target_link_libraries(circle.library yaml-cpp) + target_sources(circle.library PRIVATE ${LOOT_TABLES_SOURCES} ${LOOT_TABLES_HEADERS}) + message(STATUS "YAML is turned ON. Circle will support loot tables and YAML configuration.") +else () + message(STATUS "YAML is turned off. Loot tables module disabled.") +endif () + # Iconv option(HAVE_ICONV "Allows to enable search of iconv." OFF) if (HAVE_ICONV) diff --git a/LOOT_TABLES_PLAN.md b/LOOT_TABLES_PLAN.md new file mode 100644 index 000000000..5a4d3c12d --- /dev/null +++ b/LOOT_TABLES_PLAN.md @@ -0,0 +1,607 @@ +# План реализации модуля Loot Tables для Bylins MUD + +## Обзор + +Создание модульной системы таблиц наград (loot tables) для замены существующих механизмов dead_load и GlobalDrop. Система будет поддерживать: +- Гибкие фильтры источников (мобы, квесты, контейнеры) +- Вероятностную генерацию с учетом MIW лимитов +- YAML формат конфигурационных файлов +- Hot-reload без перезапуска сервера +- Локальные таблицы зон и глобальные таблицы + +## Ключевые архитектурные решения + +### Формат данных: YAML +- **Решение**: Использовать YAML (как запрошено) вместо XML +- **Обоснование**: Удобнее для ручного редактирования билдерами +- **Реализация**: Добавить yaml-cpp в зависимости, создать адаптер для parser_wrapper + +### Приоритет таблиц +- **Локальные таблицы зон** дополняют глобальные (не заменяют) +- Локальные имеют priority=150 по умолчанию, глобальные=100 +- Все применимые таблицы сортируются по приоритету и применяются + +### Механика fallback при MIW лимите +- **Первичная вероятность**: weight из probability_width (например, 2000/10000 = 20%) +- **Вторичная вероятность**: fallback_weight из probability_width (например, 50/10000 = 0.5%) +- Если MIW достигнут, проверяется fallback_weight вместо weight + +### Повторный выбор предметов +- **Флаг allow_duplicates** в LootEntry (по умолчанию false) +- false: предмет выбирается максимум 1 раз за генерацию (без возврата) +- true: предмет может выпасть несколько раз (с возвратом) + +## Этапы реализации + +### Этап 1: Добавление yaml-cpp и базовые структуры (2-3 дня) + +**Файлы для создания:** +- `src/gameplay/mechanics/loot_tables/loot_table_types.h` - Энумы и структуры данных +- `src/gameplay/mechanics/loot_tables/loot_table_types.cpp` - Реализация методов + +**Изменения в существующих файлах:** +- `CMakeLists.txt` - добавить yaml-cpp зависимость и новые исходники + +**Структуры данных:** + +```cpp +namespace loot_tables { + +// Энумы +enum class ELootSourceType { kMobDeath, kContainer, kQuest }; +enum class ELootTableType { kSourceCentric, kItemCentric }; +enum class ELootFilterType { kVnum, kLevelRange, kRace, kClass, kFlags, kZone }; +enum class ELootEntryType { kItem, kCurrency, kNestedTable }; + +// Фильтр источника +struct LootFilter { + ELootFilterType type; + ObjVnum vnum; + int min_level, max_level; + int race, mob_class; + FlagData flags; + ZoneVnum zone_vnum; + + bool Matches(const CharData* mob) const; +}; + +// Запись в таблице +struct LootEntry { + ELootEntryType type; + ObjVnum item_vnum; // для kItem + int currency_id; // для kCurrency + std::string nested_table_id; // для kNestedTable + + int min_count, max_count; // количество + int probability_weight; // вес (из probability_width) + int fallback_weight; // вторичный вес при MIW лимите + bool allow_duplicates; // может выпасть несколько раз? + + // Фильтры игрока + int min_player_level, max_player_level; + std::vector allowed_classes; + + bool CheckPlayerFilters(const CharData* player) const; + int RollCount() const; +}; + +} +``` + +### Этап 2: Класс LootTable и генерация (2-3 дня) + +**Файлы для создания:** +- `src/gameplay/mechanics/loot_tables/loot_table.h` - Класс LootTable +- `src/gameplay/mechanics/loot_tables/loot_table.cpp` - Реализация генерации + +**Ключевой функционал:** + +```cpp +class LootTable { +private: + std::string id_; + ELootTableType table_type_; + ELootSourceType source_type_; + std::vector source_filters_; + std::vector entries_; + + int probability_width_{10000}; // база для вероятности + int max_items_per_roll_{10}; // макс предметов за раз + int priority_{100}; // приоритет таблицы + +public: + struct GenerationContext { + const CharData* player; + const CharData* source_mob; + int luck_bonus; + }; + + struct GeneratedLoot { + std::vector> items; + std::map currencies; + }; + + GeneratedLoot Generate(const GenerationContext& ctx) const; + bool IsApplicable(const CharData* mob) const; +}; +``` + +**Алгоритм генерации:** +1. Фильтрация записей по игроку (уровень, класс) +2. Weighted random выбор с учетом luck_bonus +3. Проверка MIW лимита: + - Если лимит не достигнут: сравниваем с probability_weight + - Если лимит достигнут: сравниваем с fallback_weight +4. Если allow_duplicates=false, удаляем запись из пула +5. Повторяем до max_items_per_roll + +### Этап 3: Реестр и индексация (2 дня) + +**Файлы для создания:** +- `src/gameplay/mechanics/loot_tables/loot_registry.h` - Класс LootTablesRegistry +- `src/gameplay/mechanics/loot_tables/loot_registry.cpp` - Реализация + +**Индексы для быстрого поиска:** +```cpp +class LootTablesRegistry { +private: + // SOURCE_CENTRIC индексы + std::multimap mob_vnum_index_; + std::multimap zone_index_; + std::vector filtered_tables_; // с фильтрами + + // ITEM_CENTRIC индексы + std::multimap item_vnum_index_; + + TableMap all_tables_; + +public: + std::vector FindTablesForMob(const CharData* mob) const; + GeneratedLoot GenerateFromMob(const CharData* mob, const CharData* killer, int luck) const; + void BuildIndices(); +}; +``` + +### Этап 4: YAML парсер и загрузчик (3-4 дня) + +**Файлы для создания:** +- `src/gameplay/mechanics/loot_tables/yaml_adapter.h` - Адаптер yaml-cpp к parser_wrapper +- `src/gameplay/mechanics/loot_tables/yaml_adapter.cpp` - Реализация +- `src/gameplay/mechanics/loot_tables/loot_loader.h` - ICfgLoader для loot tables +- `src/gameplay/mechanics/loot_tables/loot_loader.cpp` - Парсинг YAML + +**YAML адаптер:** +```cpp +// Расширение parser_wrapper для поддержки YAML +namespace parser_wrapper { + +class YamlDataNode : public DataNode { +private: + YAML::Node yaml_node_; + +public: + explicit YamlDataNode(const std::filesystem::path& yaml_file); + + // Реализация всех методов DataNode через YAML::Node + bool GoToChild(const std::string& name) override; + std::string GetValue(const std::string& attr) override; + // ... +}; + +} +``` + +**Парсинг таблиц:** +```cpp +class LootTablesLoader : public cfg_manager::ICfgLoader { +public: + void Load(parser_wrapper::DataNode data) override; + void Reload(parser_wrapper::DataNode data) override; + +private: + LootTable::TablePtr ParseTable(YAML::Node node); + LootFilter ParseFilter(YAML::Node node); + LootEntry ParseEntry(YAML::Node node); + void ValidateTable(const LootTable& table); + void ValidateNoCycles(); +}; +``` + +**Формат YAML:** +```yaml +# lib/cfg/loot_tables/global.yaml +tables: + - id: boss_dragon_loot + type: source_centric + source: mob_death + priority: 200 + + filters: + - type: vnum + vnum: 12345 + + params: + probability_width: 10000 + max_items: 5 + + entries: + - type: item + vnum: 50001 + count: {min: 1, max: 1} + weight: 10000 + fallback_weight: 0 + allow_duplicates: false + + - type: currency + currency_id: 0 # GOLD + count: {min: 5000, max: 10000} + weight: 3000 +``` + +### Этап 5: Интеграция с corpse.cpp (1-2 дня) + +**Файл для изменения:** +- `src/gameplay/mechanics/corpse.cpp` - интеграция в make_corpse() + +**Точка интеграции (после строки 516):** +```cpp +// В make_corpse() после создания ингредиентов +if (ch->IsNpc() && !IS_CHARMICE(ch)) { + // Новая система лута + auto& loot_registry = loot_tables::GetGlobalRegistry(); + + int luck_bonus = killer ? CalculateLuckBonus(killer) : 0; + auto generated = loot_registry.GenerateFromMob(ch, killer, luck_bonus); + + // Размещаем предметы + for (const auto& [vnum, count] : generated.items) { + for (int i = 0; i < count; ++i) { + auto obj = world_objects.create_from_prototype_by_vnum(vnum); + if (obj) { + obj->set_vnum_zone_from(GetZoneVnumByCharPlace(ch)); + PlaceObjIntoObj(obj.get(), corpse.get()); + } + } + } + + // Размещаем валюты + for (const auto& [currency_id, amount] : generated.currencies) { + AddCurrencyToCorpse(corpse.get(), currency_id, amount); + } +} +``` + +**Хелпер для валют:** +```cpp +void AddCurrencyToCorpse(ObjData* corpse, int currency_id, long amount) { + if (currency_id == currency::GOLD) { + const auto money = CreateCurrencyObj(amount); + PlaceObjIntoObj(money.get(), corpse); + } + else if (currency_id == currency::GLORY) { + // TODO: создать объект славы или добавить через другую систему + } + else if (currency_id == currency::TORC) { + // Торки - через ExtMoney::drop_torc() + } + // Другие валюты... +} +``` + +### Этап 6: Регистрация в CfgManager (1 день) + +**Файл для изменения:** +- `src/engine/boot/cfg_manager.cpp` - добавить загрузчик + +**Регистрация:** +```cpp +CfgManager::CfgManager() { + // ... существующие загрузчики ... + + loaders_.emplace("loot_tables", + LoaderInfo("cfg/loot_tables/global.yaml", + std::make_unique( + &loot_tables::GetGlobalRegistry() + ) + ) + ); +} +``` + +**Поддержка reload:** +``` +> reload loot_tables +LOOT_TABLES: загружено 45 таблиц из global.yaml +LOOT_TABLES: загружено 12 локальных таблиц из зон +Loot tables перезагружены. +``` + +### Этап 7: Команды для иммов (1 день) + +**Файл для создания:** +- `src/engine/ui/cmd_god/do_loottable.cpp` - команда loottable +- `src/engine/ui/cmd_god/do_loottable.h` - заголовок + +**Команды:** +``` +loottable list - список всех таблиц +loottable info - детали таблицы +loottable reload - перезагрузить таблицы +loottable test [luck] - тестовая генерация +loottable generate [luck] - генерация для моба +loottable stats - статистика использования +``` + +### Этап 8: Логирование и диагностика (1 день) + +**Файлы для создания:** +- `src/gameplay/mechanics/loot_tables/loot_logger.h` - класс LootLogger +- `src/gameplay/mechanics/loot_tables/loot_logger.cpp` - реализация + +**Типы логов:** +- Редкие дропы (weight < rare_threshold) +- MIW override (fallback сработал) +- Ошибки (циклы, невалидные vnum) + +**Формат:** +``` +[2026-01-28 18:00:00] RARE: boss_dragon_loot -> item[50002] x1 | killer: Гретта(25) | weight: 500/10000 +[2026-01-28 18:01:00] MIW_OVERRIDE: undead_loot -> item[60005] | fallback: 50/10000 | killer: Иван(30) +``` + +### Этап 9: Unit-тесты (2 дня) + +**Файл для создания:** +- `tests/loot_tables.cpp` - GoogleTest тесты + +**Тестовые сценарии:** +1. Фильтрация: LootFilter::Matches() для разных типов +2. Weighted random: проверка распределения (1000 итераций) +3. MIW проверка: корректность fallback механизма +4. Повторный выбор: allow_duplicates=true/false +5. Циклические ссылки: обнаружение nested table циклов +6. Парсинг YAML: валидация корректности загрузки + +### Этап 10: Миграция и документация (2-3 дня) + +**Файлы для создания:** +- `tools/convert_deadload_to_yaml.py` - конвертер старых DL в YAML +- `docs/loot_tables_guide.md` - руководство для билдеров + +**Конвертер:** +```python +# Парсит .mob файлы с DL записями +# Конвертирует в YAML формат +# Генерирует zone_XXX_converted.yaml +``` + +**Dual-mode период:** +- Флаг USE_NEW_LOOT_SYSTEM в конфиге +- Параллельная работа старой и новой системы +- A/B тестирование на тестовом сервере + +## Критические файлы для реализации + +1. **src/gameplay/mechanics/loot_tables/loot_table_types.h** - Фундамент: все энумы и структуры +2. **src/gameplay/mechanics/loot_tables/loot_table.cpp** - Ядро: алгоритм генерации +3. **src/gameplay/mechanics/loot_tables/loot_registry.cpp** - Производительность: индексация и поиск +4. **src/gameplay/mechanics/loot_tables/loot_loader.cpp** - Загрузка: парсинг YAML и валидация +5. **src/gameplay/mechanics/corpse.cpp** - Интеграция: применение лута при смерти моба + +## Структура файлов проекта + +``` +src/gameplay/mechanics/loot_tables/ +├── loot_table_types.h/cpp # Энумы, LootFilter, LootEntry +├── loot_table.h/cpp # LootTable класс с Generate() +├── loot_registry.h/cpp # LootTablesRegistry с индексами +├── yaml_adapter.h/cpp # Адаптер yaml-cpp к parser_wrapper +├── loot_loader.h/cpp # ICfgLoader для загрузки YAML +├── loot_logger.h/cpp # Логирование дропов +└── loot_tables.h # Публичный API + +src/engine/ui/cmd_god/ +└── do_loottable.cpp/h # Команда для иммов + +lib/cfg/loot_tables/ +├── global.yaml # Глобальные таблицы +├── bosses.yaml # Боссы +├── common.yaml # Общие (gems, currency) +└── seasonal.yaml # Сезонные события + +lib/world/ltt/ +├── 100.ltt.yaml # Локальные таблицы зоны 100 +├── 120.ltt.yaml # Локальные таблицы зоны 120 +└── ... + +tools/ +└── convert_deadload_to_yaml.py # Конвертер DL -> YAML + +tests/ +└── loot_tables.cpp # Unit-тесты +``` + +## Зависимости (CMakeLists.txt) + +```cmake +# Добавить yaml-cpp +include(FetchContent) +FetchContent_Declare( + yaml-cpp + GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git + GIT_TAG yaml-cpp-0.7.0 +) +FetchContent_MakeAvailable(yaml-cpp) + +# Добавить исходники +set(LOOT_TABLES_SOURCES + src/gameplay/mechanics/loot_tables/loot_table_types.cpp + src/gameplay/mechanics/loot_tables/loot_table.cpp + src/gameplay/mechanics/loot_tables/loot_registry.cpp + src/gameplay/mechanics/loot_tables/yaml_adapter.cpp + src/gameplay/mechanics/loot_tables/loot_loader.cpp + src/gameplay/mechanics/loot_tables/loot_logger.cpp +) + +# Линковка +target_link_libraries(circle PRIVATE yaml-cpp) +``` + +## Проверка реализации + +### Функциональные тесты: +1. Создать тестового моба с таблицей лута +2. Убить моба, проверить содержимое трупа +3. Проверить фильтры по уровню/классу игрока +4. Проверить MIW лимиты и fallback +5. Проверить nested tables (3 уровня вложенности) +6. Проверить reload без перезапуска сервера + +### Тесты производительности: +1. Генерация лута из 100 таблиц (должно < 1ms) +2. Поиск таблиц для моба (должно < 100μs) +3. Парсинг 1000 таблиц при загрузке (должно < 5s) + +### End-to-end тест: +``` +1. Запустить сервер +2. Загрузить персонажа уровня 30 (класс: warrior) +3. Убить моба vnum=12345 (босс дракон) +4. Проверить труп: + - Предмет 50001 (100% шанс) + - Золото 5000-10000 (30% шанс) + - Редкий предмет 50002 (5% шанс, если MIW позволяет) +5. Логи должны показать RARE drop если выпал 50002 +``` + +## Риски и митигация + +### Риск 1: Производительность +**Проблема**: Генерация на каждую смерть может быть медленной +**Митигация**: Индексы, кэширование весов, профилирование (порог 1ms) + +### Риск 2: Совместимость +**Проблема**: Существующие DG Scripts используют %load% +**Митигация**: Dual-mode, новая команда %loottable%, постепенная миграция + +### Риск 3: Балансировка +**Проблема**: Изменение системы может сломать баланс +**Митигация**: A/B тестирование, логирование всех дропов, команды для анализа + +### Риск 4: YAML encoding +**Проблема**: Файлы должны быть в KOI8-R, yaml-cpp работает с UTF-8 +**Митигация**: Конвертация при загрузке (iconv), либо хранить YAML в UTF-8 + +## Оценка времени + +| Этап | Время | Зависимости | +|------|-------|-------------| +| 1. Базовые структуры | 2-3 дня | - | +| 2. Генерация | 2-3 дня | Этап 1 | +| 3. Реестр | 2 дня | Этап 2 | +| 4. YAML парсер | 3-4 дня | Этап 1 | +| 5. Интеграция corpse | 1-2 дня | Этапы 2-3 | +| 6. CfgManager | 1 день | Этап 4 | +| 7. Команды иммов | 1 день | Этапы 2-3 | +| 8. Логирование | 1 день | Этап 2 | +| 9. Тесты | 2 дня | Все этапы | +| 10. Миграция | 2-3 дня | Все этапы | + +**Итого**: 17-22 рабочих дня (3.5-4.5 недели) + +## Примеры YAML конфигов + +### Простой дроп (100% шанс): +```yaml +tables: + - id: newbie_wolf_loot + type: source_centric + source: mob_death + filters: + - {type: vnum, vnum: 1001} + entries: + - type: item + vnum: 10001 # Шкура волка + count: {min: 1, max: 1} + weight: 10000 +``` + +### Множественные предметы: +```yaml +tables: + - id: treasure_chest + type: source_centric + source: container + filters: + - {type: vnum, vnum: 80001} + params: + probability_width: 10000 + max_items: 3 + entries: + - type: currency + currency_id: 0 # GOLD + count: {min: 500, max: 1000} + weight: 8000 + - type: item + vnum: 50001 # Редкий меч (5%) + weight: 500 + fallback_weight: 50 # 0.5% если MIW достигнут + - type: item + vnum: 50002 # Щит (30%) + weight: 3000 +``` + +### Nested tables: +```yaml +tables: + - id: gems_common + type: source_centric + source: mob_death + entries: + - {type: item, vnum: 60001, count: {min: 1, max: 3}, weight: 5000, allow_duplicates: true} + - {type: item, vnum: 60002, count: {min: 1, max: 2}, weight: 3000, allow_duplicates: true} + + - id: dragon_boss_loot + type: source_centric + source: mob_death + filters: + - {type: vnum, vnum: 99999} + entries: + - {type: item, vnum: 99001, weight: 10000} + - {type: nested_table, table_id: gems_common, weight: 8000} +``` + +### Фильтры по игроку: +```yaml +tables: + - id: class_specific_loot + type: source_centric + source: mob_death + filters: + - {type: level_range, min: 25, max: 35} + - {type: race, race: 14} # UNDEAD + entries: + - type: item + vnum: 70001 # Меч для воинов + weight: 5000 + player_filters: + level: {min: 25, max: 50} + classes: [1, 4] # Warrior, Paladin + - type: item + vnum: 70002 # Посох для магов + weight: 5000 + player_filters: + classes: [2, 3] # Mage, Cleric +``` + +## Резюме + +Модуль loot tables заменит существующие системы dead_load и GlobalDrop единым гибким решением. Ключевые преимущества: + +✅ **Гибкость**: Множественные фильтры, nested tables, условия по игроку +✅ **Производительность**: Индексация, кэширование, оптимизированный поиск +✅ **Удобство**: YAML конфиги, hot-reload, команды для иммов +✅ **Надежность**: Валидация, обнаружение циклов, логирование +✅ **Расширяемость**: Легко добавлять новые типы фильтров/записей + +План предусматривает постепенное внедрение с сохранением обратной совместимости и тщательным тестированием. diff --git a/data/LOOT_TABLES_GUIDE.md b/data/LOOT_TABLES_GUIDE.md new file mode 100644 index 000000000..f777066eb --- /dev/null +++ b/data/LOOT_TABLES_GUIDE.md @@ -0,0 +1,306 @@ +# Loot Tables Configuration Guide + +This guide explains how to configure the loot tables system in Bylins MUD. + +## Overview + +The loot tables system allows you to define what items and currencies drop from mobs when they die. Configuration is done through YAML files in the `lib/cfg/loot_tables/` directory. + +## Directory Structure + +``` +lib/cfg/loot_tables/ +├── example.yaml # Example configuration +├── zone40.yaml # Zone 40 loot tables +└── common_drops.yaml # Shared loot pools +``` + +All `.yaml` files in this directory are automatically loaded at server startup and can be reloaded with the `loottable reload` command. + +## Basic Concepts + +### Table Types + +1. **Source-centric tables** (`type: source_centric`) - Tables attached to specific mobs, defining what they drop. +2. **Item-centric tables** (`type: item_centric`) - Tables defining where specific items drop. + +### Entry Types + +- **item** - Drops a specific item by vnum +- **currency** - Drops currency (gold, glory, ice, nogata) +- **nested_table** - References another loot table for reusable loot pools + +### Filters + +Filters determine which mobs a loot table applies to: + +- **vnum** - Match exact mob vnum +- **level_range** - Match mobs within level range +- **zone** - Match all mobs in a zone +- **race** - Match mobs by race + +### Probability + +Each entry has a `weight` that determines its relative drop chance. The `probability_width` of the table controls how likely any drop is: + +``` +chance_per_entry = weight / probability_width +``` + +If `probability_width = 1000` and entry `weight = 100`, the entry has 10% chance. + +## YAML Configuration Structure + +```yaml +# Table identifier (must be unique) +id: "zone40_bandit_drops" + +# Optional description +description: "Loot for bandits in zone 40" + +# Table type: source_centric or item_centric +type: source_centric + +# Probability divisor (higher = rarer drops) +probability_width: 1000 + +# Whether generation can produce empty result +allow_empty: true + +# Maximum items per generation +max_items: 3 + +# Filters for which mobs this table applies to +source_filters: + - type: vnum + vnum: 4000 + - type: zone + zone: 40 + - type: level_range + min_level: 10 + max_level: 20 + +# Loot entries +entries: + - type: item + vnum: 1234 + weight: 100 + count: + min: 1 + max: 1 + + - type: currency + currency: gold + weight: 500 + count: + min: 10 + max: 50 + + - type: nested_table + table_id: "common_drops" + weight: 200 +``` + +## Example: Zone 40 Configuration + +Zone 40 contains training mobs like bandits (razboniki), peasants (krestyane), bears (medved), geese (gusi), and chickens (kury). + +### zone40.yaml + +```yaml +# Bandit Leader (vnum 4000) +--- +id: "zone40_bandit_leader" +description: "Drops for the bandit leader" +type: source_centric +probability_width: 100 +allow_empty: true +max_items: 2 +source_filters: + - type: vnum + vnum: 4000 +entries: + - type: currency + currency: gold + weight: 80 + count: + min: 50 + max: 100 + - type: item + vnum: 4001 # Example: weapon + weight: 15 + - type: item + vnum: 4002 # Example: armor piece + weight: 10 + +# All bandits in zone (vnums 4000-4010) +--- +id: "zone40_bandits_common" +description: "Common drops for all bandits" +type: source_centric +probability_width: 1000 +allow_empty: true +max_items: 1 +source_filters: + - type: zone + zone: 40 + - type: level_range + min_level: 10 + max_level: 20 +entries: + - type: currency + currency: gold + weight: 300 + count: + min: 5 + max: 25 + - type: nested_table + table_id: "common_weapons" + weight: 50 + +# Animals (geese, chickens, rabbits) +--- +id: "zone40_animals" +description: "Drops from animals" +type: source_centric +probability_width: 100 +allow_empty: true +source_filters: + - type: level_range + min_level: 1 + max_level: 5 + - type: zone + zone: 40 +entries: + - type: item + vnum: 4010 # Meat + weight: 60 + count: + min: 1 + max: 2 + - type: item + vnum: 4011 # Feathers + weight: 30 +``` + +### Shared Loot Pools + +Create `common_drops.yaml` for reusable loot: + +```yaml +--- +id: "common_weapons" +description: "Basic weapons that can drop anywhere" +type: source_centric +probability_width: 100 +allow_empty: true +entries: + - type: item + vnum: 100 # Rusty sword + weight: 40 + - type: item + vnum: 101 # Old dagger + weight: 35 + - type: item + vnum: 102 # Wooden club + weight: 25 + +--- +id: "common_armor" +description: "Basic armor pieces" +type: source_centric +probability_width: 100 +allow_empty: true +entries: + - type: item + vnum: 200 # Leather cap + weight: 30 + - type: item + vnum: 201 # Cloth shirt + weight: 40 + - type: item + vnum: 202 # Worn boots + weight: 30 +``` + +## Player Filters + +You can restrict drops based on player characteristics: + +```yaml +entries: + - type: item + vnum: 5000 # Epic sword + weight: 10 + player_filters: + min_level: 30 + max_level: 0 # No max + allowed_classes: + - 1 # Warrior + - 5 # Paladin +``` + +## Admin Commands + +The `loottable` command is available to implementors: + +``` +loottable list - List all loaded loot tables +loottable info - Show table details +loottable reload - Reload all loot tables from files +loottable test - Test generation from a table +loottable stats - Show usage statistics +loottable errors - Show loading errors +``` + +## MIW (Max-In-World) Support + +Items can respect MIW limits. If the item count in world exceeds the limit, it won't drop: + +```yaml +entries: + - type: item + vnum: 9999 # Rare item with MIW limit + weight: 5 + respect_miw: true # Won't drop if MIW exceeded +``` + +## Best Practices + +1. **Organize by zone** - Create separate files for each zone +2. **Use nested tables** - Create shared pools for common drops +3. **Balance probability_width** - Higher values = rarer drops +4. **Test your tables** - Use `loottable test` before going live +5. **Start with allow_empty: true** - Prevents guaranteed drops on every kill +6. **Use level filters** - Ensure appropriate drops for mob difficulty + +## Troubleshooting + +### Tables not loading + +1. Check YAML syntax with a validator +2. Run `loottable errors` to see parsing errors +3. Ensure files have `.yaml` extension +4. Check file permissions + +### No drops happening + +1. Verify `probability_width` isn't too high +2. Check `allow_empty` setting +3. Verify source filters match your mobs +4. Use `loottable test` to debug + +### Cycle detection errors + +Nested tables cannot reference each other in a cycle. Check your `nested_table` references. + +## Building with YAML Support + +To enable the loot tables system, build with: + +```bash +cmake -DHAVE_YAML=ON .. +make +``` + +Requires: `libyaml-cpp-dev` package. diff --git a/lib.template/cfg/loot_tables/zone40.yaml b/lib.template/cfg/loot_tables/zone40.yaml new file mode 100644 index 000000000..3bc871e8f --- /dev/null +++ b/lib.template/cfg/loot_tables/zone40.yaml @@ -0,0 +1,118 @@ +# Loot tables for Zone 40 - Training area +# Contains: bandits (razboniki), peasants, animals + +# Bandit Leader (ataman) - vnum 4000 +--- +id: "zone40_ataman" +description: "Loot for the bandit leader (ataman razbonikov)" +type: source_centric +probability_width: 100 +allow_empty: true +max_items: 2 +source_filters: + - type: vnum + vnum: 4000 +entries: + - type: currency + currency: gold + weight: 70 + count: + min: 30 + max: 80 + - type: item + vnum: 4001 + weight: 20 + - type: item + vnum: 4002 + weight: 15 + +# Regular bandits - vnums 4001-4002 +--- +id: "zone40_bandits" +description: "Loot for regular bandits" +type: source_centric +probability_width: 100 +allow_empty: true +max_items: 1 +source_filters: + - type: vnum + vnum: 4001 + - type: vnum + vnum: 4002 +entries: + - type: currency + currency: gold + weight: 60 + count: + min: 5 + max: 20 + - type: item + vnum: 4003 + weight: 10 + +# Bear (medved) - typically higher level +--- +id: "zone40_bear" +description: "Bear loot - meat and pelts" +type: source_centric +probability_width: 100 +allow_empty: false +source_filters: + - type: level_range + min_level: 8 + max_level: 15 + - type: zone + zone: 40 +entries: + - type: item + vnum: 4010 + weight: 80 + count: + min: 2 + max: 4 + - type: item + vnum: 4011 + weight: 40 + +# Small animals - geese, chickens, rabbits +--- +id: "zone40_small_animals" +description: "Loot from small animals" +type: source_centric +probability_width: 100 +allow_empty: true +source_filters: + - type: level_range + min_level: 1 + max_level: 5 + - type: zone + zone: 40 +entries: + - type: item + vnum: 4020 + weight: 50 + count: + min: 1 + max: 2 + - type: item + vnum: 4021 + weight: 30 + +# Zone-wide rare drops for all mobs in zone 40 +--- +id: "zone40_rare" +description: "Rare drops for any mob in zone 40" +type: source_centric +probability_width: 10000 +allow_empty: true +max_items: 1 +source_filters: + - type: zone + zone: 40 +entries: + - type: item + vnum: 4099 + weight: 5 + - type: nested_table + table_id: "zone40_ataman" + weight: 1 diff --git a/src/engine/boot/cfg_manager.cpp b/src/engine/boot/cfg_manager.cpp index 370d5ea6b..0a95ec576 100644 --- a/src/engine/boot/cfg_manager.cpp +++ b/src/engine/boot/cfg_manager.cpp @@ -1,9 +1,9 @@ /* \authors Created by Sventovit \date 14.02.2022. - \brief Менеджер файлов конфигурации. - \details Менеджер файлов конфигурации должен хранить информацию об именах и местоположении файлов конфигурации, - порядке их загрузки и управлять процессом загрузки данных из файлов по контейнерам. + \brief О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫. + \details О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫, + О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫. */ #include "cfg_manager.h" @@ -17,6 +17,11 @@ #include "gameplay/mechanics/guilds.h" #include "gameplay/skills/skills_info.h" +#ifdef HAVE_YAML +#include "gameplay/mechanics/loot_tables/loot_tables.h" +#include "gameplay/mechanics/loot_tables/loot_loader.h" +#endif + namespace cfg_manager { CfgManager::CfgManager() { @@ -37,11 +42,16 @@ CfgManager::CfgManager() { std::make_unique(guilds::GuildsLoader()))); loaders_.emplace("mob_classes", LoaderInfo("cfg/mob_classes.xml", std::make_unique(mob_classes::MobClassesLoader()))); + +#ifdef HAVE_YAML + loaders_.emplace("loot_tables", LoaderInfo("cfg/loot_tables", + std::make_unique(&loot_tables::GetGlobalRegistry()))); +#endif } void CfgManager::ReloadCfg(const std::string &id) { if (!loaders_.contains(id)) { - err_log("Неверный параметр перезагрузки файла конфигурации (%s)", id.c_str()); + err_log("О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ (%s)", id.c_str()); return; } const auto &loader_info = loaders_.at(id); @@ -51,7 +61,7 @@ void CfgManager::ReloadCfg(const std::string &id) { void CfgManager::LoadCfg(const std::string &id) { if (!loaders_.contains(id)) { - err_log("Неверный параметр загрузки файла конфигурации (%s)", id.c_str()); + err_log("О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ (%s)", id.c_str()); return; } const auto &loader_info = loaders_.at(id); diff --git a/src/engine/core/iosystem.cpp b/src/engine/core/iosystem.cpp index 7a6597cae..e51e2b9ce 100644 --- a/src/engine/core/iosystem.cpp +++ b/src/engine/core/iosystem.cpp @@ -2,8 +2,8 @@ \file iosystem.cpp - a part of the Bylins engine. \authors Created by Sventovit. \date 15.09.2024. -\brief Система ввода-вывода. -\detail Сетевой ввод-вывод: получение команд от пользователей и отправка ответов сервера. +\brief О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫-О©╫О©╫О©╫О©╫О©╫О©╫. +\detail О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫-О©╫О©╫О©╫О©╫О©╫: О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫. */ #include "engine/core/iosystem.h" @@ -182,7 +182,7 @@ int process_input(DescriptorData *t) { read_point = t->inbuf + buf_length; space_left = kMaxRawInputLength - buf_length - 1; - // с переходом на ивенты это необходимо для предотвращения некоторых маловероятных крешей + // О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ if (t == nullptr) { log("%s", fmt::format("SYSERR: NULL descriptor in {}() at {}:{}", __func__, __FILE__, __LINE__).c_str()); @@ -216,11 +216,11 @@ int process_input(DescriptorData *t) { } if (ptr[1] == (char) IAC) { - // последовательность IAC IAC - // следует заменить просто на один IAC, но - // для раскладок kCodePageWin/kCodePageWinz это произойдет ниже. - // Почему так сделано - не знаю, но заменять не буду. - // II: потому что второй IAC может прочитаться в другом socket_read + // О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ IAC IAC + // О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫ IAC, О©╫О©╫ + // О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ kCodePageWin/kCodePageWinz О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫. + // О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ - О©╫О©╫ О©╫О©╫О©╫О©╫, О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫. + // II: О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ IAC О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ socket_read ++ptr; } else if (ptr[1] == (char) DO) { switch (ptr[2]) { @@ -341,14 +341,14 @@ int process_input(DescriptorData *t) { space_left = kMaxInputLength - 1; for (ptr = read_point; (space_left > 1) && (ptr < nl_pos); ptr++) { - // Нафиг точку с запятой - задрали уроды с тригерами (Кард) + // О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ - О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ (О©╫О©╫О©╫О©╫) if (*ptr == ';' && (t->state == EConState::kPlaying || t->state == EConState::kExdesc || t->state == EConState::kWriteboard || t->state == EConState::kWriteNote || t->state == EConState::kWriteMod)) { - // Иммам или морталам с EGodFlag::DEMIGOD разрешено использовать ";". + // О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ EGodFlag::DEMIGOD О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ ";". if (GetRealLevel(t->character) < kLvlImmortal && !GET_GOD_FLAG(t->character, EGf::kDemigod)) *ptr = ','; } @@ -417,12 +417,12 @@ int process_input(DescriptorData *t) { space_left--; } - // Для того чтобы работали все триги в старом zMUD, заменяем все вводимые 'z' на 'я' - // Увы, это кое-что ломает, напр. wizhelp, или "г я использую zMUD" + // О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ zMUD, О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ 'z' О©╫О©╫ 'О©╫' + // О©╫О©╫О©╫, О©╫О©╫О©╫ О©╫О©╫О©╫-О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫, О©╫О©╫О©╫О©╫. wizhelp, О©╫О©╫О©╫ "О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ zMUD" if (t->state == EConState::kPlaying || (t->state == EConState::kExdesc)) { if (t->keytable == kCodePageWinzZ || t->keytable == kCodePageWinzOld) { if (*(write_point - 1) == 'z') { - *(write_point - 1) = 'я'; + *(write_point - 1) = 'О©╫'; } } } @@ -455,18 +455,18 @@ int process_input(DescriptorData *t) { } if (t->snoop_by) { iosystem::write_to_output("<< ", t->snoop_by); -// iosystem::write_to_output("% ", t->snoop_by); Попытаюсь сделать вменяемый вывод снупаемого трафика в отдельное окно +// iosystem::write_to_output("% ", t->snoop_by); О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ iosystem::write_to_output(tmp, t->snoop_by); iosystem::write_to_output("\r\n", t->snoop_by); } failed_subst = 0; if ((tmp[0] == '~') && (tmp[1] == 0)) { - // очистка входной очереди + // О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ int dummy; tilde = 1; while (get_from_q(&t->input, buf2, &dummy)); - iosystem::write_to_output("Очередь очищена.\r\n", t); + iosystem::write_to_output("О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫.\r\n", t); tmp[0] = 0; } else if (*tmp == '!' && !(*(tmp + 1))) // Redo last command. @@ -696,19 +696,19 @@ bool write_to_descriptor_with_options(DescriptorData *t, const char *buffer, siz * the player's descriptor. */ -// \TODO Никакой чардаты тут быть не должно. Нужно сделать флаги/режимы для аккаунта или дескриптора +// \TODO О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫. О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫/О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ int process_output(DescriptorData *t) { char i[kMaxSockBuf * 2], o[kMaxSockBuf * 2 * 3], *pi, *po; int written = 0, offset, result; - // с переходом на ивенты это необходимо для предотвращения некоторых маловероятных крешей + // О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ if (t == nullptr) { log("%s", fmt::format("SYSERR: NULL descriptor in {}() at {}:{}", __func__, __FILE__, __LINE__).c_str()); return -1; } - // Отправляю данные снуперам + // О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ // handle snooping: prepend "% " and send to snooper if (t->output && t->snoop_by) { iosystem::write_to_output("% ", t->snoop_by); @@ -726,7 +726,7 @@ int process_output(DescriptorData *t) { // if we're in the overflow state, notify the user if (t->bufptr == ~0ull) { - strcat(i, "***ПЕРЕПОЛНЕНИЕ***\r\n"); + strcat(i, "***О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫***\r\n"); } // add the extra CRLF if the person isn't in compact mode @@ -735,8 +735,8 @@ int process_output(DescriptorData *t) { strcat(i, "\r\n"); } else if (t->state == EConState::kPlaying && t->character && !t->character->IsNpc() && t->character->IsFlagged(EPrf::kCompact)) { - // added by WorM (Видолюб) - //фикс сжатого режима добавляет в конец строки \r\n если его там нету, чтобы промпт был всегда на след. строке + // added by WorM (О©╫О©╫О©╫О©╫О©╫О©╫О©╫) + //О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ \r\n О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫, О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫. О©╫О©╫О©╫О©╫О©╫О©╫ for (size_t c = strlen(i) - 1; c > 0; c--) { if (*(i + c) == '\n' || *(i + c) == '\r') break; @@ -879,8 +879,11 @@ ssize_t perform_socket_write(socket_t desc, const char *txt, size_t length) #define write socketwrite #endif -#if defined(__APPLE__) || defined(__MACH__) || defined(__CYGWIN__) +#if defined(CIRCLE_UNIX) #include +#endif + +#if defined(__APPLE__) || defined(__MACH__) || defined(__CYGWIN__) # ifndef MSG_NOSIGNAL # define MSG_NOSIGNAL SO_NOSIGPIPE # endif @@ -983,7 +986,7 @@ int mccp_start(DescriptorData *t, int ver) { int derr; if (t->deflate) { - return 1; // компрессия уже включена + return 1; // О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ } // Set up zlib structures. @@ -1076,14 +1079,14 @@ void zlib_free(void * /*opaque*/, void *address) { #endif -// \TODO Вообще, этому скорей место в UI. Надо подумать, как перенести. +// \TODO О©╫О©╫О©╫О©╫О©╫О©╫, О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫ UI. О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫, О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫. std::string MakePrompt(DescriptorData *d) { const auto& ch = d->character; auto out = fmt::memory_buffer(); out.reserve(kMaxPromptLength); if (d->showstr_count) { fmt::format_to(std::back_inserter(out), - "\rЛистать : , Q<К>онец, R<П>овтор, B<Н>азад, или номер страницы ({}/{}).", + "\rО©╫О©╫О©╫О©╫О©╫О©╫О©╫ : , Q<О©╫>О©╫О©╫О©╫О©╫, R<О©╫>О©╫О©╫О©╫О©╫О©╫, B<О©╫>О©╫О©╫О©╫О©╫, О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ ({}/{}).", d->showstr_page, d->showstr_count); } else if (d->writer) { fmt::format_to(std::back_inserter(out), "] "); @@ -1104,7 +1107,7 @@ std::string MakePrompt(DescriptorData *d) { if (ch->IsFlagged(EPrf::kDispMana) && IS_MANA_CASTER(ch)) { int current_mana = 100 * ch->mem_queue.stored; - fmt::format_to(std::back_inserter(out), "{}э{}{} ", + fmt::format_to(std::back_inserter(out), "{}О©╫{}{} ", GetColdValueColor(current_mana, GET_MAX_MANA((ch).get())), ch->mem_queue.stored, kColorNrm); } @@ -1125,12 +1128,12 @@ std::string MakePrompt(DescriptorData *d) { sec_hp = sec_hp*60/mana_gain; int ch_hp = sec_hp/60; sec_hp %= 60; - fmt::format_to(std::back_inserter(out), "Зауч:{}:{:02} ", ch_hp, sec_hp); + fmt::format_to(std::back_inserter(out), "О©╫О©╫О©╫О©╫:{}:{:02} ", ch_hp, sec_hp); } else { - fmt::format_to(std::back_inserter(out), "Зауч:- "); + fmt::format_to(std::back_inserter(out), "О©╫О©╫О©╫О©╫:- "); } } else { - fmt::format_to(std::back_inserter(out), "Зауч:0 "); + fmt::format_to(std::back_inserter(out), "О©╫О©╫О©╫О©╫:0 "); } } @@ -1180,8 +1183,8 @@ std::string MakePrompt(DescriptorData *d) { } if (ch->IsFlagged(EPrf::kDispExits)) { - static const char *dirs[] = {"С", "В", "Ю", "З", "^", "v"}; - fmt::format_to(std::back_inserter(out), "Вых:"); + static const char *dirs[] = {"О©╫", "О©╫", "О©╫", "О©╫", "^", "v"}; + fmt::format_to(std::back_inserter(out), "О©╫О©╫О©╫:"); if (!AFF_FLAGGED(ch, EAffect::kBlind)) { for (auto dir = 0; dir < EDirection::kMaxDirNum; ++dir) { if (EXIT(ch, dir) && EXIT(ch, dir)->to_room() != kNowhere && @@ -1213,18 +1216,18 @@ std::string MakePrompt(DescriptorData *d) { } char *show_state(CharData *ch, CharData *victim) { - static const char *WORD_STATE[12] = {"Смертельно ранен", - "О.тяжело ранен", - "О.тяжело ранен", - "Тяжело ранен", - "Тяжело ранен", - "Ранен", - "Ранен", - "Ранен", - "Легко ранен", - "Легко ранен", - "Слегка ранен", - "Невредим" + static const char *WORD_STATE[12] = {"О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫", + "О©╫.О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫", + "О©╫.О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫", + "О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫", + "О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫", + "О©╫О©╫О©╫О©╫О©╫", + "О©╫О©╫О©╫О©╫О©╫", + "О©╫О©╫О©╫О©╫О©╫", + "О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫", + "О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫", + "О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫", + "О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫" }; const int ch_hp = posi_value(victim->get_hit(), victim->get_real_max_hit()) + 1; diff --git a/src/engine/ui/cmd_god/do_loottable.cpp b/src/engine/ui/cmd_god/do_loottable.cpp new file mode 100644 index 000000000..ba2dfa066 --- /dev/null +++ b/src/engine/ui/cmd_god/do_loottable.cpp @@ -0,0 +1,305 @@ +// +// Created for Bylins MUD - Loot Tables Module +// + +#include "do_loottable.h" + +#ifdef HAVE_YAML + +#include "gameplay/mechanics/loot_tables/loot_tables.h" +#include "engine/entities/char_data.h" +#include "engine/db/obj_prototypes.h" +#include "utils/utils.h" +#include "utils/utils_string.h" + +#include + +namespace { + +void ShowHelp(CharData *ch) { + SendMsgToChar(ch, + "Usage: loottable [args]\r\n" + "Commands:\r\n" + " list - List all loot tables\r\n" + " info - Show table details\r\n" + " reload - Reload all loot tables\r\n" + " test [luck] - Test generation from table\r\n" + " generate [luck] - Generate loot for mob vnum\r\n" + " stats - Show usage statistics\r\n" + " errors - Show loading errors\r\n" + ); +} + +void ListTables(CharData *ch) { + auto ®istry = loot_tables::GetGlobalRegistry(); + const auto &tables = registry.GetAllTables(); + + if (tables.empty()) { + SendMsgToChar(ch, "No loot tables loaded.\r\n"); + return; + } + + std::ostringstream ss; + ss << "Loot tables (" << tables.size() << " total):\r\n"; + ss << "----------------------------------------\r\n"; + + for (const auto &[id, table] : tables) { + ss << " " << id << " (priority: " << table->GetPriority() + << ", entries: " << table->GetEntries().size() << ")\r\n"; + } + + SendMsgToChar(ch, "%s", ss.str().c_str()); +} + +void ShowTableInfo(CharData *ch, const char *table_id) { + auto ®istry = loot_tables::GetGlobalRegistry(); + auto table = registry.GetTable(table_id); + + if (!table) { + SendMsgToChar(ch, "Table '%s' not found.\r\n", table_id); + return; + } + + std::ostringstream ss; + ss << "Table: " << table->GetId() << "\r\n"; + ss << "----------------------------------------\r\n"; + ss << "Type: " << (table->GetTableType() == loot_tables::ELootTableType::kSourceCentric + ? "source_centric" : "item_centric") << "\r\n"; + ss << "Source: "; + switch (table->GetSourceType()) { + case loot_tables::ELootSourceType::kMobDeath: ss << "mob_death"; break; + case loot_tables::ELootSourceType::kContainer: ss << "container"; break; + case loot_tables::ELootSourceType::kQuest: ss << "quest"; break; + } + ss << "\r\n"; + ss << "Priority: " << table->GetPriority() << "\r\n"; + ss << "Probability width: " << table->GetProbabilityWidth() << "\r\n"; + ss << "Max items per roll: " << table->GetMaxItemsPerRoll() << "\r\n"; + + ss << "\r\nFilters (" << table->GetFilters().size() << "):\r\n"; + for (const auto &filter : table->GetFilters()) { + ss << " - "; + switch (filter.type) { + case loot_tables::ELootFilterType::kVnum: + ss << "vnum: " << filter.vnum; + break; + case loot_tables::ELootFilterType::kLevelRange: + ss << "level: " << filter.min_level << "-" << filter.max_level; + break; + case loot_tables::ELootFilterType::kZone: + ss << "zone: " << filter.zone_vnum; + break; + case loot_tables::ELootFilterType::kRace: + ss << "race: " << filter.race; + break; + case loot_tables::ELootFilterType::kClass: + ss << "class: " << filter.mob_class; + break; + default: + ss << "unknown"; + } + ss << "\r\n"; + } + + ss << "\r\nEntries (" << table->GetEntries().size() << "):\r\n"; + for (const auto &entry : table->GetEntries()) { + ss << " - "; + switch (entry.type) { + case loot_tables::ELootEntryType::kItem: { + auto proto = obj_proto[obj_proto.get_rnum(entry.item_vnum)]; + ss << "item " << entry.item_vnum; + if (proto) { + ss << " (" << proto->get_short_description() << ")"; + } + break; + } + case loot_tables::ELootEntryType::kCurrency: + ss << "currency " << entry.currency_id; + break; + case loot_tables::ELootEntryType::kNestedTable: + ss << "nested_table " << entry.nested_table_id; + break; + } + ss << " | weight: " << entry.probability_weight; + if (entry.fallback_weight > 0) { + ss << " (fallback: " << entry.fallback_weight << ")"; + } + ss << " | count: " << entry.count.min << "-" << entry.count.max; + if (entry.allow_duplicates) { + ss << " [duplicates]"; + } + ss << "\r\n"; + } + + SendMsgToChar(ch, "%s", ss.str().c_str()); +} + +void ReloadTables(CharData *ch) { + loot_tables::Reload(); + + auto ®istry = loot_tables::GetGlobalRegistry(); + const auto &errors = loot_tables::GetLoadErrors(); + + if (errors.empty()) { + SendMsgToChar(ch, "Loot tables reloaded successfully. %zu tables loaded.\r\n", + registry.GetTableCount()); + } else { + SendMsgToChar(ch, "Loot tables reloaded with %zu errors. %zu tables loaded.\r\n", + errors.size(), registry.GetTableCount()); + for (const auto &error : errors) { + SendMsgToChar(ch, " Error: %s\r\n", error.c_str()); + } + } +} + +void TestTable(CharData *ch, const char *table_id, int luck) { + auto ®istry = loot_tables::GetGlobalRegistry(); + auto table = registry.GetTable(table_id); + + if (!table) { + SendMsgToChar(ch, "Table '%s' not found.\r\n", table_id); + return; + } + + loot_tables::GenerationContext ctx; + ctx.player = ch; + ctx.source_mob = nullptr; + ctx.luck_bonus = luck; + + auto loot = table->Generate(ctx, ®istry, nullptr); + + if (loot.IsEmpty()) { + SendMsgToChar(ch, "Test generation produced no loot.\r\n"); + return; + } + + std::ostringstream ss; + ss << "Test generation from '" << table_id << "' (luck: " << luck << "):\r\n"; + + for (const auto &[vnum, count] : loot.items) { + auto proto = obj_proto[obj_proto.get_rnum(vnum)]; + ss << " Item " << vnum; + if (proto) { + ss << " (" << proto->get_short_description() << ")"; + } + ss << " x" << count << "\r\n"; + } + + for (const auto &[currency_id, amount] : loot.currencies) { + ss << " Currency " << currency_id << ": " << amount << "\r\n"; + } + + SendMsgToChar(ch, "%s", ss.str().c_str()); +} + +void ShowStats(CharData *ch) { + auto ®istry = loot_tables::GetGlobalRegistry(); + const auto &stats = registry.GetStats(); + + std::ostringstream ss; + ss << "Loot tables statistics:\r\n"; + ss << "----------------------------------------\r\n"; + ss << "Total generations: " << stats.total_generations << "\r\n"; + ss << "Total items generated: " << stats.total_items_generated << "\r\n"; + + if (!stats.table_usage_counts.empty()) { + ss << "\r\nTable usage:\r\n"; + for (const auto &[table_id, count] : stats.table_usage_counts) { + ss << " " << table_id << ": " << count << "\r\n"; + } + } + + if (!stats.item_drop_counts.empty()) { + ss << "\r\nTop dropped items:\r\n"; + std::vector> sorted_items( + stats.item_drop_counts.begin(), stats.item_drop_counts.end()); + std::sort(sorted_items.begin(), sorted_items.end(), + [](const auto &a, const auto &b) { return a.second > b.second; }); + + int shown = 0; + for (const auto &[vnum, count] : sorted_items) { + if (++shown > 10) break; + auto proto = obj_proto[obj_proto.get_rnum(vnum)]; + ss << " " << vnum; + if (proto) { + ss << " (" << proto->get_short_description() << ")"; + } + ss << ": " << count << "\r\n"; + } + } + + SendMsgToChar(ch, "%s", ss.str().c_str()); +} + +void ShowErrors(CharData *ch) { + const auto &errors = loot_tables::GetLoadErrors(); + + if (errors.empty()) { + SendMsgToChar(ch, "No loading errors.\r\n"); + return; + } + + std::ostringstream ss; + ss << "Loading errors (" << errors.size() << "):\r\n"; + for (const auto &error : errors) { + ss << " " << error << "\r\n"; + } + + SendMsgToChar(ch, "%s", ss.str().c_str()); +} + +} // anonymous namespace + +void do_loottable(CharData *ch, char *argument, int /*cmd*/, int /*subcmd*/) { + if (!loot_tables::IsInitialized()) { + SendMsgToChar(ch, "Loot tables system is not initialized.\r\n"); + return; + } + + char arg1[kMaxInputLength]; + char arg2[kMaxInputLength]; + + two_arguments(argument, arg1, arg2); + + if (!*arg1) { + ShowHelp(ch); + return; + } + + if (utils::IsAbbr(arg1, "list")) { + ListTables(ch); + } else if (utils::IsAbbr(arg1, "info")) { + if (!*arg2) { + SendMsgToChar(ch, "Usage: loottable info \r\n"); + return; + } + ShowTableInfo(ch, arg2); + } else if (utils::IsAbbr(arg1, "reload")) { + ReloadTables(ch); + } else if (utils::IsAbbr(arg1, "test")) { + if (!*arg2) { + SendMsgToChar(ch, "Usage: loottable test [luck]\r\n"); + return; + } + char arg3[kMaxInputLength]; + one_argument(argument + strlen(arg1) + strlen(arg2) + 2, arg3); + int luck = *arg3 ? atoi(arg3) : 0; + TestTable(ch, arg2, luck); + } else if (utils::IsAbbr(arg1, "stats")) { + ShowStats(ch); + } else if (utils::IsAbbr(arg1, "errors")) { + ShowErrors(ch); + } else { + ShowHelp(ch); + } +} + +#else // HAVE_YAML + +void do_loottable(CharData *ch, char * /*argument*/, int /*cmd*/, int /*subcmd*/) { + SendMsgToChar(ch, "Loot tables system is not compiled in (requires HAVE_YAML).\r\n"); +} + +#endif // HAVE_YAML + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/engine/ui/cmd_god/do_loottable.h b/src/engine/ui/cmd_god/do_loottable.h new file mode 100644 index 000000000..95b1e8ac4 --- /dev/null +++ b/src/engine/ui/cmd_god/do_loottable.h @@ -0,0 +1,14 @@ +// +// Created for Bylins MUD - Loot Tables Module +// + +#ifndef BYLINS_SRC_ENGINE_UI_CMD_GOD_DO_LOOTTABLE_H_ +#define BYLINS_SRC_ENGINE_UI_CMD_GOD_DO_LOOTTABLE_H_ + +class CharData; + +void do_loottable(CharData *ch, char *argument, int cmd, int subcmd); + +#endif // BYLINS_SRC_ENGINE_UI_CMD_GOD_DO_LOOTTABLE_H_ + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/engine/ui/interpreter.cpp b/src/engine/ui/interpreter.cpp index f40cb8d5c..e14d4c361 100644 --- a/src/engine/ui/interpreter.cpp +++ b/src/engine/ui/interpreter.cpp @@ -48,6 +48,7 @@ #include "engine/ui/cmd_god/do_liblist.h" #include "engine/ui/cmd_god/do_last.h" #include "engine/ui/cmd_god/do_load.h" +#include "engine/ui/cmd_god/do_loottable.h" #include "engine/ui/cmd_god/do_loadstat.h" #include "engine/ui/cmd_god/do_beep.h" #include "engine/ui/cmd_god/do_overstuff.h" @@ -881,6 +882,7 @@ cpp_extern const struct command_info cmd_info[] = {"list", EPosition::kStand, do_not_here, 0, 0, -1}, {"load", EPosition::kDead, DoLoad, 0, 0, 0}, {"loadstat", EPosition::kDead, DoLoadstat, kLvlImplementator, 0, 0}, + {"loottable", EPosition::kDead, do_loottable, kLvlImplementator, 0, 0}, {"look", EPosition::kRest, DoLook, 0, kScmdLook, 200}, {"lock", EPosition::kSit, do_gen_door, 0, kScmdLock, 500}, {"map", EPosition::kRest, do_map, 0, 0, 0}, diff --git a/src/gameplay/mechanics/corpse.cpp b/src/gameplay/mechanics/corpse.cpp index 85eb84a01..58154c6fa 100644 --- a/src/gameplay/mechanics/corpse.cpp +++ b/src/gameplay/mechanics/corpse.cpp @@ -6,6 +6,7 @@ #include "engine/db/world_objects.h" #include "engine/db/obj_prototypes.h" #include "engine/entities/char_data.h" +#include "engine/entities/zone.h" #include "engine/core/handler.h" #include "third_party_libs/pugixml/pugixml.h" #include "gameplay/clans/house.h" @@ -14,6 +15,10 @@ #include "gameplay/mechanics/weather.h" #include "gameplay/mechanics/sets_drop.h" +#ifdef HAVE_YAML +#include "gameplay/mechanics/loot_tables/loot_tables.h" +#endif + // see http://stackoverflow.com/questions/20145488/cygwin-g-stdstoi-error-stoi-is-not-a-member-of-std #if defined __CYGWIN__ #include @@ -42,29 +47,29 @@ struct global_drop { day_end(-1), race_mob(-1), chance(-1) {}; - int vnum; // внум шмотки, если число отрицательное - есть список внумов - int mob_lvl; // мин левел моба - int max_mob_lvl; // макс. левел моба (0 - не учитывается) - int count_mob; // после каждых убитых в сумме мобов, заданного левела, будет падать дроп - int mobs; // убито подходящих мобов - int rnum; // рнум шмотки, если vnum валидный - int day_start; // начиная с какого дня (игрового) шмотка может выпасть с моба и ... - int day_end; // ... кончая тем днем, после которого, шмотка перестанет выпадать из моба - int race_mob; // тип моба, с которого падает данная шмотка (-1 все) - int chance; // процент выпадения (1..1000) - // список внумов с общим дропом (дропается первый возможный) - // для внумов из списка учитывается поле максимума в мире + int vnum; // О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫, О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ - О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ + int mob_lvl; // О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ + int max_mob_lvl; // О©╫О©╫О©╫О©╫. О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ (0 - О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫) + int count_mob; // О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫, О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫, О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ + int mobs; // О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ + int rnum; // О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫, О©╫О©╫О©╫О©╫ vnum О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ + int day_start; // О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ (О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫) О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫ О©╫ ... + int day_end; // ... О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫, О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫, О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫ + int race_mob; // О©╫О©╫О©╫ О©╫О©╫О©╫О©╫, О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ (-1 О©╫О©╫О©╫) + int chance; // О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ (1..1000) + // О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ (О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫) + // О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫ OlistType olist; }; -// для вещей +// О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ struct global_drop_obj { global_drop_obj() : vnum(0), chance(0), day_start(0), day_end(0) {}; - // vnum шмотки + // vnum О©╫О©╫О©╫О©╫О©╫О©╫ int vnum; - // drop_chance шмотки от 0 до 1000 + // drop_chance О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ 0 О©╫О©╫ 1000 int chance; - // здесь храним типы рум, в которых может загрузится объект + // О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫О©╫, О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ std::list sects; int day_start; int day_end; @@ -84,7 +89,7 @@ void table_drop::reload_table() { this->drop_mobs.push_back(this->mobs[mob_number]); } } -// возвратит true, если моб найден в таблице и прошел шанс +// О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ true, О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ bool table_drop::check_mob(int vnum) { for (size_t i = 0; i < this->drop_mobs.size(); i++) { if (this->drop_mobs[i] == vnum) { @@ -113,10 +118,10 @@ const char *CONFIG_FILE = LIB_MISC"global_drop.xml"; const char *STAT_FILE = LIB_PLRSTUFF"global_drop.tmp"; void init() { - // на случай релоада + // О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ drop_list.clear(); drop_list_obj.clear(); - // конфиг + // О©╫О©╫О©╫О©╫О©╫О©╫ pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(CONFIG_FILE); if (!result) { @@ -145,7 +150,7 @@ void init() { global_drop_obj tmp; int obj_vnum = parse::ReadAttrAsInt(node, "ObjVnum"); int chance = parse::ReadAttrAsInt(node, "drop_chance"); - int day_start = parse::ReadAttrAsIntT(node, "day_start"); // если не определено в файле возвращаем -1 + int day_start = parse::ReadAttrAsIntT(node, "day_start"); // О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ -1 int day_end = parse::ReadAttrAsIntT(node, "day_end"); if (day_start == -1) { day_end = 360; @@ -168,7 +173,7 @@ void init() { int mob_lvl = parse::ReadAttrAsInt(node, "mob_lvl"); int max_mob_lvl = parse::ReadAttrAsInt(node, "max_mob_lvl"); int count_mob = parse::ReadAttrAsInt(node, "count_mob"); - int day_start = parse::ReadAttrAsIntT(node, "day_start"); // если не определено в файле возвращаем -1 + int day_start = parse::ReadAttrAsIntT(node, "day_start"); // О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ -1 int day_end = parse::ReadAttrAsIntT(node, "day_end"); int race_mob = parse::ReadAttrAsIntT(node, "race_mob"); int chance = parse::ReadAttrAsIntT(node, "drop_chance"); @@ -179,7 +184,7 @@ void init() { if (day_end == -1) day_end = 360; if (race_mob == -1) - race_mob = -1; // -1 для всех рас + race_mob = -1; // -1 О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ if (obj_vnum == -1 || mob_lvl <= 0 || count_mob <= 0 || max_mob_lvl < 0) { snprintf(buf, kMaxStringLength, @@ -219,7 +224,7 @@ void init() { } tmp_node.rnum = obj_rnum; } else { - // список шмоток с единым дропом + // О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ for (pugi::xml_node item = node.child("obj"); item; item = item.next_sibling("obj")) { int item_vnum = parse::ReadAttrAsInt(item, "vnum"); if (item_vnum <= 0) { @@ -228,7 +233,7 @@ void init() { mudlog(buf, CMP, kLvlImmortal, SYSLOG, true); return; } - // проверяем шмотку + // О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ int item_rnum = GetObjRnum(item_vnum); if (item_rnum < 0) { snprintf(buf, kMaxStringLength, "...incorrect item_vnum=%d", item_vnum); @@ -246,10 +251,10 @@ void init() { drop_list.push_back(tmp_node); } - // сохраненные статы по убитым ранее мобам + // О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ std::ifstream file(STAT_FILE); if (!file.is_open()) { - log("SYSERROR: не удалось открыть файл на чтение: %s", STAT_FILE); + log("SYSERROR: О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫: %s", STAT_FILE); return; } int vnum, mobs; @@ -265,7 +270,7 @@ void init() { void save() { std::ofstream file(STAT_FILE); if (!file.is_open()) { - log("SYSERROR: не удалось открыть файл на запись: %s", STAT_FILE); + log("SYSERROR: О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫: %s", STAT_FILE); return; } for (DropListType::iterator i = drop_list.begin(); i != drop_list.end(); ++i) { @@ -274,7 +279,7 @@ void save() { } /** - * Поиск шмотки для дропа из списка с учетом макс в мире. + * О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫. * \return rnum */ int get_obj_to_drop(DropListType::iterator &i) { @@ -293,8 +298,8 @@ int get_obj_to_drop(DropListType::iterator &i) { } /** - * Глобальный дроп с мобов заданных параметров. - * Если vnum отрицательный, то поиск идет по списку общего дропа. + * О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫. + * О©╫О©╫О©╫О©╫ vnum О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫, О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫. */ bool check_mob(ObjData *corpse, CharData *mob) { if (mob->IsFlagged(EMobFlag::kMounting)) @@ -303,11 +308,11 @@ bool check_mob(ObjData *corpse, CharData *mob) { if (tables_drop[i].check_mob(GET_MOB_VNUM(mob))) { int rnum; if ((rnum = GetObjRnum(tables_drop[i].get_vnum())) < 0) { - log("Ошибка tdrop. Внум: %d", tables_drop[i].get_vnum()); + log("О©╫О©╫О©╫О©╫О©╫О©╫ tdrop. О©╫О©╫О©╫О©╫: %d", tables_drop[i].get_vnum()); return true; } - act("&GГде-то высоко-высоко раздался мелодичный звон бубенчиков.&n", false, mob, 0, 0, kToRoom); - log("Фридроп: упал предмет %s с VNUM: %d", + act("&GО©╫О©╫О©╫-О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫-О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫.&n", false, mob, 0, 0, kToRoom); + log("О©╫О©╫О©╫О©╫О©╫О©╫О©╫: О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ %s О©╫ VNUM: %d", obj_proto[rnum]->get_short_description().c_str(), obj_proto[rnum]->get_vnum()); obj_to_corpse(corpse, mob, rnum, false); @@ -318,14 +323,14 @@ bool check_mob(ObjData *corpse, CharData *mob) { int day = time_info.month * kDaysPerMonth + time_info.day + 1; if (GetRealLevel(mob) >= i->mob_lvl && (!i->max_mob_lvl - || GetRealLevel(mob) <= i->max_mob_lvl) // моб в диапазоне уровней + || GetRealLevel(mob) <= i->max_mob_lvl) // О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ && ((i->race_mob < 0) || (GET_RACE(mob) == i->race_mob) - || (get_virtual_race(mob) == i->race_mob)) // совпадает раса или для всех - && (i->day_start <= day && i->day_end >= day) // временной промежуток - && (!NPC_FLAGGED(mob, ENpcFlag::kFreeDrop)) //не падают из фридропа + || (get_virtual_race(mob) == i->race_mob)) // О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ + && (i->day_start <= day && i->day_end >= day) // О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ + && (!NPC_FLAGGED(mob, ENpcFlag::kFreeDrop)) //О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ && (!mob->has_master() - || mob->get_master()->IsNpc())) // не чармис + || mob->get_master()->IsNpc())) // О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ { ++(i->mobs); @@ -339,8 +344,8 @@ bool check_mob(ObjData *corpse, CharData *mob) { && ((GetObjMIW(obj_rnum) == ObjData::UNLIMITED_GLOBAL_MAXIMUM) || (obj_rnum >= 0 && obj_proto.actual_count(obj_rnum) < GetObjMIW(obj_rnum)))) { - act("&GГде-то высоко-высоко раздался мелодичный звон бубенчиков.&n", false, mob, 0, 0, kToRoom); - sprintf(buf, "Фридроп: упал предмет %s VNUM %d с моба %s VNUM %d (%d lvl)", + act("&GО©╫О©╫О©╫-О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫-О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫.&n", false, mob, 0, 0, kToRoom); + sprintf(buf, "О©╫О©╫О©╫О©╫О©╫О©╫О©╫: О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ %s VNUM %d О©╫ О©╫О©╫О©╫О©╫ %s VNUM %d (%d lvl)", obj_proto[obj_rnum]->get_short_description().c_str(), obj_proto[obj_rnum]->get_vnum(), GET_NAME(mob), @@ -349,7 +354,7 @@ bool check_mob(ObjData *corpse, CharData *mob) { obj_to_corpse(corpse, mob, obj_rnum, false); } i->mobs = 0; -// return true; пусть после фридропа дроп вещей продолжается +// return true; О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ } } } @@ -362,25 +367,25 @@ void make_arena_corpse(CharData *ch, CharData *killer) { auto corpse = world_objects.create_blank(); corpse->set_sex(EGender::kPoly); - sprintf(buf2, "Останки %s лежат на земле.", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫О©╫О©╫О©╫ %s О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫.", GET_PAD(ch, 1)); corpse->set_description(buf2); - sprintf(buf2, "останки %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_short_description(buf2); - sprintf(buf2, "останки %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_PName(ECase::kNom, buf2); corpse->set_aliases(buf2); - sprintf(buf2, "останков %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_PName(ECase::kGen, buf2); - sprintf(buf2, "останкам %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_PName(ECase::kDat, buf2); - sprintf(buf2, "останки %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_PName(ECase::kAcc, buf2); - sprintf(buf2, "останками %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_PName(ECase::kIns, buf2); - sprintf(buf2, "останках %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_PName(ECase::kPre, buf2); corpse->set_type(EObjType::kContainer); @@ -398,13 +403,13 @@ void make_arena_corpse(CharData *ch, CharData *killer) { corpse->set_timer(0); } ExtraDescription::shared_ptr exdesc(new ExtraDescription()); - exdesc->keyword = str_dup(corpse->get_PName(ECase::kNom).c_str()); // косметика + exdesc->keyword = str_dup(corpse->get_PName(ECase::kNom).c_str()); // О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ if (killer) { - sprintf(buf, "Убит%s на арене %s.\r\n", GET_CH_SUF_6(ch), GET_PAD(killer, 4)); + sprintf(buf, "О©╫О©╫О©╫О©╫%s О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ %s.\r\n", GET_CH_SUF_6(ch), GET_PAD(killer, 4)); } else { - sprintf(buf, "Умер%s на арене.\r\n", GET_CH_SUF_4(ch)); + sprintf(buf, "О©╫О©╫О©╫О©╫%s О©╫О©╫ О©╫О©╫О©╫О©╫О©╫.\r\n", GET_CH_SUF_4(ch)); } - exdesc->description = str_dup(buf); // косметика + exdesc->description = str_dup(buf); // О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ exdesc->next = corpse->get_ex_description(); corpse->set_ex_description(exdesc); PlaceObjToRoom(corpse.get(), ch->in_room); @@ -417,24 +422,24 @@ ObjData *make_corpse(CharData *ch, CharData *killer) { if (ch->IsNpc() && ch->IsFlagged(EMobFlag::kCorpse)) return nullptr; auto corpse = world_objects.create_blank(); - sprintf(buf2, "труп %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_aliases(buf2); corpse->set_sex(EGender::kMale); - sprintf(buf2, "Труп %s лежит здесь.", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫ %s О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫.", GET_PAD(ch, 1)); corpse->set_description(buf2); - sprintf(buf2, "труп %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_short_description(buf2); - sprintf(buf2, "труп %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_PName(ECase::kNom, buf2); - sprintf(buf2, "трупа %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_PName(ECase::kGen, buf2); - sprintf(buf2, "трупу %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_PName(ECase::kDat, buf2); - sprintf(buf2, "труп %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_PName(ECase::kAcc, buf2); - sprintf(buf2, "трупом %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_PName(ECase::kIns, buf2); - sprintf(buf2, "трупе %s", GET_PAD(ch, 1)); + sprintf(buf2, "О©╫О©╫О©╫О©╫О©╫ %s", GET_PAD(ch, 1)); corpse->set_PName(ECase::kPre, buf2); corpse->set_type(EObjType::kContainer); @@ -454,7 +459,7 @@ ObjData *make_corpse(CharData *ch, CharData *killer) { corpse->set_destroyer(max_pc_corpse_time * 2); corpse->set_timer(max_pc_corpse_time * 2); } - // выбросим трупы + // О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ for (obj = ch->carrying; obj; obj = next_obj) { next_obj = obj->get_next_content(); if (IS_CORPSE(obj)) { @@ -469,7 +474,7 @@ ObjData *make_corpse(CharData *ch, CharData *killer) { PlaceObjToInventory(UnequipChar(ch, i, CharEquipFlags()), ch); } } - // Считаем вес шмоток после того как разденем чара + // О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ corpse->set_weight(GET_WEIGHT(ch) + ch->GetCarryingWeight()); // transfer character's inventory to the corpse corpse->set_contains(ch->carrying); @@ -515,7 +520,40 @@ ObjData *make_corpse(CharData *ch, CharData *killer) { } } - // если чармис убит палачом или на арене(и владелец не в бд) то труп попадает не в клетку а в инвентарь к владельцу чармиса +#ifdef HAVE_YAML + // Generate loot from loot tables system + if (ch->IsNpc() && !IS_CHARMICE(ch) && loot_tables::IsInitialized()) { + int luck_bonus = 0; + if (killer && !killer->IsNpc()) { + // Could add luck calculation based on killer's stats + luck_bonus = 0; + } + + auto generated = loot_tables::GenerateMobLoot(ch, killer, luck_bonus); + + // Place generated items into corpse + for (const auto &[vnum, count] : generated.items) { + for (int i = 0; i < count; ++i) { + auto obj = world_objects.create_from_prototype_by_vnum(vnum); + if (obj) { + obj->set_vnum_zone_from(GetZoneVnumByCharPlace(ch)); + PlaceObjIntoObj(obj.get(), corpse.get()); + } + } + } + + // Place generated currencies into corpse + for (const auto &[currency_id, amount] : generated.currencies) { + if (currency_id == loot_tables::currency::kGold && amount > 0) { + const auto money = CreateCurrencyObj(amount); + PlaceObjIntoObj(money.get(), corpse.get()); + } + // Other currencies can be handled here when needed + } + } +#endif + + // О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫(О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫ О©╫О©╫) О©╫О©╫ О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ if (IS_CHARMICE(ch) && !ch->IsFlagged(EMobFlag::kCorpse) && ((killer && killer->IsFlagged(EPrf::kExecutor)) || (ROOM_FLAGGED(ch->in_room, ERoomFlag::kArena) && !NORENTABLE(ch->get_master())))) { if (ch->has_master()) { diff --git a/src/gameplay/mechanics/loot_tables/loot_loader.cpp b/src/gameplay/mechanics/loot_tables/loot_loader.cpp new file mode 100644 index 000000000..d9a8564b4 --- /dev/null +++ b/src/gameplay/mechanics/loot_tables/loot_loader.cpp @@ -0,0 +1,275 @@ +// +// Created for Bylins MUD - Loot Tables Module +// + +#include "loot_loader.h" + +#include "utils/logger.h" + +#include + +namespace loot_tables { + +LootTablesLoader::LootTablesLoader(LootTablesRegistry *registry) + : registry_(registry) { +} + +void LootTablesLoader::Load(parser_wrapper::DataNode /*data*/) { + // Load from default YAML paths + LoadFromDirectory("lib/cfg/loot_tables"); + LoadFromDirectory("lib/world/ltt"); +} + +void LootTablesLoader::Reload(parser_wrapper::DataNode data) { + if (registry_) { + registry_->Clear(); + } + Load(data); +} + +void LootTablesLoader::LoadFromYaml(const std::filesystem::path &file_path) { +#ifdef HAVE_YAML + if (!std::filesystem::exists(file_path)) { + errors_.push_back("File not found: " + file_path.string()); + return; + } + + try { + YAML::Node root = YAML::LoadFile(file_path.string()); + + if (!root["tables"]) { + errors_.push_back("No 'tables' section in file: " + file_path.string()); + return; + } + + int loaded_count = 0; + for (const auto &table_node : root["tables"]) { + auto table = ParseTable(table_node); + if (table) { + registry_->RegisterTable(table); + ++loaded_count; + } + } + + log("LOOT_TABLES: Loaded %d tables from %s", loaded_count, file_path.string().c_str()); + + } catch (const YAML::Exception &e) { + errors_.push_back("YAML parse error in " + file_path.string() + ": " + e.what()); + log("LOOT_TABLES: YAML error in %s: %s", file_path.string().c_str(), e.what()); + } +#else + errors_.push_back("YAML support not compiled in"); + log("LOOT_TABLES: Cannot load %s - YAML support not compiled in", file_path.string().c_str()); +#endif +} + +void LootTablesLoader::LoadFromDirectory(const std::filesystem::path &dir_path) { + if (!std::filesystem::exists(dir_path)) { + return; // Directory doesn't exist, skip silently + } + + if (!std::filesystem::is_directory(dir_path)) { + errors_.push_back("Not a directory: " + dir_path.string()); + return; + } + + for (const auto &entry : std::filesystem::directory_iterator(dir_path)) { + if (entry.is_regular_file()) { + auto ext = entry.path().extension().string(); + if (ext == ".yaml" || ext == ".yml") { + LoadFromYaml(entry.path()); + } + } + } + + // Rebuild indices after loading + if (registry_) { + registry_->BuildIndices(); + + // Validate + auto validation_errors = registry_->Validate(); + for (const auto &error : validation_errors) { + errors_.push_back(error); + log("LOOT_TABLES: Validation error: %s", error.c_str()); + } + } +} + +#ifdef HAVE_YAML + +LootTable::Ptr LootTablesLoader::ParseTable(const YAML::Node &node) { + std::string id = GetString(node, "id"); + if (id.empty()) { + errors_.push_back("Table without 'id' field"); + return nullptr; + } + + auto table = std::make_shared(id); + + // Parse table type + std::string type_str = GetString(node, "type", "source_centric"); + if (type_str == "source_centric") { + table->SetTableType(ELootTableType::kSourceCentric); + } else if (type_str == "item_centric") { + table->SetTableType(ELootTableType::kItemCentric); + } + + // Parse source type + std::string source_str = GetString(node, "source", "mob_death"); + if (source_str == "mob_death") { + table->SetSourceType(ELootSourceType::kMobDeath); + } else if (source_str == "container") { + table->SetSourceType(ELootSourceType::kContainer); + } else if (source_str == "quest") { + table->SetSourceType(ELootSourceType::kQuest); + } + + // Parse priority + table->SetPriority(GetInt(node, "priority", 100)); + + // Parse params + if (node["params"]) { + const auto ¶ms = node["params"]; + table->SetProbabilityWidth(GetInt(params, "probability_width", 10000)); + table->SetMaxItemsPerRoll(GetInt(params, "max_items", 10)); + } + + // Parse filters + if (node["filters"]) { + for (const auto &filter_node : node["filters"]) { + auto filter = ParseFilter(filter_node); + table->AddFilter(filter); + } + } + + // Parse entries + if (node["entries"]) { + for (const auto &entry_node : node["entries"]) { + auto entry = ParseEntry(entry_node); + table->AddEntry(entry); + } + } + + return table; +} + +LootFilter LootTablesLoader::ParseFilter(const YAML::Node &node) { + LootFilter filter; + + std::string type_str = GetString(node, "type", "vnum"); + + if (type_str == "vnum") { + filter.type = ELootFilterType::kVnum; + filter.vnum = GetInt(node, "vnum"); + } else if (type_str == "level_range") { + filter.type = ELootFilterType::kLevelRange; + filter.min_level = GetInt(node, "min"); + filter.max_level = GetInt(node, "max"); + } else if (type_str == "zone") { + filter.type = ELootFilterType::kZone; + filter.zone_vnum = GetInt(node, "zone"); + } else if (type_str == "race") { + filter.type = ELootFilterType::kRace; + filter.race = GetInt(node, "race"); + } else if (type_str == "class") { + filter.type = ELootFilterType::kClass; + filter.mob_class = GetInt(node, "class"); + } + + return filter; +} + +LootEntry LootTablesLoader::ParseEntry(const YAML::Node &node) { + LootEntry entry; + + std::string type_str = GetString(node, "type", "item"); + + if (type_str == "item") { + entry.type = ELootEntryType::kItem; + entry.item_vnum = GetInt(node, "vnum"); + } else if (type_str == "currency") { + entry.type = ELootEntryType::kCurrency; + entry.currency_id = GetInt(node, "currency_id", currency::kGold); + } else if (type_str == "nested_table") { + entry.type = ELootEntryType::kNestedTable; + entry.nested_table_id = GetString(node, "table_id"); + } + + // Parse count + if (node["count"]) { + entry.count = ParseCountRange(node["count"]); + } + + // Parse weights + entry.probability_weight = GetInt(node, "weight", 10000); + entry.fallback_weight = GetInt(node, "fallback_weight", 0); + entry.allow_duplicates = GetBool(node, "allow_duplicates", false); + + // Parse player filters + if (node["player_filters"]) { + entry.player_filters = ParsePlayerFilters(node["player_filters"]); + } + + return entry; +} + +CountRange LootTablesLoader::ParseCountRange(const YAML::Node &node) { + CountRange range; + + if (node.IsScalar()) { + // Single value: count: 5 + range.min = range.max = node.as(); + } else if (node.IsMap()) { + // Map: count: {min: 1, max: 5} + range.min = GetInt(node, "min", 1); + range.max = GetInt(node, "max", 1); + } + + return range; +} + +PlayerFilters LootTablesLoader::ParsePlayerFilters(const YAML::Node &node) { + PlayerFilters filters; + + if (node["level"]) { + const auto &level_node = node["level"]; + filters.min_player_level = GetInt(level_node, "min", 0); + filters.max_player_level = GetInt(level_node, "max", 0); + } + + if (node["classes"]) { + for (const auto &class_node : node["classes"]) { + filters.allowed_classes.push_back(class_node.as()); + } + } + + return filters; +} + +std::string LootTablesLoader::GetString(const YAML::Node &node, const std::string &key, + const std::string &default_val) { + if (node[key]) { + return node[key].as(); + } + return default_val; +} + +int LootTablesLoader::GetInt(const YAML::Node &node, const std::string &key, int default_val) { + if (node[key]) { + return node[key].as(); + } + return default_val; +} + +bool LootTablesLoader::GetBool(const YAML::Node &node, const std::string &key, bool default_val) { + if (node[key]) { + return node[key].as(); + } + return default_val; +} + +#endif // HAVE_YAML + +} // namespace loot_tables + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/gameplay/mechanics/loot_tables/loot_loader.h b/src/gameplay/mechanics/loot_tables/loot_loader.h new file mode 100644 index 000000000..f1d493733 --- /dev/null +++ b/src/gameplay/mechanics/loot_tables/loot_loader.h @@ -0,0 +1,77 @@ +// +// Created for Bylins MUD - Loot Tables Module +// + +#ifndef BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_LOADER_H_ +#define BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_LOADER_H_ + +#include "loot_registry.h" +#include "engine/boot/cfg_manager.h" + +#include +#include + +#ifdef HAVE_YAML +#include +#endif + +namespace loot_tables { + +// Loader for loot tables from YAML files +class LootTablesLoader : public cfg_manager::ICfgLoader { + public: + explicit LootTablesLoader(LootTablesRegistry *registry); + + // ICfgLoader interface + void Load(parser_wrapper::DataNode data) override; + void Reload(parser_wrapper::DataNode data) override; + + // Load from YAML file directly + void LoadFromYaml(const std::filesystem::path &file_path); + + // Load all YAML files from directory + void LoadFromDirectory(const std::filesystem::path &dir_path); + + // Get load errors + [[nodiscard]] const std::vector &GetErrors() const { return errors_; } + + // Clear errors + void ClearErrors() { errors_.clear(); } + + private: + LootTablesRegistry *registry_; + std::vector errors_; + +#ifdef HAVE_YAML + // Parse single table from YAML node + LootTable::Ptr ParseTable(const YAML::Node &node); + + // Parse filter from YAML node + LootFilter ParseFilter(const YAML::Node &node); + + // Parse entry from YAML node + LootEntry ParseEntry(const YAML::Node &node); + + // Parse count range from YAML node + CountRange ParseCountRange(const YAML::Node &node); + + // Parse player filters from YAML node + PlayerFilters ParsePlayerFilters(const YAML::Node &node); + + // Helper to get string value with default + static std::string GetString(const YAML::Node &node, const std::string &key, + const std::string &default_val = ""); + + // Helper to get int value with default + static int GetInt(const YAML::Node &node, const std::string &key, int default_val = 0); + + // Helper to get bool value with default + static bool GetBool(const YAML::Node &node, const std::string &key, bool default_val = false); +#endif +}; + +} // namespace loot_tables + +#endif // BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_LOADER_H_ + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/gameplay/mechanics/loot_tables/loot_logger.cpp b/src/gameplay/mechanics/loot_tables/loot_logger.cpp new file mode 100644 index 000000000..48a19074a --- /dev/null +++ b/src/gameplay/mechanics/loot_tables/loot_logger.cpp @@ -0,0 +1,121 @@ +// +// Created for Bylins MUD - Loot Tables Module +// + +#include "loot_logger.h" + +#include "engine/entities/char_data.h" +#include "utils/logger.h" + +#include +#include +#include + +namespace loot_tables { + +LootLogger &LootLogger::Instance() { + static LootLogger instance; + return instance; +} + +void LootLogger::LogRareDrop(const std::string &table_id, + ObjVnum item_vnum, + int count, + const CharData *killer, + int weight, + int probability_width) { + if (!enabled_) { + return; + } + + std::ostringstream ss; + ss << "RARE: " << table_id << " -> item[" << item_vnum << "] x" << count; + + if (killer && !killer->IsNpc()) { + ss << " | killer: " << killer->get_name() << "(" << GetRealLevel(killer) << ")"; + } + + ss << " | weight: " << weight << "/" << probability_width; + + WriteLog(ELootLogType::kRareDrop, ss.str()); +} + +void LootLogger::LogMiwOverride(const std::string &table_id, + ObjVnum item_vnum, + const CharData *killer, + int fallback_weight, + int probability_width) { + if (!enabled_) { + return; + } + + std::ostringstream ss; + ss << "MIW_OVERRIDE: " << table_id << " -> item[" << item_vnum << "]"; + ss << " | fallback: " << fallback_weight << "/" << probability_width; + + if (killer && !killer->IsNpc()) { + ss << " | killer: " << killer->get_name() << "(" << GetRealLevel(killer) << ")"; + } + + WriteLog(ELootLogType::kMiwOverride, ss.str()); +} + +void LootLogger::LogError(const std::string &message) { + if (!enabled_) { + return; + } + + WriteLog(ELootLogType::kError, "ERROR: " + message); +} + +void LootLogger::LogDebug(const std::string &message) { + if (!enabled_) { + return; + } + + WriteLog(ELootLogType::kDebug, "DEBUG: " + message); +} + +bool LootLogger::IsRareDrop(int weight, int probability_width) const { + if (probability_width <= 0) { + return false; + } + + // Normalize weight to 10000 scale for comparison + int normalized = (weight * 10000) / probability_width; + return normalized < rare_threshold_; +} + +void LootLogger::WriteLog(ELootLogType type, const std::string &message) { + // Get current time + std::time_t now = std::time(nullptr); + std::tm *tm_info = std::localtime(&now); + + std::ostringstream timestamp; + timestamp << std::put_time(tm_info, "[%Y-%m-%d %H:%M:%S]"); + + std::string full_message = timestamp.str() + " " + message; + + // Log to syslog + switch (type) { + case ELootLogType::kRareDrop: + case ELootLogType::kMiwOverride: + log("LOOT_TABLES: %s", full_message.c_str()); + break; + + case ELootLogType::kError: + log("LOOT_TABLES ERROR: %s", full_message.c_str()); + break; + + case ELootLogType::kDebug: + // Only log debug in debug mode +#ifdef DEBUG + log("LOOT_TABLES DEBUG: %s", full_message.c_str()); +#endif + break; + } +} + +} // namespace loot_tables + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/gameplay/mechanics/loot_tables/loot_logger.h b/src/gameplay/mechanics/loot_tables/loot_logger.h new file mode 100644 index 000000000..eb67e3416 --- /dev/null +++ b/src/gameplay/mechanics/loot_tables/loot_logger.h @@ -0,0 +1,73 @@ +// +// Created for Bylins MUD - Loot Tables Module +// + +#ifndef BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_LOGGER_H_ +#define BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_LOGGER_H_ + +#include "loot_table.h" + +#include + +class CharData; + +namespace loot_tables { + +// Log types for loot events +enum class ELootLogType { + kRareDrop, // Item with low probability dropped + kMiwOverride, // MIW limit triggered, fallback used + kError, // Error during generation + kDebug // Debug information +}; + +// Logger for loot events +class LootLogger { + public: + // Singleton instance + static LootLogger &Instance(); + + // Set rare drop threshold (weight below which drops are logged) + void SetRareThreshold(int threshold) { rare_threshold_ = threshold; } + + // Enable/disable logging + void SetEnabled(bool enabled) { enabled_ = enabled; } + + // Log a rare drop + void LogRareDrop(const std::string &table_id, + ObjVnum item_vnum, + int count, + const CharData *killer, + int weight, + int probability_width); + + // Log MIW override + void LogMiwOverride(const std::string &table_id, + ObjVnum item_vnum, + const CharData *killer, + int fallback_weight, + int probability_width); + + // Log error + void LogError(const std::string &message); + + // Log debug message + void LogDebug(const std::string &message); + + // Check if item drop is considered rare + [[nodiscard]] bool IsRareDrop(int weight, int probability_width) const; + + private: + LootLogger() = default; + + bool enabled_{true}; + int rare_threshold_{500}; // Default: 5% (500/10000) is considered rare + + void WriteLog(ELootLogType type, const std::string &message); +}; + +} // namespace loot_tables + +#endif // BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_LOGGER_H_ + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/gameplay/mechanics/loot_tables/loot_registry.cpp b/src/gameplay/mechanics/loot_tables/loot_registry.cpp new file mode 100644 index 000000000..c77727c3e --- /dev/null +++ b/src/gameplay/mechanics/loot_tables/loot_registry.cpp @@ -0,0 +1,313 @@ +// +// Created for Bylins MUD - Loot Tables Module +// + +#include "loot_registry.h" +#include "loot_loader.h" + +#include "engine/entities/char_data.h" +#include "engine/entities/zone.h" +#include "engine/db/db.h" +#include "utils/utils.h" +#include "utils/logger.h" + +#include +#include + +namespace loot_tables { + +void LootStats::Reset() { + total_generations = 0; + total_items_generated = 0; + item_drop_counts.clear(); + table_usage_counts.clear(); +} + +void LootTablesRegistry::RegisterTable(TablePtr table) { + if (!table) { + return; + } + + const std::string &id = table->GetId(); + if (id.empty()) { + log("LOOT_TABLES: Attempted to register table with empty ID"); + return; + } + + all_tables_[id] = table; +} + +void LootTablesRegistry::UnregisterTable(const std::string &id) { + all_tables_.erase(id); +} + +void LootTablesRegistry::Clear() { + all_tables_.clear(); + mob_vnum_index_.clear(); + zone_index_.clear(); + filtered_tables_.clear(); +} + +LootTablesRegistry::TableConstPtr LootTablesRegistry::GetTable(const std::string &id) const { + auto it = all_tables_.find(id); + if (it != all_tables_.end()) { + return it->second; + } + return nullptr; +} + +LootTablesRegistry::TablePtr LootTablesRegistry::GetMutableTable(const std::string &id) { + auto it = all_tables_.find(id); + if (it != all_tables_.end()) { + return it->second; + } + return nullptr; +} + +const std::unordered_map & +LootTablesRegistry::GetAllTables() const { + return all_tables_; +} + +void LootTablesRegistry::BuildIndices() { + mob_vnum_index_.clear(); + zone_index_.clear(); + filtered_tables_.clear(); + + for (const auto &[id, table] : all_tables_) { + if (table->GetTableType() != ELootTableType::kSourceCentric) { + continue; + } + + bool has_vnum_filter = false; + bool has_zone_filter = false; + + for (const auto &filter : table->GetFilters()) { + switch (filter.type) { + case ELootFilterType::kVnum: + mob_vnum_index_.emplace(filter.vnum, table); + has_vnum_filter = true; + break; + + case ELootFilterType::kZone: + zone_index_.emplace(filter.zone_vnum, table); + has_zone_filter = true; + break; + + default: + // Complex filters go to filtered_tables_ + break; + } + } + + // If table has no simple filters, add to filtered list + if (!has_vnum_filter && !has_zone_filter) { + filtered_tables_.push_back(table); + } + } +} + +std::vector +LootTablesRegistry::FindTablesForMob(const CharData *mob) const { + std::vector result; + + if (!mob || !mob->IsNpc()) { + return result; + } + + ObjVnum mob_vnum = GET_MOB_VNUM(mob); + ZoneVnum zone_vnum = GetZoneVnumByCharPlace(const_cast(mob)); + + // Check vnum index + auto vnum_range = mob_vnum_index_.equal_range(mob_vnum); + for (auto it = vnum_range.first; it != vnum_range.second; ++it) { + result.push_back(it->second); + } + + // Check zone index + auto zone_range = zone_index_.equal_range(zone_vnum); + for (auto it = zone_range.first; it != zone_range.second; ++it) { + // Avoid duplicates + if (std::find(result.begin(), result.end(), it->second) == result.end()) { + result.push_back(it->second); + } + } + + // Check filtered tables + for (const auto &table : filtered_tables_) { + if (table->IsApplicable(mob)) { + if (std::find(result.begin(), result.end(), table) == result.end()) { + result.push_back(table); + } + } + } + + // Sort by priority (higher first) + std::sort(result.begin(), result.end(), + [](const TableConstPtr &a, const TableConstPtr &b) { + return a->GetPriority() > b->GetPriority(); + }); + + return result; +} + +GeneratedLoot LootTablesRegistry::GenerateFromMob( + const CharData *mob, + const CharData *killer, + int luck_bonus) const { + + GeneratedLoot result; + + auto tables = FindTablesForMob(mob); + if (tables.empty()) { + return result; + } + + GenerationContext ctx; + ctx.player = killer; + ctx.source_mob = mob; + ctx.luck_bonus = luck_bonus; + ctx.recursion_depth = 0; + + for (const auto &table : tables) { + auto table_loot = table->Generate(ctx, this, miw_check_func_); + result.Merge(table_loot); + + // Update statistics + if (!table_loot.IsEmpty()) { + stats_.table_usage_counts[table->GetId()]++; + } + } + + // Update statistics + stats_.total_generations++; + stats_.total_items_generated += result.TotalItemCount(); + + for (const auto &[vnum, count] : result.items) { + stats_.item_drop_counts[vnum] += count; + } + + return result; +} + +std::vector LootTablesRegistry::Validate() const { + std::vector errors; + + // Check for cycles in nested tables + std::unordered_set visited; + std::unordered_set in_path; + + for (const auto &[id, table] : all_tables_) { + visited.clear(); + in_path.clear(); + + if (HasCycle(id, visited, in_path)) { + errors.push_back("Cycle detected in table: " + id); + } + } + + // Check for invalid vnums in entries + for (const auto &[id, table] : all_tables_) { + for (const auto &entry : table->GetEntries()) { + if (entry.type == ELootEntryType::kNestedTable) { + if (all_tables_.find(entry.nested_table_id) == all_tables_.end()) { + errors.push_back("Table '" + id + "' references non-existent table: " + + entry.nested_table_id); + } + } + } + } + + return errors; +} + +bool LootTablesRegistry::HasCycle( + const std::string &table_id, + std::unordered_set &visited, + std::unordered_set &in_path) const { + + if (in_path.count(table_id)) { + return true; // Cycle detected + } + + if (visited.count(table_id)) { + return false; // Already checked, no cycle + } + + visited.insert(table_id); + in_path.insert(table_id); + + auto it = all_tables_.find(table_id); + if (it != all_tables_.end()) { + for (const auto &entry : it->second->GetEntries()) { + if (entry.type == ELootEntryType::kNestedTable) { + if (HasCycle(entry.nested_table_id, visited, in_path)) { + return true; + } + } + } + } + + in_path.erase(table_id); + return false; +} + +// Global registry instance +static LootTablesRegistry g_registry; +static LootTablesLoader g_loader(&g_registry); +static bool g_initialized = false; + +LootTablesRegistry &GetGlobalRegistry() { + return g_registry; +} + +void Initialize() { + if (g_initialized) { + return; + } + + g_loader.ClearErrors(); + g_loader.LoadFromDirectory("lib/cfg/loot_tables"); + g_loader.LoadFromDirectory("lib/world/ltt"); + + g_initialized = true; + + log("LOOT_TABLES: Initialized with %zu tables", g_registry.GetTableCount()); +} + +void Shutdown() { + g_registry.Clear(); + g_initialized = false; +} + +void Reload() { + g_registry.Clear(); + g_loader.ClearErrors(); + g_loader.LoadFromDirectory("lib/cfg/loot_tables"); + g_loader.LoadFromDirectory("lib/world/ltt"); + + log("LOOT_TABLES: Reloaded with %zu tables", g_registry.GetTableCount()); +} + +bool IsInitialized() { + return g_initialized; +} + +GeneratedLoot GenerateMobLoot(const CharData *mob, const CharData *killer, int luck_bonus) { + if (!g_initialized) { + return {}; + } + return g_registry.GenerateFromMob(mob, killer, luck_bonus); +} + +void SetMiwCheckFunc(LootTable::MiwCheckFunc func) { + g_registry.SetMiwCheckFunc(std::move(func)); +} + +const std::vector &GetLoadErrors() { + return g_loader.GetErrors(); +} + +} // namespace loot_tables + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/gameplay/mechanics/loot_tables/loot_registry.h b/src/gameplay/mechanics/loot_tables/loot_registry.h new file mode 100644 index 000000000..5f7610c53 --- /dev/null +++ b/src/gameplay/mechanics/loot_tables/loot_registry.h @@ -0,0 +1,108 @@ +// +// Created for Bylins MUD - Loot Tables Module +// + +#ifndef BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_REGISTRY_H_ +#define BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_REGISTRY_H_ + +#include "loot_table.h" + +#include +#include +#include + +namespace loot_tables { + +// Statistics for loot generation +struct LootStats { + size_t total_generations{0}; + size_t total_items_generated{0}; + std::map item_drop_counts; + std::map table_usage_counts; + + void Reset(); +}; + +// Registry for all loot tables +class LootTablesRegistry { + public: + using TablePtr = LootTable::Ptr; + using TableConstPtr = LootTable::ConstPtr; + + LootTablesRegistry() = default; + + // Register a table + void RegisterTable(TablePtr table); + + // Unregister a table by ID + void UnregisterTable(const std::string &id); + + // Clear all tables + void Clear(); + + // Get table by ID + [[nodiscard]] TableConstPtr GetTable(const std::string &id) const; + + // Get mutable table by ID (for editing) + [[nodiscard]] TablePtr GetMutableTable(const std::string &id); + + // Get all tables + [[nodiscard]] const std::unordered_map &GetAllTables() const; + + // Get table count + [[nodiscard]] size_t GetTableCount() const { return all_tables_.size(); } + + // Find all tables applicable to the given mob + [[nodiscard]] std::vector FindTablesForMob(const CharData *mob) const; + + // Generate loot from all applicable tables for a mob + [[nodiscard]] GeneratedLoot GenerateFromMob( + const CharData *mob, + const CharData *killer, + int luck_bonus = 0) const; + + // Build/rebuild indices for fast lookup + void BuildIndices(); + + // Validate all tables (check for cycles, invalid vnums, etc.) + [[nodiscard]] std::vector Validate() const; + + // Get statistics + [[nodiscard]] const LootStats &GetStats() const { return stats_; } + + // Reset statistics + void ResetStats() { stats_.Reset(); } + + // Set MIW check function + void SetMiwCheckFunc(LootTable::MiwCheckFunc func) { miw_check_func_ = std::move(func); } + + private: + // All tables by ID + std::unordered_map all_tables_; + + // Index by mob vnum (for SOURCE_CENTRIC tables with kVnum filter) + std::multimap mob_vnum_index_; + + // Index by zone vnum (for SOURCE_CENTRIC tables with kZone filter) + std::multimap zone_index_; + + // Tables with complex filters (level range, race, etc.) + std::vector filtered_tables_; + + // MIW check function + LootTable::MiwCheckFunc miw_check_func_; + + // Statistics + mutable LootStats stats_; + + // Check for cycles in nested tables + [[nodiscard]] bool HasCycle(const std::string &table_id, + std::unordered_set &visited, + std::unordered_set &in_path) const; +}; + +} // namespace loot_tables + +#endif // BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_REGISTRY_H_ + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/gameplay/mechanics/loot_tables/loot_table.cpp b/src/gameplay/mechanics/loot_tables/loot_table.cpp new file mode 100644 index 000000000..35f9410ff --- /dev/null +++ b/src/gameplay/mechanics/loot_tables/loot_table.cpp @@ -0,0 +1,251 @@ +// +// Created for Bylins MUD - Loot Tables Module +// + +#include "loot_table.h" +#include "loot_registry.h" + +#include "utils/random.h" + +namespace loot_tables { + +void GeneratedLoot::Merge(const GeneratedLoot &other) { + // Merge items + for (const auto &[vnum, count] : other.items) { + bool found = false; + for (auto &[existing_vnum, existing_count] : items) { + if (existing_vnum == vnum) { + existing_count += count; + found = true; + break; + } + } + if (!found) { + items.emplace_back(vnum, count); + } + } + + // Merge currencies + for (const auto &[currency_id, amount] : other.currencies) { + currencies[currency_id] += amount; + } +} + +bool GeneratedLoot::IsEmpty() const { + return items.empty() && currencies.empty(); +} + +size_t GeneratedLoot::TotalItemCount() const { + size_t total = 0; + for (const auto &[vnum, count] : items) { + total += count; + } + return total; +} + +LootTable::LootTable(std::string id) + : id_(std::move(id)) { +} + +void LootTable::AddFilter(const LootFilter &filter) { + source_filters_.push_back(filter); +} + +void LootTable::AddEntry(const LootEntry &entry) { + entries_.push_back(entry); +} + +void LootTable::ClearEntries() { + entries_.clear(); +} + +bool LootTable::IsApplicable(const CharData *mob) const { + if (!mob) { + return false; + } + + // If no filters, table applies to nothing + if (source_filters_.empty()) { + return false; + } + + // Check if any filter matches + for (const auto &filter : source_filters_) { + if (filter.Matches(mob)) { + return true; + } + } + + return false; +} + +GeneratedLoot LootTable::Generate( + const GenerationContext &ctx, + const LootTablesRegistry *registry, + const MiwCheckFunc &miw_check) const { + + GeneratedLoot result; + + if (entries_.empty()) { + return result; + } + + // Check recursion depth + if (ctx.recursion_depth >= GenerationContext::kMaxRecursionDepth) { + return result; + } + + // Track which entries have been used (for allow_duplicates=false) + std::vector entry_used(entries_.size(), false); + + GenerateInternal(ctx, registry, miw_check, result, entry_used); + + return result; +} + +void LootTable::GenerateInternal( + const GenerationContext &ctx, + const LootTablesRegistry *registry, + const MiwCheckFunc &miw_check, + GeneratedLoot &result, + std::vector &entry_used) const { + + // Calculate total weight of available entries + int total_weight = 0; + std::vector effective_weights(entries_.size(), 0); + + for (size_t i = 0; i < entries_.size(); ++i) { + const auto &entry = entries_[i]; + + // Skip if already used and duplicates not allowed + if (entry_used[i] && !entry.allow_duplicates) { + continue; + } + + // Check player filters + if (!entry.CheckPlayerFilters(ctx.player)) { + continue; + } + + int weight = GetEffectiveWeight(entry, miw_check); + effective_weights[i] = weight; + total_weight += weight; + } + + if (total_weight <= 0) { + return; + } + + // Generate items up to max_items_per_roll + int items_generated = 0; + int max_attempts = max_items_per_roll_ * 3; // Prevent infinite loops + + while (items_generated < max_items_per_roll_ && max_attempts > 0) { + --max_attempts; + + // Roll for probability + int roll = number(1, probability_width_); + + // Apply luck bonus + if (ctx.luck_bonus > 0) { + // Luck bonus reduces the roll, making it easier to hit lower weights + roll = std::max(1, roll - (roll * ctx.luck_bonus / 100)); + } + + // Select entry based on weighted random + int weight_roll = number(1, total_weight); + int cumulative = 0; + int selected_idx = -1; + + for (size_t i = 0; i < entries_.size(); ++i) { + if (effective_weights[i] <= 0) { + continue; + } + + cumulative += effective_weights[i]; + if (weight_roll <= cumulative) { + selected_idx = static_cast(i); + break; + } + } + + if (selected_idx < 0) { + break; + } + + const auto &entry = entries_[selected_idx]; + + // Check if roll hits probability + if (roll > entry.probability_weight) { + continue; + } + + // Generate loot based on entry type + switch (entry.type) { + case ELootEntryType::kItem: { + int count = entry.RollCount(); + if (count > 0) { + result.items.emplace_back(entry.item_vnum, count); + ++items_generated; + } + break; + } + + case ELootEntryType::kCurrency: { + long amount = entry.RollCount(); + if (amount > 0) { + result.currencies[entry.currency_id] += amount; + ++items_generated; + } + break; + } + + case ELootEntryType::kNestedTable: { + if (registry && !entry.nested_table_id.empty()) { + auto nested_table = registry->GetTable(entry.nested_table_id); + if (nested_table) { + GenerationContext nested_ctx = ctx; + nested_ctx.recursion_depth++; + + auto nested_loot = nested_table->Generate(nested_ctx, registry, miw_check); + result.Merge(nested_loot); + items_generated += static_cast(nested_loot.TotalItemCount()); + } + } + break; + } + } + + // Mark entry as used if duplicates not allowed + if (!entry.allow_duplicates) { + entry_used[selected_idx] = true; + + // Recalculate total weight + total_weight -= effective_weights[selected_idx]; + effective_weights[selected_idx] = 0; + + if (total_weight <= 0) { + break; + } + } + } +} + +int LootTable::GetEffectiveWeight( + const LootEntry &entry, + const MiwCheckFunc &miw_check) const { + + // For items, check MIW limit + if (entry.type == ELootEntryType::kItem && miw_check) { + if (miw_check(entry.item_vnum)) { + // MIW limit reached, use fallback weight + return entry.fallback_weight; + } + } + + return entry.probability_weight; +} + +} // namespace loot_tables + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/gameplay/mechanics/loot_tables/loot_table.h b/src/gameplay/mechanics/loot_tables/loot_table.h new file mode 100644 index 000000000..109d3eca9 --- /dev/null +++ b/src/gameplay/mechanics/loot_tables/loot_table.h @@ -0,0 +1,125 @@ +// +// Created for Bylins MUD - Loot Tables Module +// + +#ifndef BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_TABLE_H_ +#define BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_TABLE_H_ + +#include "loot_table_types.h" + +#include +#include +#include + +namespace loot_tables { + +// Forward declaration +class LootTablesRegistry; + +// Context for loot generation +struct GenerationContext { + const CharData *player{nullptr}; // Player who killed the mob (for filters) + const CharData *source_mob{nullptr}; // Source mob (for reference) + int luck_bonus{0}; // Bonus to drop chance (0-100) + int recursion_depth{0}; // Current recursion depth (for nested tables) + + // Maximum recursion depth for nested tables + static constexpr int kMaxRecursionDepth = 10; +}; + +// Result of loot generation +struct GeneratedLoot { + // Generated items: vnum -> count + std::vector> items; + + // Generated currencies: currency_id -> amount + std::map currencies; + + // Merge another GeneratedLoot into this one + void Merge(const GeneratedLoot &other); + + // Check if empty + [[nodiscard]] bool IsEmpty() const; + + // Get total item count + [[nodiscard]] size_t TotalItemCount() const; +}; + +// Loot table class +class LootTable { + public: + using Ptr = std::shared_ptr; + using ConstPtr = std::shared_ptr; + + // MIW check function type + using MiwCheckFunc = std::function; + + LootTable() = default; + explicit LootTable(std::string id); + + // Getters + [[nodiscard]] const std::string &GetId() const { return id_; } + [[nodiscard]] ELootTableType GetTableType() const { return table_type_; } + [[nodiscard]] ELootSourceType GetSourceType() const { return source_type_; } + [[nodiscard]] int GetPriority() const { return priority_; } + [[nodiscard]] int GetProbabilityWidth() const { return probability_width_; } + [[nodiscard]] int GetMaxItemsPerRoll() const { return max_items_per_roll_; } + [[nodiscard]] const std::vector &GetFilters() const { return source_filters_; } + [[nodiscard]] const std::vector &GetEntries() const { return entries_; } + + // Setters + void SetId(const std::string &id) { id_ = id; } + void SetTableType(ELootTableType type) { table_type_ = type; } + void SetSourceType(ELootSourceType type) { source_type_ = type; } + void SetPriority(int priority) { priority_ = priority; } + void SetProbabilityWidth(int width) { probability_width_ = width; } + void SetMaxItemsPerRoll(int max_items) { max_items_per_roll_ = max_items; } + + // Add filter + void AddFilter(const LootFilter &filter); + + // Add entry + void AddEntry(const LootEntry &entry); + + // Clear all entries + void ClearEntries(); + + // Check if this table is applicable to the given mob + [[nodiscard]] bool IsApplicable(const CharData *mob) const; + + // Generate loot from this table + [[nodiscard]] GeneratedLoot Generate( + const GenerationContext &ctx, + const LootTablesRegistry *registry = nullptr, + const MiwCheckFunc &miw_check = nullptr) const; + + private: + std::string id_; + ELootTableType table_type_{ELootTableType::kSourceCentric}; + ELootSourceType source_type_{ELootSourceType::kMobDeath}; + std::vector source_filters_; + std::vector entries_; + + int probability_width_{10000}; // Base for probability (10000 = 100.00%) + int max_items_per_roll_{10}; // Maximum items per single generation + int priority_{100}; // Table priority (higher = processed first) + + // Internal generation with tracking of already selected entries + void GenerateInternal( + const GenerationContext &ctx, + const LootTablesRegistry *registry, + const MiwCheckFunc &miw_check, + GeneratedLoot &result, + std::vector &entry_used) const; + + // Calculate effective weight for entry considering MIW + [[nodiscard]] int GetEffectiveWeight( + const LootEntry &entry, + const MiwCheckFunc &miw_check) const; +}; + +} // namespace loot_tables + +#endif // BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_TABLE_H_ + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/gameplay/mechanics/loot_tables/loot_table_types.cpp b/src/gameplay/mechanics/loot_tables/loot_table_types.cpp new file mode 100644 index 000000000..87fc06bf2 --- /dev/null +++ b/src/gameplay/mechanics/loot_tables/loot_table_types.cpp @@ -0,0 +1,161 @@ +// +// Created for Bylins MUD - Loot Tables Module +// + +#include "loot_table_types.h" + +#include "engine/entities/char_data.h" +#include "engine/entities/zone.h" +#include "engine/db/db.h" +#include "utils/utils.h" +#include "utils/random.h" + +namespace loot_tables { + +bool LootFilter::Matches(const CharData *mob) const { + if (!mob || !mob->IsNpc()) { + return false; + } + + switch (type) { + case ELootFilterType::kVnum: + return GET_MOB_VNUM(mob) == vnum; + + case ELootFilterType::kLevelRange: { + int mob_level = GetRealLevel(mob); + if (min_level > 0 && mob_level < min_level) { + return false; + } + if (max_level > 0 && mob_level > max_level) { + return false; + } + return true; + } + + case ELootFilterType::kRace: + return mob->get_race() == race; + + case ELootFilterType::kClass: + return static_cast(mob->GetClass()) == mob_class; + + case ELootFilterType::kFlags: + return false; // TODO: implement flags matching + + case ELootFilterType::kZone: + return GetZoneVnumByCharPlace(const_cast(mob)) == zone_vnum; + } + + return false; +} + +LootFilter LootFilter::FromVnum(ObjVnum obj_vnum) { + LootFilter filter; + filter.type = ELootFilterType::kVnum; + filter.vnum = obj_vnum; + return filter; +} + +LootFilter LootFilter::FromLevelRange(int min_lvl, int max_lvl) { + LootFilter filter; + filter.type = ELootFilterType::kLevelRange; + filter.min_level = min_lvl; + filter.max_level = max_lvl; + return filter; +} + +LootFilter LootFilter::FromZone(ZoneVnum zone) { + LootFilter filter; + filter.type = ELootFilterType::kZone; + filter.zone_vnum = zone; + return filter; +} + +LootFilter LootFilter::FromRace(int race_id) { + LootFilter filter; + filter.type = ELootFilterType::kRace; + filter.race = race_id; + return filter; +} + +int CountRange::Roll() const { + if (min >= max) { + return min; + } + return number(min, max); +} + +bool PlayerFilters::Matches(const CharData *player) const { + if (!player || player->IsNpc()) { + return true; // No player = no restrictions + } + + // Check level + int player_level = GetRealLevel(player); + if (min_player_level > 0 && player_level < min_player_level) { + return false; + } + if (max_player_level > 0 && player_level > max_player_level) { + return false; + } + + // Check class + if (!allowed_classes.empty()) { + int player_class = to_underlying(player->GetClass()); + bool class_found = false; + for (int allowed_class : allowed_classes) { + if (allowed_class == player_class) { + class_found = true; + break; + } + } + if (!class_found) { + return false; + } + } + + return true; +} + +bool PlayerFilters::IsEmpty() const { + return min_player_level == 0 + && max_player_level == 0 + && allowed_classes.empty(); +} + +bool LootEntry::CheckPlayerFilters(const CharData *player) const { + return player_filters.Matches(player); +} + +int LootEntry::RollCount() const { + return count.Roll(); +} + +LootEntry LootEntry::Item(ObjVnum vnum, int weight, CountRange cnt) { + LootEntry entry; + entry.type = ELootEntryType::kItem; + entry.item_vnum = vnum; + entry.probability_weight = weight; + entry.count = cnt; + return entry; +} + +LootEntry LootEntry::Currency(int curr_id, int weight, CountRange cnt) { + LootEntry entry; + entry.type = ELootEntryType::kCurrency; + entry.currency_id = curr_id; + entry.probability_weight = weight; + entry.count = cnt; + return entry; +} + +LootEntry LootEntry::NestedTable(const std::string &table_id, int weight) { + LootEntry entry; + entry.type = ELootEntryType::kNestedTable; + entry.nested_table_id = table_id; + entry.probability_weight = weight; + return entry; +} + +} // namespace loot_tables + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/gameplay/mechanics/loot_tables/loot_table_types.h b/src/gameplay/mechanics/loot_tables/loot_table_types.h new file mode 100644 index 000000000..fe738848d --- /dev/null +++ b/src/gameplay/mechanics/loot_tables/loot_table_types.h @@ -0,0 +1,166 @@ +// +// Created for Bylins MUD - Loot Tables Module +// + +#ifndef BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_TABLE_TYPES_H_ +#define BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_TABLE_TYPES_H_ + +#include "engine/structs/structs.h" +#include "engine/structs/flag_data.h" + +#include +#include +#include + +class CharData; + +namespace loot_tables { + +// Source type for loot generation +enum class ELootSourceType { + kMobDeath, // Loot from mob death (corpse) + kContainer, // Loot from container opening + kQuest // Loot from quest completion +}; + +// Table type determines how entries are organized +enum class ELootTableType { + kSourceCentric, // Table is attached to sources (mobs), entries are items + kItemCentric // Table is attached to items, entries are sources where they drop +}; + +// Filter type for matching sources +enum class ELootFilterType { + kVnum, // Match by exact vnum + kLevelRange, // Match by mob level range + kRace, // Match by mob race + kClass, // Match by mob class + kFlags, // Match by mob flags (NPC flags) + kZone // Match by zone vnum +}; + +// Entry type in loot table +enum class ELootEntryType { + kItem, // Generate an item + kCurrency, // Generate currency (gold, glory, etc.) + kNestedTable // Reference to another loot table +}; + +// Currency identifiers +namespace currency { +constexpr int kGold = 0; +constexpr int kGlory = 1; +constexpr int kIce = 2; +constexpr int kNogata = 3; +} // namespace currency + +// Filter for matching loot sources (mobs, containers, etc.) +struct LootFilter { + ELootFilterType type{ELootFilterType::kVnum}; + + // For kVnum filter + ObjVnum vnum{0}; + + // For kLevelRange filter + int min_level{0}; + int max_level{0}; + + // For kRace filter + int race{0}; + + // For kClass filter + int mob_class{0}; + + // For kFlags filter + FlagData flags; + + // For kZone filter + ZoneVnum zone_vnum{0}; + + // Check if this filter matches the given mob + [[nodiscard]] bool Matches(const CharData *mob) const; + + // Create filter from vnum + static LootFilter FromVnum(ObjVnum vnum); + + // Create filter from level range + static LootFilter FromLevelRange(int min_lvl, int max_lvl); + + // Create filter from zone + static LootFilter FromZone(ZoneVnum zone); + + // Create filter from race + static LootFilter FromRace(int race); +}; + +// Count range for item generation +struct CountRange { + int min{1}; + int max{1}; + + CountRange() = default; + CountRange(int min_val, int max_val) : min(min_val), max(max_val) {} + + // Roll random count in range [min, max] + [[nodiscard]] int Roll() const; +}; + +// Player-specific filters for loot entries +struct PlayerFilters { + // Level restrictions + int min_player_level{0}; + int max_player_level{0}; // 0 = no limit + + // Class restrictions (empty = all classes) + std::vector allowed_classes; + + // Check if player matches these filters + [[nodiscard]] bool Matches(const CharData *player) const; + + // Check if filters are empty (no restrictions) + [[nodiscard]] bool IsEmpty() const; +}; + +// Single entry in a loot table +struct LootEntry { + ELootEntryType type{ELootEntryType::kItem}; + + // For kItem type + ObjVnum item_vnum{0}; + + // For kCurrency type + int currency_id{currency::kGold}; + + // For kNestedTable type + std::string nested_table_id; + + // Generation parameters + CountRange count; + int probability_weight{10000}; // Primary weight (out of probability_width) + int fallback_weight{0}; // Secondary weight when MIW limit reached + bool allow_duplicates{false}; // Can this entry drop multiple times per generation? + + // Player-specific filters + PlayerFilters player_filters; + + // Check if entry can drop for the given player + [[nodiscard]] bool CheckPlayerFilters(const CharData *player) const; + + // Roll random count for this entry + [[nodiscard]] int RollCount() const; + + // Create item entry + static LootEntry Item(ObjVnum vnum, int weight, CountRange count = {1, 1}); + + // Create currency entry + static LootEntry Currency(int currency_id, int weight, CountRange count); + + // Create nested table entry + static LootEntry NestedTable(const std::string &table_id, int weight); +}; + +} // namespace loot_tables + +#endif // BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_TABLE_TYPES_H_ + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/gameplay/mechanics/loot_tables/loot_tables.h b/src/gameplay/mechanics/loot_tables/loot_tables.h new file mode 100644 index 000000000..a5199e7b6 --- /dev/null +++ b/src/gameplay/mechanics/loot_tables/loot_tables.h @@ -0,0 +1,47 @@ +// +// Created for Bylins MUD - Loot Tables Module +// +// Public API for loot tables system +// + +#ifndef BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_TABLES_H_ +#define BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_TABLES_H_ + +#include "loot_table_types.h" +#include "loot_table.h" +#include "loot_registry.h" +#include "loot_loader.h" +#include "loot_logger.h" + +namespace loot_tables { + +// Get global loot tables registry +LootTablesRegistry &GetGlobalRegistry(); + +// Initialize loot tables system +void Initialize(); + +// Shutdown loot tables system +void Shutdown(); + +// Reload all loot tables +void Reload(); + +// Check if loot tables system is initialized +bool IsInitialized(); + +// Generate loot for a mob death +// Returns items and currencies to place in corpse +GeneratedLoot GenerateMobLoot(const CharData *mob, const CharData *killer, int luck_bonus = 0); + +// Set MIW check function (checks if item is at max_in_world limit) +void SetMiwCheckFunc(LootTable::MiwCheckFunc func); + +// Get loading errors from last load/reload +const std::vector &GetLoadErrors(); + +} // namespace loot_tables + +#endif // BYLINS_SRC_GAMEPLAY_MECHANICS_LOOT_TABLES_LOOT_TABLES_H_ + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/utils/logger.cpp b/src/utils/logger.cpp index f4c2300a9..b6cd65ad9 100644 --- a/src/utils/logger.cpp +++ b/src/utils/logger.cpp @@ -6,9 +6,10 @@ #include #include #include -#include +#include +#include -#if defined(__clang__) +#if defined(__clang__) || (defined(__GNUC__) && __GNUC__ < 13) #define HAS_TIME_ZONE 0 #else #define HAS_TIME_ZONE 1 @@ -75,7 +76,7 @@ std::size_t vlog_buffer(char *buffer, const std::size_t buffer_size, const char const auto utc_now = std::chrono::time_point_cast(std::chrono::system_clock::now()); const auto now = std::chrono::zoned_time{time_zone, utc_now}; - const auto str = std::format("{:%Y-%m-%d %T}", now); + const auto str = fmt::format("{:%Y-%m-%d %T}", now); timestamp_length = snprintf(buffer, buffer_size, "%s :: ", str.c_str()); #else // Реализация без std::chrono::time_zone, используем std::chrono::local_time diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 57ffb756f..b3374156f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,14 +2,14 @@ cmake_minimum_required(VERSION 3.10) enable_testing() -# Поиск GTest: сначала пробуем find_package (работает на большинстве дистрибутивов), -# затем ручной поиск для нестандартных установок +# О©╫О©╫О©╫О©╫О©╫ GTest: О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ find_package (О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫), +# О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ if (NOT GTEST_INCLUDE OR NOT GTEST_LIB) - # Сначала пробуем стандартный поиск через find_package + # О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ find_package find_package(GTest QUIET) if (GTest_FOUND OR GTEST_FOUND) message(STATUS "GTest found via find_package") - # Современный CMake (3.20+) использует GTest::gtest target + # О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ CMake (3.20+) О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ GTest::gtest target if (TARGET GTest::gtest) set(GTEST_LIB GTest::gtest) get_target_property(GTEST_INCLUDE GTest::gtest INTERFACE_INCLUDE_DIRECTORIES) @@ -18,10 +18,10 @@ if (NOT GTEST_INCLUDE OR NOT GTEST_LIB) set(GTEST_INCLUDE "${GTEST_INCLUDE_DIRS}") endif () else () - # Ручной поиск для нестандартных установок + # О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ message(STATUS "GTest not found via find_package, trying manual search...") - # Ищем заголовочные файлы + # О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ find_path(GTEST_INCLUDE_DIR gtest/gtest.h PATHS $ENV{GTEST_ROOT}/include @@ -29,7 +29,7 @@ if (NOT GTEST_INCLUDE OR NOT GTEST_LIB) /usr/local/include ) - # Ищем библиотеку в стандартных путях для разных дистрибутивов + # О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ find_library(GTEST_LIB gtest PATHS $ENV{GTEST_ROOT}/lib @@ -80,7 +80,8 @@ set(TESTS radix.trie.cpp compact.trie.cpp compact.trie.iterators.cpp - compact.trie.prefixes.cpp) + compact.trie.prefixes.cpp + $<$:loot_tables.cpp>) set(UTILITIES char.utilities.hpp char.utilities.cpp) @@ -102,7 +103,7 @@ add_test(all tests) add_custom_target(checks COMMAND ${CMAKE_CTEST_COMMAND} -V DEPENDS tests) -# Копируем тестовые данные в корень проекта (откуда запускаются тесты) +# О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫ О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫ (О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫О©╫ О©╫О©╫О©╫О©╫О©╫) add_custom_target(tests.misc.grouping DEPENDS ${CMAKE_SOURCE_DIR}/lib.template/misc/grouping COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_SOURCE_DIR}/misc diff --git a/tests/loot_tables.cpp b/tests/loot_tables.cpp new file mode 100644 index 000000000..6aed6f2e2 --- /dev/null +++ b/tests/loot_tables.cpp @@ -0,0 +1,330 @@ +#ifdef HAVE_YAML + +#include "gameplay/mechanics/loot_tables/loot_table_types.h" +#include "gameplay/mechanics/loot_tables/loot_table.h" +#include "gameplay/mechanics/loot_tables/loot_registry.h" + +#include +#include +#include + +using namespace loot_tables; + +// Test LootFilter +class LootFilterTest : public ::testing::Test { +protected: + void SetUp() override { + } +}; + +TEST_F(LootFilterTest, FromVnum) { + auto filter = LootFilter::FromVnum(12345); + EXPECT_EQ(ELootFilterType::kVnum, filter.type); + EXPECT_EQ(12345, filter.vnum); +} + +TEST_F(LootFilterTest, FromLevelRange) { + auto filter = LootFilter::FromLevelRange(10, 20); + EXPECT_EQ(ELootFilterType::kLevelRange, filter.type); + EXPECT_EQ(10, filter.min_level); + EXPECT_EQ(20, filter.max_level); +} + +TEST_F(LootFilterTest, FromZone) { + auto filter = LootFilter::FromZone(100); + EXPECT_EQ(ELootFilterType::kZone, filter.type); + EXPECT_EQ(100, filter.zone_vnum); +} + +TEST_F(LootFilterTest, FromRace) { + auto filter = LootFilter::FromRace(5); + EXPECT_EQ(ELootFilterType::kRace, filter.type); + EXPECT_EQ(5, filter.race); +} + +// Test CountRange +class CountRangeTest : public ::testing::Test { +}; + +TEST_F(CountRangeTest, FixedCount) { + CountRange range(5, 5); + for (int i = 0; i < 100; ++i) { + EXPECT_EQ(5, range.Roll()); + } +} + +TEST_F(CountRangeTest, RangeCount) { + CountRange range(1, 10); + std::map counts; + for (int i = 0; i < 1000; ++i) { + int val = range.Roll(); + EXPECT_GE(val, 1); + EXPECT_LE(val, 10); + counts[val]++; + } + // Check that we got some variety + EXPECT_GT(counts.size(), 1u); +} + +// Test LootEntry +class LootEntryTest : public ::testing::Test { +}; + +TEST_F(LootEntryTest, ItemEntry) { + auto entry = LootEntry::Item(50001, 5000, {1, 3}); + EXPECT_EQ(ELootEntryType::kItem, entry.type); + EXPECT_EQ(50001, entry.item_vnum); + EXPECT_EQ(5000, entry.probability_weight); + EXPECT_EQ(1, entry.count.min); + EXPECT_EQ(3, entry.count.max); +} + +TEST_F(LootEntryTest, CurrencyEntry) { + auto entry = LootEntry::Currency(currency::kGold, 8000, {100, 500}); + EXPECT_EQ(ELootEntryType::kCurrency, entry.type); + EXPECT_EQ(currency::kGold, entry.currency_id); + EXPECT_EQ(8000, entry.probability_weight); +} + +TEST_F(LootEntryTest, NestedTableEntry) { + auto entry = LootEntry::NestedTable("gems_common", 3000); + EXPECT_EQ(ELootEntryType::kNestedTable, entry.type); + EXPECT_EQ("gems_common", entry.nested_table_id); + EXPECT_EQ(3000, entry.probability_weight); +} + +// Test PlayerFilters +class PlayerFiltersTest : public ::testing::Test { +}; + +TEST_F(PlayerFiltersTest, EmptyFilters) { + PlayerFilters filters; + EXPECT_TRUE(filters.IsEmpty()); + EXPECT_TRUE(filters.Matches(nullptr)); +} + +// Test LootTable +class LootTableTest : public ::testing::Test { +protected: + LootTable::Ptr table; + + void SetUp() override { + table = std::make_shared("test_table"); + table->SetProbabilityWidth(10000); + table->SetMaxItemsPerRoll(5); + } +}; + +TEST_F(LootTableTest, EmptyTable) { + GenerationContext ctx; + auto loot = table->Generate(ctx); + EXPECT_TRUE(loot.IsEmpty()); +} + +TEST_F(LootTableTest, SingleItemGuaranteed) { + auto entry = LootEntry::Item(50001, 10000); // 100% chance + table->AddEntry(entry); + table->AddFilter(LootFilter::FromVnum(12345)); + + GenerationContext ctx; + auto loot = table->Generate(ctx); + + EXPECT_FALSE(loot.IsEmpty()); + EXPECT_EQ(1u, loot.items.size()); + EXPECT_EQ(50001, loot.items[0].first); + EXPECT_EQ(1, loot.items[0].second); +} + +TEST_F(LootTableTest, CurrencyGeneration) { + auto entry = LootEntry::Currency(currency::kGold, 10000, {100, 200}); + table->AddEntry(entry); + table->AddFilter(LootFilter::FromVnum(12345)); + + GenerationContext ctx; + auto loot = table->Generate(ctx); + + EXPECT_FALSE(loot.IsEmpty()); + EXPECT_TRUE(loot.currencies.count(currency::kGold) > 0); + EXPECT_GE(loot.currencies[currency::kGold], 100); + EXPECT_LE(loot.currencies[currency::kGold], 200); +} + +// Test GeneratedLoot +class GeneratedLootTest : public ::testing::Test { +}; + +TEST_F(GeneratedLootTest, Empty) { + GeneratedLoot loot; + EXPECT_TRUE(loot.IsEmpty()); + EXPECT_EQ(0u, loot.TotalItemCount()); +} + +TEST_F(GeneratedLootTest, Merge) { + GeneratedLoot loot1; + loot1.items.emplace_back(50001, 2); + loot1.currencies[currency::kGold] = 100; + + GeneratedLoot loot2; + loot2.items.emplace_back(50001, 1); + loot2.items.emplace_back(50002, 1); + loot2.currencies[currency::kGold] = 50; + loot2.currencies[currency::kGlory] = 10; + + loot1.Merge(loot2); + + EXPECT_EQ(2u, loot1.items.size()); + EXPECT_EQ(3, loot1.items[0].second); // 50001: 2 + 1 + EXPECT_EQ(1, loot1.items[1].second); // 50002: 1 + EXPECT_EQ(150, loot1.currencies[currency::kGold]); // 100 + 50 + EXPECT_EQ(10, loot1.currencies[currency::kGlory]); +} + +// Test LootTablesRegistry +class LootTablesRegistryTest : public ::testing::Test { +protected: + LootTablesRegistry registry; + + void SetUp() override { + } +}; + +TEST_F(LootTablesRegistryTest, EmptyRegistry) { + EXPECT_EQ(0u, registry.GetTableCount()); + EXPECT_EQ(nullptr, registry.GetTable("nonexistent")); +} + +TEST_F(LootTablesRegistryTest, RegisterTable) { + auto table = std::make_shared("test_table"); + registry.RegisterTable(table); + + EXPECT_EQ(1u, registry.GetTableCount()); + EXPECT_NE(nullptr, registry.GetTable("test_table")); +} + +TEST_F(LootTablesRegistryTest, UnregisterTable) { + auto table = std::make_shared("test_table"); + registry.RegisterTable(table); + registry.UnregisterTable("test_table"); + + EXPECT_EQ(0u, registry.GetTableCount()); + EXPECT_EQ(nullptr, registry.GetTable("test_table")); +} + +TEST_F(LootTablesRegistryTest, Clear) { + auto table1 = std::make_shared("table1"); + auto table2 = std::make_shared("table2"); + registry.RegisterTable(table1); + registry.RegisterTable(table2); + + registry.Clear(); + + EXPECT_EQ(0u, registry.GetTableCount()); +} + +// Test cycle detection +TEST_F(LootTablesRegistryTest, CycleDetection) { + // Create tables with cycle: A -> B -> C -> A + auto tableA = std::make_shared("table_a"); + tableA->AddEntry(LootEntry::NestedTable("table_b", 10000)); + + auto tableB = std::make_shared("table_b"); + tableB->AddEntry(LootEntry::NestedTable("table_c", 10000)); + + auto tableC = std::make_shared("table_c"); + tableC->AddEntry(LootEntry::NestedTable("table_a", 10000)); + + registry.RegisterTable(tableA); + registry.RegisterTable(tableB); + registry.RegisterTable(tableC); + + auto errors = registry.Validate(); + EXPECT_FALSE(errors.empty()); + // Should detect cycle + bool found_cycle_error = false; + for (const auto &error : errors) { + if (error.find("Cycle") != std::string::npos) { + found_cycle_error = true; + break; + } + } + EXPECT_TRUE(found_cycle_error); +} + +TEST_F(LootTablesRegistryTest, InvalidNestedTableReference) { + auto table = std::make_shared("test_table"); + table->AddEntry(LootEntry::NestedTable("nonexistent_table", 10000)); + + registry.RegisterTable(table); + + auto errors = registry.Validate(); + EXPECT_FALSE(errors.empty()); + bool found_reference_error = false; + for (const auto &error : errors) { + if (error.find("non-existent") != std::string::npos) { + found_reference_error = true; + break; + } + } + EXPECT_TRUE(found_reference_error); +} + +// Test weighted distribution +class WeightedDistributionTest : public ::testing::Test { +protected: + LootTable::Ptr table; + LootTablesRegistry registry; + + void SetUp() override { + table = std::make_shared("distribution_test"); + table->SetProbabilityWidth(10000); + table->SetMaxItemsPerRoll(1); + table->AddFilter(LootFilter::FromVnum(12345)); + + // Add entries with different weights + auto entry1 = LootEntry::Item(50001, 7000); // 70% + entry1.allow_duplicates = true; + auto entry2 = LootEntry::Item(50002, 2000); // 20% + entry2.allow_duplicates = true; + auto entry3 = LootEntry::Item(50003, 1000); // 10% + entry3.allow_duplicates = true; + + table->AddEntry(entry1); + table->AddEntry(entry2); + table->AddEntry(entry3); + + registry.RegisterTable(table); + } +}; + +TEST_F(WeightedDistributionTest, DistributionApproximatelyCorrect) { + std::map counts; + const int iterations = 10000; + + GenerationContext ctx; + for (int i = 0; i < iterations; ++i) { + auto loot = table->Generate(ctx, ®istry); + for (const auto &[vnum, count] : loot.items) { + counts[vnum] += count; + } + } + + // Check that distribution is approximately correct (within 10% tolerance) + double total = counts[50001] + counts[50002] + counts[50003]; + if (total > 0) { + double ratio1 = counts[50001] / total; + double ratio2 = counts[50002] / total; + double ratio3 = counts[50003] / total; + + // These checks are approximate - weighted random has variance + EXPECT_GT(ratio1, 0.5); // Should be around 70% + EXPECT_LT(ratio1, 0.9); + EXPECT_GT(ratio2, 0.1); // Should be around 20% + EXPECT_LT(ratio2, 0.35); + EXPECT_GT(ratio3, 0.02); // Should be around 10% + EXPECT_LT(ratio3, 0.25); + } +} + +#endif // HAVE_YAML + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp :