diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..c1afde54 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,78 @@ +## Code Guidelines + +This file provides guidance when working with code in this repository. + +## Conversation Guidelines +- Primary Objective: Engage in honest, insight-driven dialogue that advances understanding. + +### Core Principles +- Intellectual honesty: Share genuine insights without unnecessary flattery or dismissiveness +- Critical engagement: Push on important considerations rather than accepting ideas at face value +- Balanced evaluation: Present both positive and negative opinions only when well-reasoned and warranted +- Directional clarity: Focus on whether ideas move us forward or lead us astray + +### What to Avoid +- Sycophantic responses or unwarranted positivity +- Dismissing ideas without proper consideration +- Superficial agreement or disagreement +- Flattery that doesn't serve the conversation + +### Success Metric +The only currency that matters: Does this advance or halt productive thinking? If we're heading down an unproductive path, point it out directly. + +## C++23 Modernization Guidelines + +When working on the MUD codebase, follow these modern C++23 practices: + +### Required Modern Features +- **Strings**: Use `fmt::format` instead of printf family, `std::string_view` for parameters, `std::string` for owned strings +- **Containers**: Use `std::vector`, `std::array`, `std::span` instead of C arrays, `std::unordered_map` for hash tables +- **Memory**: Smart pointers (`std::unique_ptr`, `std::shared_ptr`) instead of raw pointers, RAII for all resources +- **Algorithms**: `std::ranges` and `std::views` instead of manual loops, range-based for loops +- **Error Handling**: `std::expected` instead of error codes, `std::optional` for nullable values +- **Enums**: `magic_enum` for enum-to-string conversion +- **JSON**: `nlohmann/json` for all JSON processing +- **CLI**: `cxxopts` for command line argument parsing +- **Testing**: `Catch2` for unit tests and test-driven development +- **Constants**: Named constants (`constexpr`/`constinit`) instead of bare numbers (magic numbers) + +### Forbidden Legacy Practices +- No `printf`, `sprintf`, `char*` manipulation, `malloc`/`free`, `NULL`, C-style casts, `#define` constants +- No manual memory management or C-style arrays +- No bare numbers/magic numbers - use named constants + +### Code Cleanup Requirements +- Remove unused `#include` headers from legacy code +- Replace magic numbers with named constants +- Clean up commented-out code and obsolete functions + +### Code Style Examples +```cpp +// āœ… Modern string handling +void send_message(std::string_view msg, std::span recipients) { + auto formatted = fmt::format("Message: {}", msg); + for (const auto& player : recipients) { + player.send(formatted); + } +} + +// āœ… Named constants instead of magic numbers +constexpr int MAX_PLAYERS_PER_ROOM = 50; +constexpr std::chrono::seconds SAVE_INTERVAL{300}; + +// āœ… Modern error handling +auto parse_command(std::string_view input) -> std::expected { + if (input.empty()) return std::unexpected(ParseError::EmptyInput); + // ... parsing logic + return Command{cmd_name, args}; +} + +// āœ… Modern ranges +auto online_players = all_players + | std::views::filter(&Player::is_online) + | std::views::transform(&Player::get_name); +``` + +### Required Libraries +- Standard: ``, ``, ``, `` +- External: `libfmt`, `nlohmann/json`, `magic_enum`, `cxxopts`, `spdlog`, `Catch2` diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 27ec6960..3cbdc2bb 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -11,5 +11,8 @@ jobs: - uses: jwlawson/actions-setup-cmake@v1.13 with: build-version: 3.22.0 - - run: cmake . - - run: make + - run: cmake -B build -G Ninja + - run: cmake --build build + - run: mv lib.default lib + - run: build/tests + - run: build/fierymud --check diff --git a/.gitignore b/.gitignore index 3a5cfbb6..4f5341ed 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ cbuild/ .venv __pycache__ .aider* +.cache/ +.serena/ diff --git a/Analysis.md b/Analysis.md new file mode 100644 index 00000000..c29e92dc --- /dev/null +++ b/Analysis.md @@ -0,0 +1,156 @@ +# FieryMUD Comprehensive Analysis Report + +## šŸ“Š **Executive Summary** + +**Codebase Scale**: 108 C++ files, ~80K+ LOC +**Architecture**: Modern C++23 MUD server with legacy CircleMUD foundations +**Overall Health**: ⚔ **Good** with modernization opportunities + +--- + +## šŸ—ļø **Architecture Overview** + +### **Core Systems** +- **Main Loop**: `main.cpp` → `comm.cpp` - robust networking & game loop +- **Command Processing**: `interpreter.cpp` - privilege-based dispatch system +- **World Management**: `db.cpp` - JSON-based persistence & zone loading +- **Object Lifecycle**: `handler.cpp` - centralized game object management + +### **Key Subsystems** +- **Classes**: Warrior, Cleric, Sorcerer, Rogue (`class.cpp`, specialized files) +- **Magic**: Circle-based memorization system (`spell_mem.cpp`) +- **Combat**: Turn-based mechanics (`fight.cpp`) +- **Scripting**: DG Scripts for dynamic content (`dg_*.cpp`) +- **OLC**: In-game content editing (`*edit.cpp`) + +--- + +## šŸ“ˆ **Quality Assessment** + +### **Strengths** āœ… +- **Modern C++23** standards adoption +- **Comprehensive STL usage** (string, vector, containers) +- **Excellent documentation** via CLAUDE.md +- **Professional formatting** with fmt library +- **JSON serialization** replacing legacy formats +- **Robust testing** framework (Catch2) + +### **Areas for Improvement** āš ļø +- **16 TODO/FIXME** items identified requiring attention +- **Buffer function usage** (`strcpy`, `sprintf`) in legacy components +- **Memory management** patterns could benefit from RAII +- **Cyclomatic complexity** in some combat/spell logic + +--- + +## šŸ”’ **Security Analysis** + +### **Risk Level**: 🟨 **Medium** + +### **Identified Concerns** +1. **Buffer Functions**: Use of `strcpy`/`sprintf` in `dg_olc.cpp` + - **Impact**: Potential buffer overflows + - **Recommendation**: Replace with safer alternatives (`fmt`, `std::string`) + +2. **Password Handling**: Basic encryption patterns detected + - **Location**: `players.cpp:structs.hpp` + - **Status**: Uses crypt(3) - acceptable but could modernize + +3. **Input Validation**: Command processing appears robust + - **Privilege System**: Well-implemented access controls + - **Command Dispatch**: Centralized validation through `interpreter.cpp` + +### **Positive Security Features** +- No obvious SQL injection vectors (JSON-based persistence) +- Privilege-based command system +- Input sanitization patterns present + +--- + +## ⚔ **Performance Characteristics** + +### **Efficiency Metrics** +- **Loop Count**: 1,897 iterations across 108 files +- **Algorithmic Complexity**: One O(n²) reference in `magic.cpp` +- **Memory Usage**: Mixed C/C++ patterns, room for optimization + +### **Performance Strengths** +- **Modern STL**: Efficient container usage +- **Networking**: Robust socket handling in `comm.cpp` +- **Game Loop**: Well-structured main loop architecture + +### **Optimization Opportunities** +- **String Operations**: Heavy string processing could benefit from string_view +- **Combat System**: Complex calculations could use caching +- **Database Access**: JSON parsing optimization potential + +--- + +## šŸ›ļø **Architectural Patterns** + +### **Design Patterns Identified** +- **Strategy Pattern**: Found in spell parsing, movement systems +- **Observer Pattern**: Event system implementation +- **Factory Pattern**: Character/object creation systems + +### **Architectural Quality** +- **Separation of Concerns**: Well-defined module boundaries +- **Modularity**: Clear subsystem organization +- **Extensibility**: Plugin-friendly design patterns +- **Legacy Integration**: Smooth CircleMUD evolution + +### **Modernization Opportunities** +- **RAII Adoption**: More consistent resource management +- **Template Usage**: Generic programming opportunities +- **Const-Correctness**: Additional const usage possible + +--- + +## šŸ“‹ **Priority Recommendations** + +### **High Priority** šŸ”“ +1. **Security Hardening** + - Replace buffer functions in `dg_olc.cpp` + - Audit input validation paths + - Consider password hash modernization + +2. **Code Quality** + - Address 16 TODO/FIXME items + - Standardize memory management patterns + - Improve cyclomatic complexity in complex functions + +### **Medium Priority** 🟔 +3. **Performance Optimization** + - Profile and optimize O(n²) algorithms + - Implement string operation optimizations + - Cache frequently computed values + +4. **Architecture Enhancement** + - Increase RAII usage + - Template-based generic programming + - Enhanced const-correctness + +### **Low Priority** 🟢 +5. **Documentation & Testing** + - Expand test coverage + - API documentation improvements + - Code commenting standards + +--- + +## šŸŽÆ **Overall Assessment** + +**FieryMUD represents a well-architected, modern C++ MUD server with strong foundations.** The codebase demonstrates excellent evolution from legacy CircleMUD origins while embracing modern C++23 standards. + +**Key Success Factors:** +- Solid architectural patterns +- Modern language features adoption +- Comprehensive testing framework +- Well-documented development practices + +**Strategic Focus Areas:** +- Security hardening (buffer functions) +- Performance optimization opportunities +- Continued modernization journey + +**Recommendation**: Continue current development trajectory with focused attention on security improvements and performance optimization. \ No newline at end of file diff --git a/CLAN_SCRIPTING.md b/CLAN_SCRIPTING.md new file mode 100644 index 00000000..8ad1709e --- /dev/null +++ b/CLAN_SCRIPTING.md @@ -0,0 +1,261 @@ +# Clan Scripting Features + +This document describes the clan-related variables and functions available in DG Scripts for accessing clan information from scripts attached to mobs, objects, and rooms. + +## Available Clan Variables + +All clan variables are accessed through character fields using the syntax `%character.field%`. If the character is not in a clan, most fields return empty strings or "0" as appropriate. + +### Basic Clan Information + +| Variable | Type | Description | Non-clan Return | +|----------|------|-------------|-----------------| +| `%actor.clan%` | string | Clan name | empty string | +| `%actor.clan_rank%` | string | Character's rank title | empty string | +| `%actor.clan_id%` | number | Unique clan ID | "0" | +| `%actor.clan_abbr%` | string | Clan abbreviation | empty string | +| `%actor.clan_description%` | string | Clan description | empty string | +| `%actor.clan_motd%` | string | Clan message of the day | empty string | + +### Clan Settings + +| Variable | Type | Description | Non-clan Return | +|----------|------|-------------|-----------------| +| `%actor.clan_dues%` | number | Monthly dues amount (platinum) | "0" | +| `%actor.clan_app_fee%` | number | Application fee (platinum) | "0" | +| `%actor.clan_min_level%` | number | Minimum level to apply | "0" | +| `%actor.clan_member_count%` | number | Total number of clan members | "0" | + +### Clan Treasury + +| Variable | Type | Description | Non-clan Return | +|----------|------|-------------|-----------------| +| `%actor.clan_treasure_total%` | number | Total clan wealth (all coins) | "0" | +| `%actor.clan_treasure_platinum%` | number | Platinum coins in treasury | "0" | +| `%actor.clan_treasure_gold%` | number | Gold coins in treasury | "0" | +| `%actor.clan_treasure_silver%` | number | Silver coins in treasury | "0" | +| `%actor.clan_treasure_copper%` | number | Copper coins in treasury | "0" | + +### Clan Facilities + +| Variable | Type | Description | Non-clan Return | +|----------|------|-------------|-----------------| +| `%actor.clan_bank_room%` | number | Bank room vnum (-1 if disabled) | "-1" | +| `%actor.clan_chest_room%` | number | Chest room vnum (-1 if disabled) | "-1" | +| `%actor.clan_hall_room%` | number | Hall room vnum (-1 if disabled) | "-1" | + +### Clan Permissions + +| Variable | Type | Description | Non-clan Return | +|----------|------|-------------|-----------------| +| `%actor.clan_can_deposit%` | boolean | Can deposit to clan bank (1/0) | "0" | +| `%actor.clan_can_withdraw%` | boolean | Can withdraw from clan bank (1/0) | "0" | +| `%actor.clan_can_store%` | boolean | Can store items in clan chest (1/0) | "0" | +| `%actor.clan_can_retrieve%` | boolean | Can retrieve items from clan chest (1/0) | "0" | + +## Script Examples + +### Example 1: Clan Welcome Message + +``` +* Trigger: Greet (100) - Entry trigger on mob +* Check if player has a clan and welcome them appropriately + +if %actor.clan% + say Welcome, %actor.clan_rank% %actor.name% of %actor.clan%! + say Your clan has %actor.clan_member_count% members. + if %actor.clan_treasure_total% > 1000 + say Your clan's treasury is doing well with %actor.clan_treasure_total% coins! + else + say Perhaps your clan could use some more funds... + end +else + say Greetings, clanless wanderer. + say Consider joining one of our fine guilds! +end +``` + +### Example 2: Clan Bank Guard + +``` +* Trigger: Command (100) - Command trigger on guard mob +* Commands: deposit withdraw bank + +set room_vnum %actor.clan_bank_room% + +if %room_vnum% == -1 + say Your clan's bank access has been disabled. + halt +end + +if %actor.in_room% != %room_vnum% + say You can only access your clan bank from room %room_vnum%. + halt +end + +if %cmd% == deposit + if %actor.clan_can_deposit% + say You may proceed with your deposit. + else + say You don't have permission to deposit funds. + end +elseif %cmd% == withdraw + if %actor.clan_can_withdraw% + say You may proceed with your withdrawal. + else + say You don't have permission to withdraw funds. + end +else + say Use 'deposit' or 'withdraw' to access your clan bank. +end +``` + +### Example 3: Clan Information Board + +``` +* Trigger: Command (100) - Command trigger on object +* Commands: read info + +if %actor.clan% + %echo% &Y--- %actor.clan% Information ---&n + %echo% &WClan:&n %actor.clan% (%actor.clan_abbr%) + %echo% &WYour Rank:&n %actor.clan_rank% + %echo% &WDescription:&n %actor.clan_description% + %echo% &WMembers:&n %actor.clan_member_count% + %echo% &WDues:&n %actor.clan_dues% platinum per month + %echo% &WApplication Fee:&n %actor.clan_app_fee% platinum + %echo% &WMinimum Level:&n %actor.clan_min_level% + %echo% &WTreasury:&n %actor.clan_treasure_platinum%p %actor.clan_treasure_gold%g %actor.clan_treasure_silver%s %actor.clan_treasure_copper%c + + if %actor.clan_bank_room% != -1 + %echo% &WBank Room:&n %actor.clan_bank_room% + else + %echo% &RBank access is disabled&n + end + + if %actor.clan_chest_room% != -1 + %echo% &WChest Room:&n %actor.clan_chest_room% + else + %echo% &RChest access is disabled&n + end + + if %actor.clan_hall_room% != -1 + %echo% &WHall Room:&n %actor.clan_hall_room% + else + %echo% &RHall access is disabled&n + end + + %echo% &WPermissions:&n + if %actor.clan_can_deposit% + %echo% - Can deposit to bank + end + if %actor.clan_can_withdraw% + %echo% - Can withdraw from bank + end + if %actor.clan_can_store% + %echo% - Can store items in chest + end + if %actor.clan_can_retrieve% + %echo% - Can retrieve items from chest + end + + if %actor.clan_motd% + %echo% &YMOTD:&n %actor.clan_motd% + end +else + %echo% You are not a member of any clan. +end +``` + +### Example 4: Clan Recruitment Officer + +``` +* Trigger: Speech (100) - Speech trigger on mob +* Speech: join apply clan + +if %actor.clan% + say You're already a member of %actor.clan%! + halt +end + +* List available clans with their requirements +say Here are the available clans: + +* This would need to be customized for each clan in your MUD +say The Warriors Guild requires level 10 and costs 100 platinum to join. +say The Merchants Guild requires level 5 and costs 50 platinum to join. +say The Mages Circle requires level 15 and costs 200 platinum to join. + +say Use 'clan apply ' to submit an application. +``` + +### Example 5: Conditional Clan Facilities + +``` +* Trigger: Entry (100) - Room entry trigger +* Only allow clan members into their bank room + +if %actor.clan% + set required_room %actor.clan_bank_room% + if %required_room% == %self.vnum% + %echo% %actor.name% enters the %actor.clan% bank. + %echoaround% %actor% %actor.name% shows their %actor.clan% credentials. + elseif %required_room% != -1 + %echo% This is not your clan's designated bank room. + %echo% Your clan's bank is located in room %required_room%. + %teleport% %actor% %required_room% + else + %echo% Your clan's bank access has been disabled. + %teleport% %actor% 3001 + end +else + %echo% Only clan members may enter this area. + %teleport% %actor% 3001 +end +``` + +### Example 6: Clan Hall Access Control + +``` +* Trigger: Entry (100) - Room entry trigger +* Restricts access to clan hall to clan members only + +if %actor.clan% + set clan_hall %actor.clan_hall_room% + if %clan_hall% == -1 + %echo% Your clan does not have a designated hall. + %teleport% %actor% 3001 + elseif %clan_hall% == %self.vnum% + %echo% Welcome to the %actor.clan% clan hall, %actor.clan_rank% %actor.name%! + %echoaround% %actor% %actor.name% enters the clan hall with pride. + else + %echo% This is not your clan's hall. + %echo% Your clan hall is located in room %clan_hall%. + %teleport% %actor% 3001 + end +else + %echo% Only clan members may enter this sacred hall. + %teleport% %actor% 3001 +end +``` + +## Notes + +- All clan variables are read-only from scripts +- Characters can only be members of one clan at a time (first membership is used) +- NPCs are never considered clan members +- Permission checks use the character's current rank and individual permissions +- Room vnums of -1 indicate disabled facilities +- Treasury values are returned as integers (coin amounts) +- Boolean permissions return "1" for true, "0" for false + +## Integration with Existing Systems + +These clan variables integrate seamlessly with the existing DG scripting system and can be used in: + +- Mobile triggers (entry, greet, command, speech, etc.) +- Object triggers (command, get, drop, etc.) +- Room triggers (entry, command, etc.) +- Any context where character variables are available + +The implementation follows the existing variable naming conventions and error handling patterns used throughout the DG scripting system. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..ab303269 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,223 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Conversation Guidelines +- Primary Objective: Engage in honest, insight-driven dialogue that advances understanding. + +### Core Principles +- Intellectual honesty: Share genuine insights without unnecessary flattery or dismissiveness +- Critical engagement: Push on important considerations rather than accepting ideas at face value +- Balanced evaluation: Present both positive and negative opinions only when well-reasoned and warranted +- Directional clarity: Focus on whether ideas move us forward or lead us astray + +### What to Avoid +- Sycophantic responses or unwarranted positivity +- Dismissing ideas without proper consideration +- Superficial agreement or disagreement +- Flattery that doesn't serve the conversation + +### Success Metric +The only currency that matters: Does this advance or halt productive thinking? If we're heading down an unproductive path, point it out directly. + +## C++23 Modernization Guidelines + +When working on the MUD codebase, follow these modern C++23 practices: + +### Required Modern Features +- **Strings**: Use `fmt::format` instead of printf family, `std::string_view` for parameters, `std::string` for owned strings +- **Containers**: Use `std::vector`, `std::array`, `std::span` instead of C arrays, `std::unordered_map` for hash tables +- **Memory**: Smart pointers (`std::unique_ptr`, `std::shared_ptr`) instead of raw pointers, RAII for all resources +- **Algorithms**: `std::ranges` and `std::views` instead of manual loops, range-based for loops +- **Error Handling**: `std::expected` instead of error codes, `std::optional` for nullable values +- **Enums**: `magic_enum` for enum-to-string conversion +- **JSON**: `nlohmann/json` for all JSON processing +- **CLI**: `cxxopts` for command line argument parsing +- **Testing**: `Catch2` for unit tests and test-driven development +- **Constants**: Named constants (`constexpr`/`constinit`) instead of bare numbers (magic numbers) + +### Forbidden Legacy Practices +- No `printf`, `sprintf`, `char*` manipulation, `malloc`/`free`, `NULL`, C-style casts, `#define` constants +- No manual memory management or C-style arrays +- No bare numbers/magic numbers - use named constants + +### Code Cleanup Requirements +- Remove unused `#include` headers from legacy code +- Replace magic numbers with named constants +- Clean up commented-out code and obsolete functions + +### Code Style Examples +```cpp +// āœ… Modern string handling +void send_message(std::string_view msg, std::span recipients) { + auto formatted = fmt::format("Message: {}", msg); + for (const auto& player : recipients) { + player.send(formatted); + } +} + +// āœ… Named constants instead of magic numbers +constexpr int MAX_PLAYERS_PER_ROOM = 50; +constexpr std::chrono::seconds SAVE_INTERVAL{300}; + +// āœ… Modern error handling +auto parse_command(std::string_view input) -> std::expected { + if (input.empty()) return std::unexpected(ParseError::EmptyInput); + // ... parsing logic + return Command{cmd_name, args}; +} + +// āœ… Modern ranges +auto online_players = all_players + | std::views::filter(&Player::is_online) + | std::views::transform(&Player::get_name); +``` + +### Required Libraries +- Standard: ``, ``, ``, `` +- External: `libfmt`, `nlohmann/json`, `magic_enum`, `cxxopts`, `spdlog`, `Catch2` + +## Project Build System + +This project uses CMake with Ninja generator for faster builds. Key commands: + +### Building the MUD +```bash +cmake -B build -G Ninja --install-prefix /opt/MUD/ . +cmake --build build --target install +``` + +### Running Tests +```bash +cmake --build build --target tests +./build/tests +``` + +### Development Build +```bash +cmake -B build -G Ninja . +cmake --build build +``` + +### Running the MUD +```bash +# First time setup - copy default library files +cp -r lib.default lib + +# Run the MUD +./build/fierymud +``` + +## Architecture Overview + +FieryMUD is a modern C++ MUD (Multi-User Dungeon) evolved from CircleMUD. Key architectural components: + +### Core Systems +- **Main Loop**: `src/main.cpp` - Entry point and initialization +- **Networking**: `src/comm.cpp` - Socket handling, game loop, telnet protocol +- **Command Processing**: `src/interpreter.cpp` - Command dispatch and privilege system +- **Database**: `src/db.cpp` - World loading, player persistence, zone management +- **Object Management**: `src/handler.cpp` - Game object lifecycle and manipulation + +### Key Data Structures (`src/structs.hpp`) +- `CharData` - Players and NPCs with abilities, equipment, effects +- `ObjData` - Game objects (weapons, armor, containers, etc.) +- `RoomData` - Locations with exits and contents +- `DescriptorData` - Network connection management + +### Game Systems +- **Classes**: Warrior, Cleric, Sorcerer, Rogue in `src/class.cpp`, `src/warrior.cpp`, etc. +- **Magic System**: Circle-based spell memorization in `src/spell_mem.cpp` +- **Combat**: Turn-based combat in `src/fight.cpp` +- **Skills**: Comprehensive skill system in `src/skills.cpp` +- **Scripting**: DG Scripts for dynamic content (`src/dg_*.cpp`) + +### OLC (Online Creation) +In-game content editing system: +- `src/medit.cpp` - Mobile editing +- `src/oedit.cpp` - Object editing +- `src/redit.cpp` - Room editing +- `src/zedit.cpp` - Zone editing + +### File Organization +- `src/` - All source code +- `lib/` - Runtime data (player files, world data, configuration) +- `lib/world/` - Zone files in JSON format +- `lib/players/` - Player save files +- `scripts/` - Python utilities for data conversion + +## Development Guidelines + +### Code Style +- Modern C++23 features encouraged +- Uses STL containers (`std::string`, `std::vector`, etc.) +- Extensive use of `fmt` library for string formatting +- JSON for data serialization with `nlohmann/json` + +### Testing +- Uses Catch2 testing framework +- Test files in `tests/` directory +- Run tests with `./build/tests` +- **IMPORTANT**: Changes should be tested using Catch2 - add appropriate unit tests for new functionality + +### Common Development Tasks +- **Adding new commands**: Add to `src/interpreter.cpp` command table +- **New spells**: Add to `src/spells.cpp` and spell tables +- **New classes**: Follow pattern of existing class files +- **World content**: Use OLC system in-game or edit JSON files directly +- **Database changes**: Update save/load functions in relevant files + +### Important Constants +- Game constants in `src/constants.cpp` +- Magic numbers and flags in `src/structs.hpp` +- Skill/spell definitions in respective system files + +## Configuration + +### Required Setup +1. Copy `lib.default/` to `lib/` on first run +2. Edit `lib/etc/` files for game configuration +3. World data in `lib/world/` as JSON files + +### VSCode Debugging +```json +{ + "type": "lldb", + "request": "launch", + "name": "Debug", + "program": "${workspaceFolder}/build/fierymud", + "args": [], + "cwd": "${workspaceFolder}" +} +``` + +## Testing Guidelines + +### Manual Testing for Game Features +When implementing game features (especially clan system, commands, and user interactions): + +1. **Build and Run**: Always build and run the MUD for manual testing of user-facing features + ```bash + cmake --build build + ./build/fierymud + ``` + +2. **Test User Scenarios**: Test both regular user and god-level access patterns + - Regular clan members should be able to use basic clan commands + - Gods should have access to all clan administrative commands + - Test permission boundaries and error conditions + +3. **Test Room-Based Features**: For features requiring specific room locations: + - Verify room-based validation works correctly (clan bank/chest rooms) + - Test VNUM/RNUM conversion issues if room numbers are involved + - Test from both correct and incorrect room locations + +4. **Interactive Testing**: The user can perform comprehensive testing of implemented features + - User has access to god-level characters for testing administrative functions + - User can test clan membership scenarios and permission systems + - User can verify command abbreviation and fuzzy matching functionality + +### Important Testing Notes +- **Automated Tests**: Run `./build/tests` for unit tests, but game features require manual testing +- **Integration Testing**: Manual testing is essential for command parsing, permission systems, and user interactions +- **User Feedback**: The user can provide immediate feedback on functionality, UI/UX, and edge cases that automated tests might miss \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index f25016fd..ef13fd7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,10 +30,13 @@ add_compile_options(-g) include_directories(src/) FILE(GLOB sources src/*.cpp) +# Create a list of sources without main.cpp for tests +set(test_sources ${sources}) +list(REMOVE_ITEM test_sources ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp) # Testing using Catch2 -FILE(GLOB test_files test/*.cpp) -add_executable(tests ${sources} ${test_files}) +FILE(GLOB test_files tests/*.cpp) +add_executable(tests ${test_sources} ${test_files}) target_link_libraries(tests PRIVATE Catch2::Catch2 version crypt fmt::fmt nlohmann_json::nlohmann_json magic_enum::magic_enum) @@ -54,6 +57,6 @@ set(CMAKE_C_COMPILER gcc) add_executable(fierymud ${sources}) -target_link_libraries(fierymud PRIVATE version crypt fmt::fmt nlohmann_json::nlohmann_json magic_enum::magic_enum)# asio::asio) +target_link_libraries(fierymud PRIVATE version crypt fmt::fmt nlohmann_json::nlohmann_json magic_enum::magic_enum cxxopts::cxxopts)# asio::asio) install (TARGETS fierymud DESTINATION bin) diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 00000000..4ba4c702 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,383 @@ +# FieryMUD Web Interface - System Design + +## Overview + +A modern web application for viewing, editing, and managing FieryMUD world content with real-time synchronization and visual representation. + +## Architecture + +### High-Level System Design +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Frontend │ │ Backend API │ │ FieryMUD │ +│ React SPA │◄──►│ Node.js/ │◄──►│ C++ Core │ +│ │ │ Express │ │ │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ │ │ + │ │ │ + ā–¼ ā–¼ ā–¼ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ State Mgmt │ │ Database │ │ File System │ +│ React Query │ │ PostgreSQL │ │ lib/world/ │ +│ Zustand │ │ Redis Cache │ │ lib/players/ │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +### Technology Stack +- **Frontend**: React 18, TypeScript, Tailwind CSS, React Query, Zustand +- **Backend**: Node.js 20, Express.js, TypeScript, Prisma ORM +- **Database**: PostgreSQL 15 (primary), Redis 7 (cache) +- **Real-time**: Socket.io, Server-Sent Events +- **Visualization**: D3.js, React Flow, Three.js +- **Authentication**: JWT with role-based access + +## Data Architecture + +### Core Entities +```typescript +interface Character { + id: string; + name: string; + class: CharacterClass; + level: number; + experience: number; + stats: CharacterStats; + equipment: Equipment[]; + inventory: Item[]; + location: RoomReference; + online: boolean; + lastLogin: Date; + clan?: ClanReference; +} + +interface Room { + vnum: number; + title: string; + description: string; + zone: ZoneReference; + exits: Exit[]; + flags: RoomFlag[]; + sector: SectorType; + contents: Item[]; + occupants: Character[]; + position: WorldCoordinate; +} + +interface Zone { + vnum: number; + name: string; + description: string; + level: LevelRange; + resetMode: ResetMode; + resetTime: number; + rooms: Room[]; + mobs: Mobile[]; + objects: Item[]; + bounds: ZoneBounds; +} + +interface Mobile { + vnum: number; + name: string; + shortDesc: string; + longDesc: string; + level: number; + stats: MobileStats; + equipment: Equipment[]; + flags: MobileFlag[]; + triggers: Trigger[]; + location?: RoomReference; +} + +interface Item { + vnum: number; + name: string; + shortDesc: string; + longDesc: string; + type: ItemType; + flags: ItemFlag[]; + values: ItemValues; + location?: Location; +} +``` + +## API Design + +### REST Endpoints + +#### Character Management +``` +GET /api/characters - List all characters +GET /api/characters/:id - Get character details +PUT /api/characters/:id - Update character +DELETE /api/characters/:id - Delete character +POST /api/characters/:id/actions - Perform character actions (move, get, etc) +GET /api/characters/:id/mail - Get character mail +POST /api/characters/:id/mail - Send mail to character +``` + +#### World Management +``` +GET /api/zones - List all zones +GET /api/zones/:vnum - Get zone details +PUT /api/zones/:vnum - Update zone +POST /api/zones - Create new zone +DELETE /api/zones/:vnum - Delete zone + +GET /api/rooms - List/search rooms +GET /api/rooms/:vnum - Get room details +PUT /api/rooms/:vnum - Update room +POST /api/rooms - Create new room +DELETE /api/rooms/:vnum - Delete room + +GET /api/mobs - List/search mobs +GET /api/mobs/:vnum - Get mob details +PUT /api/mobs/:vnum - Update mob +POST /api/mobs - Create new mob +DELETE /api/mobs/:vnum - Delete mob + +GET /api/objects - List/search objects +GET /api/objects/:vnum - Get object details +PUT /api/objects/:vnum - Update object +POST /api/objects - Create new object +DELETE /api/objects/:vnum - Delete object +``` + +#### World Visualization +``` +GET /api/world/map - Get world map data +GET /api/world/graph/:zone - Get zone connectivity graph +GET /api/world/coordinates - Get spatial positioning data +``` + +#### Real-time Events +``` +WebSocket /ws/game-events - Real-time game state updates +WebSocket /ws/world-changes - World edit notifications +``` + +### API Middleware & Security +- **Authentication**: JWT tokens with role-based permissions +- **Rate Limiting**: 100 req/min for standard users, 1000 req/min for admins +- **Input Validation**: Zod schemas for all API inputs +- **Error Handling**: Standardized error responses with proper HTTP codes +- **Logging**: Structured logging with correlation IDs + +## Frontend Architecture + +### Component Structure +``` +src/ +ā”œā”€ā”€ components/ +│ ā”œā”€ā”€ character/ +│ │ ā”œā”€ā”€ CharacterList.tsx +│ │ ā”œā”€ā”€ CharacterDetail.tsx +│ │ ā”œā”€ā”€ CharacterInventory.tsx +│ │ └── CharacterMail.tsx +│ ā”œā”€ā”€ world/ +│ │ ā”œā”€ā”€ ZoneList.tsx +│ │ ā”œā”€ā”€ RoomEditor.tsx +│ │ ā”œā”€ā”€ MobEditor.tsx +│ │ ā”œā”€ā”€ ObjectEditor.tsx +│ │ └── WorldVisualization.tsx +│ ā”œā”€ā”€ ui/ +│ │ ā”œā”€ā”€ Layout.tsx +│ │ ā”œā”€ā”€ Navigation.tsx +│ │ ā”œā”€ā”€ DataGrid.tsx +│ │ └── EditModal.tsx +│ └── common/ +│ ā”œā”€ā”€ ErrorBoundary.tsx +│ ā”œā”€ā”€ LoadingSpinner.tsx +│ └── Toast.tsx +ā”œā”€ā”€ pages/ +│ ā”œā”€ā”€ Dashboard.tsx +│ ā”œā”€ā”€ Characters.tsx +│ ā”œā”€ā”€ World.tsx +│ └── Settings.tsx +ā”œā”€ā”€ hooks/ +│ ā”œā”€ā”€ useCharacters.ts +│ ā”œā”€ā”€ useWorld.ts +│ └── useWebSocket.ts +ā”œā”€ā”€ stores/ +│ ā”œā”€ā”€ authStore.ts +│ ā”œā”€ā”€ worldStore.ts +│ └── uiStore.ts +└── utils/ + ā”œā”€ā”€ api.ts + ā”œā”€ā”€ validation.ts + └── formatters.ts +``` + +### Key Features +- **Responsive Design**: Mobile-first approach with desktop enhancements +- **Real-time Updates**: Live data synchronization via WebSocket +- **Optimistic Updates**: Immediate UI feedback with rollback on error +- **Infinite Scrolling**: Efficient handling of large datasets +- **Search & Filtering**: Advanced search with faceted filtering +- **Bulk Operations**: Multi-select actions for efficiency +- **Undo/Redo**: Change history with rollback capabilities + +## World Visualization System + +### Map Types +1. **Zone Overview**: High-level zone layout with connections +2. **Room Network**: Detailed room-to-room connections within zones +3. **3D World View**: Immersive 3D representation of world spaces +4. **Minimap**: Quick navigation overlay + +### Visualization Technologies +- **D3.js**: Data-driven room network graphs +- **React Flow**: Interactive node-based editing +- **Three.js**: 3D world visualization +- **Canvas API**: High-performance 2D rendering + +### Visual Features +- **Interactive Navigation**: Click-to-navigate, zoom, pan +- **Real-time Updates**: Live position tracking of characters +- **Contextual Information**: Hover tooltips with room/mob/object details +- **Visual Indicators**: Color-coding for room types, danger levels, etc. +- **Path Finding**: Visual route planning between locations + +## Data Synchronization + +### File System Integration +``` +MUD File Format → Parser → Database → API → Frontend + ↓ ↓ ↓ ↓ ↓ + lib/world/ TypeScript PostgreSQL REST React + *.wld Parsers Tables JSON Components + *.mob + *.obj + *.zon +``` + +### Synchronization Strategy +- **Two-way Sync**: Web changes → Database → File system +- **Change Detection**: File watchers for external modifications +- **Conflict Resolution**: Last-write-wins with change history +- **Backup Strategy**: Automatic backups before modifications + +## Security & Permissions + +### Authentication System +- **Role-based Access**: Admin, Builder, Player, Guest +- **Permission Matrix**: Granular permissions per entity type +- **Session Management**: Secure JWT with refresh tokens +- **Audit Logging**: Complete change tracking + +### Data Protection +- **Input Sanitization**: XSS and injection prevention +- **Rate Limiting**: API endpoint protection +- **CORS Configuration**: Secure cross-origin access +- **Data Validation**: Server-side validation for all operations + +## Performance Considerations + +### Backend Optimization +- **Database Indexing**: Optimized queries for common operations +- **Caching Strategy**: Redis for frequently accessed data +- **Connection Pooling**: Efficient database connections +- **Background Jobs**: Async file processing + +### Frontend Optimization +- **Code Splitting**: Dynamic imports for route-based loading +- **Virtual Scrolling**: Efficient large list rendering +- **Image Optimization**: WebP with fallbacks +- **Bundle Analysis**: Webpack bundle optimization + +## Implementation Phases + +### Phase 1: Core Infrastructure (4-6 weeks) +- Backend API foundation +- Database schema and migrations +- Authentication system +- Basic frontend shell + +### Phase 2: Character Management (3-4 weeks) +- Character viewing and editing +- Mail system integration +- Inventory management +- Real-time character tracking + +### Phase 3: World Management (4-6 weeks) +- Zone/room/mob/object CRUD operations +- File system synchronization +- Basic world visualization +- Bulk import/export tools + +### Phase 4: Advanced Features (6-8 weeks) +- 3D world visualization +- Advanced search and filtering +- Real-time collaboration +- Mobile optimization + +## Deployment Architecture + +### Development Environment +``` +Frontend Dev Server (Vite) → Backend Dev Server → Local MUD Instance +Port 3000 Port 3001 Port 4000 +``` + +### Production Environment +``` +Load Balancer → Web Server (Nginx) → API Servers → Database Cluster + → Redis Cluster + → MUD Server +``` + +### DevOps Pipeline +- **CI/CD**: GitHub Actions with automated testing +- **Containerization**: Docker for consistent deployments +- **Monitoring**: Application performance monitoring +- **Backup Strategy**: Automated database and file backups + +## API Examples + +### Character Data +```json +{ + "id": "char_123", + "name": "Gandalf", + "class": "sorcerer", + "level": 50, + "experience": 2500000, + "stats": { + "strength": 18, + "intelligence": 25, + "wisdom": 22, + "constitution": 20, + "charisma": 19, + "dexterity": 16 + }, + "location": { + "vnum": 3001, + "title": "Temple of Midgaard", + "zone": "Midgaard" + }, + "online": true, + "lastLogin": "2025-01-15T10:30:00Z" +} +``` + +### Room Data +```json +{ + "vnum": 3001, + "title": "Temple of Midgaard", + "description": "A magnificent temple with soaring columns...", + "zone": { + "vnum": 30, + "name": "Midgaard" + }, + "exits": [ + {"direction": "north", "to": 3002, "keywords": ["door"], "flags": []}, + {"direction": "south", "to": 3000, "keywords": [], "flags": []} + ], + "flags": ["sanctuary", "no_attack"], + "sector": "inside", + "coordinates": {"x": 0, "y": 0, "z": 0} +} +``` + +This design provides a comprehensive foundation for a modern MUD management interface while maintaining compatibility with the existing FieryMUD architecture. \ No newline at end of file diff --git a/src/act.clan.cpp b/src/act.clan.cpp new file mode 100644 index 00000000..5930de4c --- /dev/null +++ b/src/act.clan.cpp @@ -0,0 +1,2078 @@ +#include "act.hpp" + +#include "arguments.hpp" +#include "clan.hpp" +#include "comm.hpp" +#include "db.hpp" +#include "dg_scripts.hpp" +#include "find.hpp" +#include "function_registration.hpp" +#include "handler.hpp" +#include "logging.hpp" +#include "messages.hpp" +#include "modify.hpp" +#include "money.hpp" +#include "objects.hpp" +#include "pfiles.hpp" +#include "players.hpp" +#include "screen.hpp" +#include "utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Template implementations will be defined later + +// Legacy permission checking - updated for new enum +// Legacy function replaced by decorator system +bool can_use_clan_command(const CharData *ch, ClanPermission required_permission) { + return has_clan_permission_or_god(ch, required_permission); +} + +// Forward declaration - defined after permissions namespace +uint32_t get_clan_permissions(const CharData *ch); + +// ============================================================================= +// MODERN C++23 HELPER FUNCTIONS - ELIMINATE STATIC CASTS +// ============================================================================= + +// Type-safe enum to underlying type conversion using magic_enum +template constexpr auto to_underlying(Enum e) noexcept { return magic_enum::enum_integer(e); } + +// ============================================================================= +// PERMISSION SYSTEM ALIASES - 3-STAGE DESIGN +// ============================================================================= + +// Readable aliases for the 3-stage permission system +namespace permissions { +// Define distinct permission flags for the help system +constexpr uint32_t PUBLIC_FLAG = 1; // Everyone can run - flag value 1 +constexpr uint32_t GOD_FLAG = 2; // Only gods can run - flag value 2 +constexpr uint32_t CLAN_BASE_FLAG = 4; // Base flag for clan permissions - starts at 4 + +constexpr auto PUBLIC = std::optional(PUBLIC_FLAG); // Everyone can run +constexpr auto GOD_ONLY = std::optional(GOD_FLAG); // Only gods can run + +// Helper function for specific clan permissions +constexpr std::optional clan_permission(ClanPermission perm) { + // Shift clan permissions to bits 8+ to avoid conflict with system flags (bits 0-7) + constexpr uint32_t CLAN_PERM_SHIFT = 8; + return std::optional(CLAN_BASE_FLAG | (to_underlying(perm) << CLAN_PERM_SHIFT)); +} + +// Basic clan membership (any clan member can access) +constexpr auto CLAN_MEMBER = std::optional(CLAN_BASE_FLAG); +} // namespace permissions + +// For function registry compatibility - provide appropriate permission flags for help display +uint32_t get_clan_permissions(const CharData *ch) { + uint32_t user_permissions = 0; + + // Everyone gets public permissions + user_permissions |= permissions::PUBLIC_FLAG; + + // Gods get god permissions (which includes public) and all clan permissions + if (GET_LEVEL(ch) >= LVL_IMMORT) { + user_permissions |= permissions::GOD_FLAG; + user_permissions |= permissions::CLAN_BASE_FLAG; + + // Give gods all possible clan permissions + constexpr uint32_t CLAN_PERM_SHIFT = 8; + for (int perm = 0; perm < static_cast(ClanPermission::MAX_PERMISSIONS); ++perm) { + user_permissions |= (1u << (CLAN_PERM_SHIFT + perm)); + } + } + + // Clan members get clan-specific permissions based on their rank + auto clan = get_clan(ch); + if (clan) { + auto member = get_clan_member(ch); + if (member) { + const auto &ranks = clan.value()->ranks(); + if (member->rank_index >= 0 && member->rank_index < static_cast(ranks.size())) { + const auto &rank = ranks[member->rank_index]; + + // Add clan base flag for all clan members + user_permissions |= permissions::CLAN_BASE_FLAG; + + // Add specific clan permissions based on rank + // Shift clan permissions to bits 8+ to avoid conflict with system flags (bits 0-7) + constexpr uint32_t CLAN_PERM_SHIFT = 8; + for (int i = 0; i < static_cast(ClanPermission::MAX_PERMISSIONS); ++i) { + ClanPermission perm = static_cast(i); + if (rank.has_permission(perm)) { + uint32_t perm_flag = static_cast(perm) << CLAN_PERM_SHIFT; + user_permissions |= perm_flag; + } + } + } + } + } + + return user_permissions; +} + +// Type-safe enum to size_t conversion for bitset operations +template constexpr size_t to_bitset_index(Enum e) noexcept { + return static_cast(to_underlying(e)); +} + +// Set permission in bitset using type-safe conversion +inline void set_permission(PermissionSet &permissions, ClanPermission perm) { permissions.set(to_bitset_index(perm)); } + +// Legacy macro - now replaced by CLAN_COMMAND decorator system +// #define CLANCMD(name) static void(name)(CharData * ch, Arguments argument) + +// ============================================================================= +// CLAN SYSTEM IMPLEMENTATION +// ============================================================================= +// +// This file implements the clan system using a modern C++ decorator pattern +// for permission checking. All commands use the CLAN_COMMAND macro with +// appropriate permission decorators defined in clan.hpp. +// +// Key improvements: +// - Template-based permission decorators eliminate boilerplate +// - Centralized permission logic in clan.hpp +// - Clean separation between public and member commands +// - Fuzzy search for clan names/abbreviations +// - Consistent error handling and messaging +// +// ============================================================================= + +// SECTION: Permission and validation helpers + +// Fuzzy string matching utilities +namespace { +// Calculate Levenshtein distance between two strings +size_t levenshtein_distance(std::string_view s1, std::string_view s2) { + const size_t len1 = s1.length(); + const size_t len2 = s2.length(); + + // Create a matrix to store distances + std::vector> dp(len1 + 1, std::vector(len2 + 1)); + + // Initialize first row and column + for (size_t i = 0; i <= len1; ++i) + dp[i][0] = i; + for (size_t j = 0; j <= len2; ++j) + dp[0][j] = j; + + // Fill the matrix + for (size_t i = 1; i <= len1; ++i) { + for (size_t j = 1; j <= len2; ++j) { + if (std::tolower(s1[i - 1]) == std::tolower(s2[j - 1])) { + dp[i][j] = dp[i - 1][j - 1]; // No operation needed + } else { + dp[i][j] = 1 + std::min({ + dp[i - 1][j], // Deletion + dp[i][j - 1], // Insertion + dp[i - 1][j - 1] // Substitution + }); + } + } + } + + return dp[len1][len2]; +} + +// Calculate fuzzy match score (0.0 = no match, 1.0 = perfect match) +double fuzzy_match_score(std::string_view target, std::string_view query) { + if (query.empty()) + return 0.0; + if (target.empty()) + return 0.0; + + // Exact match gets perfect score + if (std::equal(target.begin(), target.end(), query.begin(), query.end(), + [](char a, char b) { return std::tolower(a) == std::tolower(b); })) { + return 1.0; + } + + // Prefix match gets high score + if (target.length() >= query.length()) { + bool is_prefix = std::equal(query.begin(), query.end(), target.begin(), + [](char a, char b) { return std::tolower(a) == std::tolower(b); }); + if (is_prefix) { + return 0.9; + } + } + + // Substring match gets good score + std::string target_lower(target); + std::string query_lower(query); + std::ranges::transform(target_lower, target_lower.begin(), ::tolower); + std::ranges::transform(query_lower, query_lower.begin(), ::tolower); + + if (target_lower.find(query_lower) != std::string::npos) { + return 0.8; + } + + // Use Levenshtein distance for fuzzy matching + size_t distance = levenshtein_distance(target, query); + size_t max_len = std::max(target.length(), query.length()); + + if (distance > max_len) + return 0.0; + + // Score based on how many edits are needed relative to string length + double similarity = 1.0 - (static_cast(distance) / max_len); + + // Only consider it a fuzzy match if similarity is above threshold + return similarity > 0.5 ? similarity : 0.0; +} + +// Find best fuzzy matches for clan names/abbreviations +std::vector> find_fuzzy_clan_matches(std::string_view query, size_t max_results = 3) { + std::vector> matches; + + for (const auto &clan : clan_repository.all()) { + // Check both name and abbreviation (with and without ANSI codes) + double name_score = + std::max(fuzzy_match_score(clan->name(), query), fuzzy_match_score(strip_ansi(clan->name()), query)); + + double abbr_score = std::max(fuzzy_match_score(clan->abbreviation(), query), + fuzzy_match_score(strip_ansi(clan->abbreviation()), query)); + + double best_score = std::max(name_score, abbr_score); + + if (best_score > 0.0) { + matches.emplace_back(clan, best_score); + } + } + + // Sort by score (highest first) + std::ranges::sort(matches, [](const auto &a, const auto &b) { return a.second > b.second; }); + + // Limit results + if (matches.size() > max_results) { + matches.resize(max_results); + } + + return matches; +} + +// Helper function to find a clan with full fuzzy search support +std::pair, bool> find_clan_with_fuzzy_search(std::string_view clan_name) { + if (clan_name.empty()) { + return {std::nullopt, false}; + } + + // Try exact matches first + auto found_clan = clan_repository.find_by_abbreviation(clan_name); + if (!found_clan) { + found_clan = clan_repository.find_by_name(clan_name); + } + + // If not found, try matching against stripped ANSI versions + if (!found_clan) { + for (const auto &c : clan_repository.all()) { + if (strip_ansi(c->abbreviation()) == clan_name || strip_ansi(c->name()) == clan_name) { + found_clan = c; + break; + } + } + } + + // If still not found, try fuzzy matching + bool used_fuzzy = false; + if (!found_clan) { + auto fuzzy_matches = find_fuzzy_clan_matches(clan_name, 1); + if (!fuzzy_matches.empty() && fuzzy_matches[0].second >= 0.7) { + found_clan = fuzzy_matches[0].first; + used_fuzzy = true; + } + } + + return {found_clan, used_fuzzy}; +} + +// Helper function to show clan suggestions when lookup fails +void show_clan_suggestions(CharData *ch, std::string_view searched_name) { + auto fuzzy_matches = find_fuzzy_clan_matches(searched_name, 3); + if (!fuzzy_matches.empty()) { + char_printf(ch, "Did you mean:\n"); + for (const auto &[suggested_clan, score] : fuzzy_matches) { + char_printf(ch, " {} ({})\n", strip_ansi(suggested_clan->name()), + strip_ansi(suggested_clan->abbreviation())); + } + } +} +} // namespace + +// SECTION: Clan lookup and command helpers +// Helper function to display clan command help +static void display_clan_help(CharData *ch) { + char_printf(ch, "Available clan commands:\n"); + + // Get user's current permission flags + auto permissions = get_clan_permissions(ch); + + // Check if user is already in a clan + bool is_clan_member = get_clan(ch).has_value(); + + // Use function registry to print available commands with "clan_" prefix + auto help_text = + FunctionRegistry::print_available_with_prefix("clan_", permissions, [](std::string_view name) -> std::string { + // Strip "clan_" prefix for cleaner display + if (name.starts_with("clan_")) { + return std::string(name.substr(5)); + } + return std::string(name); + }); + + // Apply context-sensitive filtering + std::string filtered_help; + std::istringstream help_stream(help_text); + std::string line; + + while (std::getline(help_stream, line)) { + // Hide 'apply' command from clan members + if (is_clan_member && line.find("apply") != std::string::npos && + line.find("Apply to join a clan") != std::string::npos) { + continue; // Skip this line + } + + filtered_help += line + "\n"; + } + + if (filtered_help.empty()) { + char_printf(ch, " No clan commands are available to you.\n"); + } else { + char_printf(ch, "{}", filtered_help); + } + + // Add god-specific note if applicable + if (GET_LEVEL(ch) >= LVL_IMMORT) { + char_printf(ch, "\nNote: As a god, you must specify the clan for most commands.\n"); + } +} + +// Helper function to validate room access for clan operations +static bool validate_clan_room_access(CharData *ch, room_num clan_room, const std::string &room_type) { + if (GET_LEVEL(ch) >= LVL_IMMORT) { + return true; // Gods can access from anywhere + } + + if (clan_room == NOWHERE) { + char_printf(ch, "Clan {} access is currently disabled.\n", room_type); + return false; + } + + // Convert clan_room (VNUM) to RNUM for comparison with ch->in_room (RNUM) + room_num clan_room_rnum = real_room(clan_room); + if (clan_room_rnum == NOWHERE) { + char_printf(ch, "Clan {} room does not exist.\n", room_type); + return false; + } + + if (ch->in_room != clan_room_rnum) { + char_printf(ch, "You must be in your clan's {} room to perform this action.\n", room_type); + return false; + } + + return true; +} + +// Helper function to handle god permission checks with consistent messaging +static bool check_god_only_operation(CharData *ch, const std::string &operation) { + if (GET_LEVEL(ch) < LVL_IMMORT) { + char_printf(ch, "Only gods may {}.\n", operation); + return false; + } + return true; +} + +static std::optional find_clan_for_command(CharData *ch, Arguments &argument) { + if (GET_LEVEL(ch) < LVL_IMMORT) { + // For non-gods, always use their own clan (ignore any arguments) + auto clan = get_clan(ch); + if (!clan) { + char_printf(ch, "You are not a member of a clan.\n"); + return std::nullopt; + } + return clan; + } + + if (argument.empty()) { + auto clan = get_clan(ch); + if (clan) { + return clan; + } + char_printf(ch, "Please specify a clan.\n"); + return std::nullopt; + } + + // Gods can specify the clan. + std::string searched_clan_name; + bool used_fuzzy_match = false; + auto clan = [&argument, &searched_clan_name, &used_fuzzy_match]() -> std::optional { + auto clan_id_opt = argument.try_shift_number(); + if (clan_id_opt) { + searched_clan_name = std::to_string(*clan_id_opt); + return clan_repository.find_by_id(*clan_id_opt); + } + auto clan_name = argument.shift(); + searched_clan_name = std::string(clan_name); + + auto [found_clan, used_fuzzy] = find_clan_with_fuzzy_search(clan_name); + used_fuzzy_match = used_fuzzy; + + return found_clan; + }(); + + if (!clan) { + char_printf(ch, "No such clan '{}'.\n", searched_clan_name); + + // Provide fuzzy search suggestions + show_clan_suggestions(ch, searched_clan_name); + + // Debug: List available clans for gods + if (GET_LEVEL(ch) >= LVL_IMMORT) { + char_printf(ch, "\nAll available clans:\n"); + for (const auto &c : clan_repository.all()) { + char_printf(ch, " ID: {} Name: \"{}\" (stripped: \"{}\") Abbr: \"{}\" (stripped: \"{}\")\n", c->id(), + c->name(), strip_ansi(c->name()), c->abbreviation(), strip_ansi(c->abbreviation())); + } + } + return std::nullopt; + } + + // Notify user if we used fuzzy matching to find the clan + if (used_fuzzy_match) { + char_printf(ch, "Assuming you meant '{}' ({}).\n", strip_ansi((*clan)->name()), + strip_ansi((*clan)->abbreviation())); + } + + // For gods, allow access to any clan even if they're not a member + if (GET_LEVEL(ch) >= LVL_IMMORT) { + return *clan; + } + + // Check if the character is actually a member of this clan + if (get_clan_id(ch) == (*clan)->id()) { + return *clan; + } + + char_printf(ch, "You are not a member of that clan.\n"); + return std::nullopt; +} + +// SECTION: Public clan commands (no permissions required) + +CLAN_COMMAND(clan_list, clan_permissions::RequiresNoPermissions) { + if (clan_repository.count() == 0) { + char_printf(ch, "No clans have formed yet.\n"); + return; + } + + /* List clans, # of members, power, and app fee */ + paging_printf(ch, AUND " Num Clan Members App Fee/Lvl\n" ANRM); + for (const auto &clan : clan_repository.all()) { + paging_printf(ch, fmt::format("[{:3}] {:<{}} {:3} {:5}p/{:3}\n", clan->id(), clan->abbreviation(), + 18 + count_color_chars(clan->abbreviation()) * 2, clan->member_count(), + clan->app_fee(), clan->min_application_level())); + } + + start_paging(ch); +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_list, "clan_list", CommandCategory::CLAN, permissions::PUBLIC, + "list - List all clans and their application fees"); + +// SECTION: Financial clan commands (require specific permissions) + +CLAN_COMMAND(clan_deposit, clan_permissions::RequiresDepositFunds) { + auto clan = find_clan_for_command(ch, argument); + if (!clan) { + return; + } + + // Check bank room access + if (!validate_clan_room_access(ch, clan.value()->bank_room(), "bank")) { + return; + } + + auto money_opt = parse_money(argument.get()); + if (!money_opt || money_opt->value() <= 0) { + char_printf(ch, "How much do you want to deposit?\n"); + return; + } + + Money coins = *money_opt; + /* Gods have bottomless pockets */ + if (GET_LEVEL(ch) < LVL_GOD) { + Money owned = ch->points.money; + if (!owned.charge(coins)) { + char_printf(ch, "You do not have that kind of money!\n"); + return; + } + for (int i = 0; i < NUM_COIN_TYPES; i++) { + ch->points.money[i] -= coins[i]; + } + } + + // Add money to clan treasury + if (auto result = clan.value()->admin_add_treasure(coins); !result) { + char_printf(ch, "Failed to add money to clan treasury: {}\n", result.error()); + return; + } + + save_player_char(ch); + + char_printf(ch, "You deposit {} into {}'s account.\n", statemoney(coins), clan.value()->abbreviation()); + clan_repository.save(); +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_deposit, "clan_deposit", CommandCategory::CLAN, + permissions::clan_permission(ClanPermission::DEPOSIT_FUNDS), + "deposit - Deposit money into the clan treasury"); + +CLAN_COMMAND(clan_withdraw, clan_permissions::RequiresWithdrawFunds) { + + auto clan = find_clan_for_command(ch, argument); + if (!clan) { + return; + } + + // Check bank room access + if (!validate_clan_room_access(ch, clan.value()->bank_room(), "bank")) { + return; + } + + auto money_opt = parse_money(argument.get()); + if (!money_opt || money_opt->value() <= 0) { + char_printf(ch, "How much do you want to withdraw?\n"); + return; + } + + Money coins = *money_opt; + + auto treasure = clan.value()->treasure(); + if (treasure.value() < coins.value()) { + char_printf(ch, "The clan doesn't have that much money.\n"); + return; + } + + // Subtract money from clan treasury + if (auto result = clan.value()->admin_subtract_treasure(coins); !result) { + char_printf(ch, "Failed to withdraw money from clan treasury: {}\n", result.error()); + return; + } + + for (int i = 0; i < NUM_COIN_TYPES; i++) { + ch->points.money[i] += coins[i]; + } + save_player_char(ch); + + char_printf(ch, "You withdraw from {}'s account: {}\n", clan.value()->abbreviation(), statemoney(coins)); + clan_repository.save(); +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_withdraw, "clan_withdraw", CommandCategory::CLAN, + permissions::clan_permission(ClanPermission::WITHDRAW_FUNDS), + "withdraw - Withdraw money from the clan treasury"); + +// SECTION: Storage clan commands (require storage permissions) + +CLAN_COMMAND(clan_store, clan_permissions::RequiresStoreItems) { + ObjData *obj; + + auto clan = find_clan_for_command(ch, argument); + if (!clan) { + return; + } + + // Check chest room access + if (!validate_clan_room_access(ch, clan.value()->chest_room(), "chest")) { + return; + } + + // Try to parse an optional number followed by object name + auto quantity_opt = argument.try_shift_number(); + if (quantity_opt && *quantity_opt != 1) { + char_printf(ch, "You can only store one item at a time.\n"); + return; + } + + auto obj_name = argument.shift(); + if (obj_name.empty()) { + char_printf(ch, "Store what in the clan vault?\n"); + return; + } + if (!(obj = find_obj_in_list(ch->carrying, find_vis_by_name(ch, std::string(obj_name).data())))) { + char_printf(ch, "You aren't carrying {} {}.\n", an(obj_name), obj_name); + } else if (OBJ_FLAGGED(obj, ITEM_NODROP)) { + act("You can't store $p in $P because it's CURSED!", false, ch, obj, nullptr, TO_CHAR); + } else if (GET_OBJ_TYPE(obj) == ITEM_CONTAINER && obj->contains) { + char_printf(ch, "You must empty the container before storing it.\n"); + } else if (GET_OBJ_TYPE(obj) == ITEM_MONEY) { + char_printf(ch, "You cannot store money items in the clan vault.\n"); + } else if (GET_OBJ_VNUM(obj) == -1) { + char_printf(ch, "You cannot store objects without a valid item number.\n"); + } else { + obj_from_char(obj); + auto vnum = GET_OBJ_VNUM(obj); + std::string name = obj->short_description ? obj->short_description : "unknown item"; + extract_obj(obj); + + if (auto result = clan.value()->admin_add_storage_item(vnum, 1); !result) { + char_printf(ch, "Failed to store item in clan vault: {}\n", result.error()); + return; + } + + save_player_char(ch); + char_printf(ch, "You store {} in the clan vault.\n", name); + clan.value()->notify(ch, fmt::format("{} stores {} in the clan vault.", GET_NAME(ch), name)); + clan_repository.save(); + } +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_store, "clan_store", CommandCategory::CLAN, + permissions::clan_permission(ClanPermission::STORE_ITEMS), + "store - Store an item in the clan vault"); + +CLAN_COMMAND(clan_retrieve, clan_permissions::RequiresRetrieveItems) { + auto clan = find_clan_for_command(ch, argument); + if (!clan) { + return; + } + + // Check chest room access + if (!validate_clan_room_access(ch, clan.value()->chest_room(), "chest")) { + return; + } + + // Try to parse an optional number followed by object name + auto quantity_opt = argument.try_shift_number(); + if (quantity_opt && *quantity_opt != 1) { + char_printf(ch, "You can only retrieve one item at a time.\n"); + return; + } + + auto obj_name = argument.shift(); + if (obj_name.empty()) { + char_printf(ch, "Retrieve what from the clan vault?\n"); + return; + } + // We need to find the vnum in the storage that matches this name + ObjectId obj_vnum = 0; + bool found = false; + + // Find the object in storage matching the name + // Build a temporary cache for faster lookup + std::unordered_map name_to_vnum_cache; + for (const auto &[vnum, count] : clan.value()->storage()) { + if (count <= 0) + continue; + + auto obj = read_object(vnum, VIRTUAL); + if (!obj) + continue; + + // Cache all possible names for this object + std::string names = obj->name; + std::string name_token; + std::istringstream name_stream(names); + while (name_stream >> name_token) { + name_to_vnum_cache[name_token] = vnum; + } + extract_obj(obj); + } + + // Quick lookup in cache + auto cache_it = name_to_vnum_cache.find(std::string(obj_name)); + if (cache_it != name_to_vnum_cache.end()) { + obj_vnum = cache_it->second; + found = true; + } + + if (!found) { + char_printf(ch, "There is no {} {} in the clan vault.\n", an(obj_name), obj_name); + return; + } + + auto obj = read_object(obj_vnum, VIRTUAL); + if (!obj) { + char_printf(ch, "Unable to read object from vault.\n"); + return; + } + + obj_to_char(obj, ch); + + save_player_char(ch); + char_printf(ch, "You retrieve {} from the clan vault.\n", obj->short_description); + clan.value()->admin_remove_storage_item(obj_vnum, 1); + + clan.value()->notify(ch, fmt::format("{} retrieves {} from the clan vault.", GET_NAME(ch), obj->short_description)); + clan_repository.save(); +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_retrieve, "clan_retrieve", CommandCategory::CLAN, + permissions::clan_permission(ClanPermission::RETRIEVE_ITEMS), + "retrieve - Retrieve an item from the clan vault"); + +// SECTION: Communication commands (require chat permissions) + +CLAN_COMMAND(clan_tell, clan_permissions::RequiresClanMembership) { + CharData *me = REAL_CHAR(ch); + + if (EFF_FLAGGED(ch, EFF_SILENCE)) { + char_printf(ch, "You cannot speak while silenced.\n"); + return; + } + + if (!speech_ok(ch, 0)) + return; + + auto clan = find_clan_for_command(ch, argument); + if (!clan) { + return; + } + + if (argument.empty()) { + char_printf(ch, "Tell your clan what?\n"); + return; + } + + auto speech = drunken_speech(std::string(argument.get()), GET_COND(ch, DRUNK)); + + char_printf(ch, AFMAG "You tell {}" AFMAG ", '" AHMAG "{}" AFMAG "'\n" ANRM, + ch->player.level < LVL_IMMORT ? "your clan" : clan.value()->abbreviation(), speech); + + for (const auto &member : clan.value()->members()) { + // Find the online character by name + auto target = clan_security::find_clan_member_safe(member.name); + if (!target || !target->desc || !IS_PLAYING(target->desc)) + continue; + + auto tch = REAL_CHAR(target); + if (!tch || tch == me) + continue; + + if (STATE(target->desc) != CON_PLAYING || PLR_FLAGGED(tch, PLR_WRITING) || PLR_FLAGGED(tch, PLR_MAILING) || + EDITING(target->desc)) { + if (!PRF_FLAGGED(tch, PRF_OLCCOMM)) + continue; + } + + if (PRF_FLAGGED(tch, PRF_NOCLANCOMM)) + continue; + + char_printf(tch, AFMAG "{} tells {}" AFMAG ", '" AHMAG "{}" AFMAG "'\n" ANRM, + GET_INVIS_LEV(me) > GET_LEVEL(tch) ? "Someone" : GET_NAME(me), + tch->player.level < LVL_IMMORT ? "your clan" : clan.value()->abbreviation(), speech); + } + + // Send to snooping gods + auto clan_id = clan.value()->id(); + auto it = clan_snoop_table.find(clan_id); + if (it != clan_snoop_table.end()) { + for (auto snoop_ch : it->second) { + if (!snoop_ch || !snoop_ch->desc || snoop_ch == me) + continue; + + if (STATE(snoop_ch->desc) != CON_PLAYING) + continue; + + // Send snoop message with special formatting + char_printf(snoop_ch, AFYEL "[SNOOP {}] {} tells clan, '" AHMAG "{}" AFYEL "'\n" ANRM, + clan.value()->abbreviation(), + GET_INVIS_LEV(me) > GET_LEVEL(snoop_ch) ? "Someone" : GET_NAME(me), speech); + } + } +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_tell, "clan_tell", CommandCategory::CLAN, + permissions::clan_permission(ClanPermission::CLAN_CHAT), + "tell - Send a message to all clan members"); + +// SECTION: Administrative commands (require management permissions) + +CLAN_COMMAND(clan_set, clan_permissions::RequiresRankManagement) { + auto clan = find_clan_for_command(ch, argument); + if (!clan) { + return; + } + + auto cmd = argument.shift(); + + if (cmd.empty()) { + char_printf(ch, "Available clan set options:\n"); + char_printf(ch, " abbr - Set clan abbreviation\n"); + char_printf(ch, " addrank - Add a new rank\n"); + char_printf(ch, " appfee <amount> - Set application fee (platinum)\n"); + char_printf(ch, " applev <level> - Set minimum application level\n"); + char_printf(ch, " delrank <number> - Delete a rank\n"); + char_printf(ch, " dues <amount> - Set monthly dues (platinum)\n"); + char_printf(ch, " name <text> - Set clan name\n"); + char_printf(ch, " permissions <rank> <list> - Set permissions for a rank\n"); + char_printf(ch, " title <rank> <text> - Set title for a rank\n"); + if (GET_LEVEL(ch) >= LVL_GOD) { + char_printf(ch, " bankroom <vnum> - Set bank room (-1 to disable, gods only)\n"); + char_printf(ch, " chestroom <vnum> - Set chest room (-1 to disable, gods only)\n"); + } + return; + } + + if (matches_start(cmd, "abbr")) { + auto new_abbreviation = argument.get(); + if (new_abbreviation.empty()) { + char_printf(ch, "What do you want to set the clan abbreviation to?\n"); + return; + } + + if (ansi_strlen(new_abbreviation) > Clan::MAX_CLAN_ABBR_LEN) { + char_printf(ch, "Clan abbreviations may be at most {} characters in length.\n", Clan::MAX_CLAN_ABBR_LEN); + return; + } + + // Gods have direct admin access + if (GET_LEVEL(ch) >= LVL_IMMORT) { + clan.value()->admin_set_abbreviation(std::string(new_abbreviation)); + char_printf(ch, "Clan abbreviation updated to {}.\n", new_abbreviation); + } else { + if (!has_clan_permission(ch, ClanPermission::LEADER_OVERRIDE)) { + char_printf(ch, "You don't have permission to change the clan abbreviation.\n"); + return; + } + clan.value()->admin_set_abbreviation(std::string(new_abbreviation)); + char_printf(ch, "Clan abbreviation updated to {}.\n", new_abbreviation); + } + } else if (matches_start(cmd, "addrank")) { + if (argument.empty()) { + char_printf(ch, "What do you want to name the new rank?\n"); + return; + } + + auto title = argument.get(); + // Create a default rank with empty privileges + PermissionSet privileges; + ClanRank rank(std::string(title), privileges); + + // Gods have direct admin access + if (GET_LEVEL(ch) >= LVL_IMMORT) { + if (auto result = clan.value()->admin_add_rank(rank); !result) { + char_printf(ch, "Failed to add rank: {}\n", result.error()); + return; + } + char_printf(ch, "New rank '{}' added.\n", title); + } else { + if (!has_clan_permission(ch, ClanPermission::MANAGE_RANKS)) { + char_printf(ch, "You don't have permission to add ranks.\n"); + return; + } + if (auto result = clan.value()->admin_add_rank(rank); !result) { + char_printf(ch, "Failed to add rank: {}\n", result.error()); + return; + } + char_printf(ch, "New rank '{}' added.\n", title); + } + } else if (matches_start(cmd, "appfee")) { + auto fee_opt = argument.try_shift_number(); + if (!fee_opt) { + char_printf(ch, "How much platinum should the clan's application fee be?\n"); + return; + } + + if (*fee_opt < 0) { + char_printf(ch, "Application fee cannot be negative.\n"); + return; + } + + // Gods have direct admin access + if (GET_LEVEL(ch) >= LVL_IMMORT) { + clan.value()->admin_set_app_fee(*fee_opt); + char_printf(ch, "{}'s application fee is now {} platinum.\n", clan.value()->name(), *fee_opt); + } else { + if (!has_clan_permission(ch, ClanPermission::SET_APP_FEES)) { + char_printf(ch, "You don't have permission to change application fees.\n"); + return; + } + clan.value()->admin_set_app_fee(*fee_opt); + char_printf(ch, "{}'s application fee is now {} platinum.\n", clan.value()->name(), *fee_opt); + } + } else if (matches_start(cmd, "applev")) { + auto level_opt = argument.try_shift_number(); + if (!level_opt) { + char_printf(ch, "What should the clan's minimum application level be?\n"); + return; + } + + if (*level_opt < 1 || *level_opt > LVL_IMPL) { + char_printf(ch, "The minimum application level must be between 1 and {}.\n", LVL_IMPL); + return; + } + + // Gods have direct admin access + if (GET_LEVEL(ch) >= LVL_IMMORT) { + clan.value()->admin_set_min_application_level(*level_opt); + char_printf(ch, "{}'s minimum application level is now {}.\n", clan.value()->name(), *level_opt); + } else { + if (!has_clan_permission(ch, ClanPermission::SET_APP_LEVEL)) { + char_printf(ch, "You don't have permission to change application level.\n"); + return; + } + clan.value()->admin_set_min_application_level(*level_opt); + char_printf(ch, "{}'s minimum application level is now {}.\n", clan.value()->name(), *level_opt); + } + } else if (matches_start(cmd, "delrank")) { + auto rank_opt = argument.try_shift_number(); + if (!rank_opt) { + char_printf(ch, "Which rank do you want to delete?\n"); + return; + } + + if (*rank_opt < 1) { + char_printf(ch, "Rank number must be positive.\n"); + return; + } + + const auto &ranks = clan.value()->ranks(); + if (*rank_opt > ranks.size()) { + char_printf(ch, "Invalid rank number. Clan has {} ranks.\n", ranks.size()); + return; + } + + // NOTE: This would require adding admin_remove_rank to Clan class + // For now, just indicate it's not implemented + char_printf(ch, "Rank deletion is not yet implemented.\n"); + return; + } else if (matches_start(cmd, "dues")) { + auto dues_opt = argument.try_shift_number(); + if (!dues_opt) { + char_printf(ch, "How much platinum should the clan's dues be?\n"); + return; + } + + if (*dues_opt < 0) { + char_printf(ch, "Dues cannot be negative.\n"); + return; + } + + // Gods have direct admin access + if (GET_LEVEL(ch) >= LVL_IMMORT) { + clan.value()->admin_set_dues(*dues_opt); + char_printf(ch, "{}'s monthly dues are now {} platinum.\n", clan.value()->name(), *dues_opt); + } else { + if (!has_clan_permission(ch, ClanPermission::SET_DUES)) { + char_printf(ch, "You don't have permission to change dues.\n"); + return; + } + clan.value()->admin_set_dues(*dues_opt); + char_printf(ch, "{}'s monthly dues are now {} platinum.\n", clan.value()->name(), *dues_opt); + } + } else if (matches_start(cmd, "name")) { + auto new_name = argument.get(); + if (new_name.empty()) { + char_printf(ch, "What do you want to name the clan?\n"); + return; + } + + if (ansi_strlen(new_name) > Clan::MAX_CLAN_NAME_LEN) { + char_printf(ch, "Clan names may be at most {} characters in length.\n", Clan::MAX_CLAN_NAME_LEN); + return; + } + + // Gods have direct admin access + if (GET_LEVEL(ch) >= LVL_IMMORT) { + clan.value()->admin_set_name(std::string(new_name)); + char_printf(ch, "Clan name updated to {}.\n", new_name); + } else { + if (!has_clan_permission(ch, ClanPermission::LEADER_OVERRIDE)) { + char_printf(ch, "You don't have permission to change the clan name.\n"); + return; + } + clan.value()->admin_set_name(std::string(new_name)); + char_printf(ch, "Clan name updated to {}.\n", new_name); + } + } else if (matches_start(cmd, "permissions")) { + auto rank_opt = argument.try_shift_number(); + if (!rank_opt) { + char_printf(ch, "For which rank do you want to set permissions?\n"); + char_printf(ch, "Usage: clan set permissions <rank> <permission1> [permission2] ...\n"); + char_printf(ch, "Available permissions: description motd grant ranks title enroll expel\n"); + char_printf(ch, " promote demote app_fees app_level dues deposit\n"); + char_printf(ch, " withdraw store retrieve alts chat all none\n"); + return; + } + + if (*rank_opt < 1) { + char_printf(ch, "Rank number must be positive.\n"); + return; + } + + const auto &ranks = clan.value()->ranks(); + if (*rank_opt > ranks.size()) { + char_printf(ch, "Invalid rank number. Clan has {} ranks.\n", ranks.size()); + return; + } + + if (argument.empty()) { + char_printf(ch, "What permissions do you want to set for rank {}?\n", *rank_opt); + char_printf(ch, "Available permissions: description motd grant ranks title enroll expel\n"); + char_printf(ch, " promote demote app_fees app_level dues deposit\n"); + char_printf(ch, " withdraw store retrieve alts chat all none\n"); + return; + } + + // Get the current rank + auto current_rank = ranks[*rank_opt - 1]; + PermissionSet new_permissions; + + // Parse permission list + std::string perm_str; + while (!(perm_str = std::string(argument.shift())).empty()) { + if (perm_str == "all") { + new_permissions.set(); // Set all bits + } else if (perm_str == "none") { + new_permissions.reset(); // Clear all bits + } else if (perm_str == "description") { + set_permission(new_permissions, ClanPermission::SET_DESCRIPTION); + } else if (perm_str == "motd") { + set_permission(new_permissions, ClanPermission::SET_MOTD); + } else if (perm_str == "grant") { + set_permission(new_permissions, ClanPermission::LEADER_OVERRIDE); + } else if (perm_str == "ranks") { + set_permission(new_permissions, ClanPermission::MANAGE_RANKS); + } else if (perm_str == "title") { + set_permission(new_permissions, ClanPermission::NONE); + } else if (perm_str == "enroll") { + set_permission(new_permissions, ClanPermission::INVITE_MEMBERS); + } else if (perm_str == "expel") { + set_permission(new_permissions, ClanPermission::KICK_MEMBERS); + } else if (perm_str == "promote") { + set_permission(new_permissions, ClanPermission::PROMOTE_MEMBERS); + } else if (perm_str == "demote") { + set_permission(new_permissions, ClanPermission::DEMOTE_MEMBERS); + } else if (perm_str == "app_fees") { + set_permission(new_permissions, ClanPermission::SET_APP_FEES); + } else if (perm_str == "app_level") { + set_permission(new_permissions, ClanPermission::SET_APP_LEVEL); + } else if (perm_str == "dues") { + set_permission(new_permissions, ClanPermission::SET_DUES); + } else if (perm_str == "deposit") { + set_permission(new_permissions, ClanPermission::DEPOSIT_FUNDS); + } else if (perm_str == "withdraw") { + set_permission(new_permissions, ClanPermission::WITHDRAW_FUNDS); + } else if (perm_str == "store") { + set_permission(new_permissions, ClanPermission::STORE_ITEMS); + } else if (perm_str == "retrieve") { + set_permission(new_permissions, ClanPermission::RETRIEVE_ITEMS); + } else if (perm_str == "alts") { + set_permission(new_permissions, ClanPermission::MANAGE_ALTS); + } else if (perm_str == "chat") { + set_permission(new_permissions, ClanPermission::CLAN_CHAT); + } else { + char_printf(ch, + "Unknown permission '{}'. Use 'clan set permissions {}' to see available permissions.\n", + perm_str, *rank_opt); + return; + } + } + + // Update the rank permissions directly + if (GET_LEVEL(ch) >= LVL_IMMORT) { + if (clan.value()->admin_update_rank_permissions(*rank_opt - 1, new_permissions)) { + char_printf(ch, "Permissions updated for rank {} ({}).\n", *rank_opt, current_rank.title()); + } else { + char_printf(ch, "Failed to update permissions for rank {}.\n", *rank_opt); + return; + } + } else { + if (!has_clan_permission(ch, ClanPermission::MANAGE_RANKS)) { + char_printf(ch, "You don't have permission to change rank permissions.\n"); + return; + } + if (clan.value()->admin_update_rank_permissions(*rank_opt - 1, new_permissions)) { + char_printf(ch, "Permissions updated for rank {} ({}).\n", *rank_opt, current_rank.title()); + } else { + char_printf(ch, "Failed to update permissions for rank {}.\n", *rank_opt); + return; + } + } + } else if (matches_start(cmd, "title")) { + auto rank_opt = argument.try_shift_number(); + if (!rank_opt) { + char_printf(ch, "For which rank do you want to set a title?\n"); + return; + } + + if (*rank_opt < 1) { + char_printf(ch, "Rank number must be positive.\n"); + return; + } + + auto title = argument.get(); + if (title.empty()) { + char_printf(ch, "What title do you want to set?\n"); + return; + } + + if (ansi_strlen(title) > Clan::MAX_CLAN_TITLE_LEN) { + char_printf(ch, "Clan titles may be at most {} characters long.\n", Clan::MAX_CLAN_TITLE_LEN); + return; + } + + const auto &ranks = clan.value()->ranks(); + if (*rank_opt > ranks.size()) { + char_printf(ch, "Invalid rank number. Clan has {} ranks.\n", ranks.size()); + return; + } + + // NOTE: This would require adding admin_update_rank_title to Clan class + // For now, just indicate it's not implemented + char_printf(ch, "Rank title updates are not yet implemented.\n"); + return; + } else if (matches_start(cmd, "bankroom")) { + if (GET_LEVEL(ch) < LVL_GOD) { + char_printf(ch, "Only gods can set clan bank rooms.\n"); + return; + } + + auto room_vnum_opt = argument.try_shift_number(); + if (!room_vnum_opt) { + char_printf(ch, "What room vnum should be the clan's bank room? (Use -1 to disable bank access)\n"); + return; + } + + room_num room_vnum = *room_vnum_opt; + + // Validate room exists if positive vnum + if (room_vnum > 0 && real_room(room_vnum) == NOWHERE) { + char_printf(ch, "Room {} does not exist.\n", room_vnum); + return; + } + + clan.value()->admin_set_bank_room(room_vnum); + if (room_vnum == NOWHERE) { + char_printf(ch, "Clan bank access disabled.\n"); + } else { + char_printf(ch, "Clan bank room set to {}.\n", room_vnum); + } + } else if (matches_start(cmd, "chestroom")) { + if (GET_LEVEL(ch) < LVL_GOD) { + char_printf(ch, "Only gods can set clan chest rooms.\n"); + return; + } + + auto room_vnum_opt = argument.try_shift_number(); + if (!room_vnum_opt) { + char_printf(ch, "What room vnum should be the clan's chest room? (Use -1 to disable chest access)\n"); + return; + } + + room_num room_vnum = *room_vnum_opt; + + // Validate room exists if positive vnum + if (room_vnum > 0 && real_room(room_vnum) == NOWHERE) { + char_printf(ch, "Room {} does not exist.\n", room_vnum); + return; + } + + clan.value()->admin_set_chest_room(room_vnum); + if (room_vnum == NOWHERE) { + char_printf(ch, "Clan chest access disabled.\n"); + } else { + char_printf(ch, "Clan chest room set to {}.\n", room_vnum); + } + } else { + char_printf(ch, "Unknown clan set option '{}'. Use 'clan set' to see available options.\n", cmd); + return; + } + + clan_repository.save(); +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_set, "clan_set", CommandCategory::CLAN, + permissions::clan_permission(ClanPermission::MANAGE_RANKS), + "set <clan> <option> - Set clan properties (admin only)"); + +CLAN_COMMAND(clan_apply, clan_permissions::RequiresNoPermissions) { + if (argument.empty()) { + char_printf(ch, "Apply to which clan?\n"); + return; + } + + auto clan_name = argument.shift(); + auto [clan, used_fuzzy] = find_clan_with_fuzzy_search(clan_name); + + if (!clan) { + char_printf(ch, "No such clan exists.\n"); + show_clan_suggestions(ch, clan_name); + return; + } + + if (used_fuzzy) { + char_printf(ch, "Assuming you meant '{}' ({}).\n", strip_ansi((*clan)->name()), + strip_ansi((*clan)->abbreviation())); + } + + // Check if character meets application requirements + if (GET_LEVEL(ch) < (*clan)->min_application_level()) { + char_printf(ch, "You must be at least level {} to apply to {}.\n", (*clan)->min_application_level(), + (*clan)->name()); + return; + } + + // Check if character can afford application fee + if (GET_LEVEL(ch) < LVL_IMMORT && (*clan)->app_fee() > 0) { + Money app_fee; + app_fee[PLATINUM] = (*clan)->app_fee(); + Money owned = ch->points.money; + if (!owned.charge(app_fee)) { + char_printf(ch, "You cannot afford the {} platinum application fee.\n", (*clan)->app_fee()); + return; + } + + // Charge the fee + for (int i = 0; i < NUM_COIN_TYPES; i++) { + ch->points.money[i] -= app_fee[i]; + } + } + + // Check if already a member + if (get_clan_id(ch) == (*clan)->id()) { + char_printf(ch, "You are already a member of {}.\n", (*clan)->name()); + return; + } + + // For now, applications are disabled - clan members must manually invite + char_printf(ch, "Clan applications are currently disabled. Contact a clan member to be invited.\n"); +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_apply, "clan_apply", CommandCategory::CLAN, permissions::PUBLIC, + "apply <clan> - Apply to join a clan"); + +// SECTION: God-only commands (require CLAN_ADMIN permission) + +CLAN_COMMAND(clan_create, clan_permissions::RequiresPermissions<ClanPermission::CLAN_ADMIN>) { + if (!check_god_only_operation(ch, "create clans")) { + return; + } + + auto abbreviation = argument.shift(); + if (abbreviation.empty()) { + char_printf(ch, "What is the abbreviation for the new clan?\n"); + return; + } + + if (ansi_strlen(abbreviation) > Clan::MAX_CLAN_ABBR_LEN) { + char_printf(ch, "Clan abbreviations can be at most {} visible characters long.\n", Clan::MAX_CLAN_ABBR_LEN); + return; + } + + if (clan_repository.find_by_abbreviation(abbreviation)) { + char_printf(ch, "A clan with a similar abbreviation already exists.\n"); + return; + } + + // Generate a new ID - this is simplistic, should be improved + ClanID new_id = clan_repository.count() + 1; + + auto clan = clan_repository.create(new_id, std::string{abbreviation}, std::string{abbreviation}); + if (!clan) { + char_printf(ch, "Error creating clan.\n"); + return; + } + + // Add default ranks + // Leader rank with full permissions + PermissionSet leader_permissions; + leader_permissions.set(); // Set all bits for full admin access + if (auto result = clan->admin_add_rank(ClanRank("Leader", leader_permissions)); !result) { + char_printf(ch, "Failed to create leader rank: {}\n", result.error()); + clan_repository.remove(clan->id()); + return; + } + + // Member rank with basic permissions + PermissionSet member_permissions; + set_permission(member_permissions, ClanPermission::MANAGE_ALTS); + set_permission(member_permissions, ClanPermission::CLAN_CHAT); + if (auto result = clan->admin_add_rank(ClanRank("Member", member_permissions)); !result) { + char_printf(ch, "Failed to create member rank: {}\n", result.error()); + clan_repository.remove(clan->id()); + return; + } + + // Add the creator as the leader (rank 0) + std::string creator_name = GET_NAME(ch); + if (clan->add_member_by_name(creator_name, 0)) { // 0-based index, so rank 0 is the leader + ch->player_specials->clan_id = clan->id(); + save_player(ch); + } else { + char_printf(ch, "Warning: Failed to add you as clan leader.\n"); + } + + char_printf(ch, "New clan {} created.\n", abbreviation); + log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} creates new clan: {}", GET_NAME(ch), abbreviation); + + clan_repository.save(); +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_create, "clan_create", CommandCategory::CLAN, permissions::GOD_ONLY, + "create <abbreviation> - Create a new clan (gods only)"); + +CLAN_COMMAND(clan_destroy, clan_permissions::RequiresPermissions<ClanPermission::CLAN_ADMIN>) { + auto clan = find_clan_for_command(ch, argument); + if (!clan) { + return; + } + + auto clan_name = clan.value()->name(); + auto clan_id = clan.value()->id(); + + // Notify all members that the clan is disbanded + clan.value()->notify(ch, "Your clan has been disbanded!"); + + char_printf(ch, AFMAG "You have deleted the clan {}&0.\n" ANRM, clan_name); + log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} has destroyed the clan {}.", GET_NAME(ch), clan_name); + + clan_repository.remove(clan_id); + clan_repository.save(); +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_destroy, "clan_destroy", CommandCategory::CLAN, permissions::GOD_ONLY, + "destroy <clan> - Destroy a clan (gods only)"); + +CLAN_COMMAND(clan_info, clan_permissions::RequiresNoPermissions) { + auto clan_name = argument.get(); + ClanPtr clan; + + if (clan_name.empty()) { + auto clan_opt = find_clan_for_command(ch, argument); + if (!clan_opt) { + char_printf(ch, "Which clan's info do you want to view?\n"); + return; + } + clan = clan_opt.value(); + } else { + auto [found_clan, used_fuzzy] = find_clan_with_fuzzy_search(clan_name); + + if (!found_clan) { + char_printf(ch, "'{}' does not refer to a valid clan.\n", clan_name); + show_clan_suggestions(ch, clan_name); + return; + } + + if (used_fuzzy) { + char_printf(ch, "Assuming you meant '{}' ({}).\n", strip_ansi((*found_clan)->name()), + strip_ansi((*found_clan)->abbreviation())); + } + + clan = *found_clan; + } + + // Show clan information + if (!clan) { + char_printf(ch, "Error getting clan information.\n"); + return; + } + + std::string title = fmt::format("[ Clan {}: {} ]", clan->id(), clan->name()); + paging_printf(ch, "{:-^70}\n", title); + + paging_printf(ch, + "Nickname: " AFYEL "{}&0" ANRM + " " + "Ranks: " AFYEL "{}&0" ANRM + " " + "Members: " AFYEL "{}&0" ANRM + "\n" + "App Fee: " AFCYN "{}&0" ANRM + " " + "App Level: " AFYEL "{}&0" ANRM + " " + "Dues: " AFCYN "{}&0" ANRM "\n", + clan->abbreviation(), clan->ranks().size(), clan->member_count(), clan->app_fee(), + clan->min_application_level(), clan->dues()); + + // Show clan description + if (!clan->description().empty()) { + paging_printf(ch, "\nDescription:\n{}", clan->description()); + } else { + paging_printf(ch, "\nNo description set for this clan.\n"); + } + + // Show additional information for members + bool show_all = GET_LEVEL(ch) >= LVL_IMMORT || (get_clan_id(ch) == clan->id()); + + if (show_all) { + paging_printf(ch, "Treasure: {}\n", statemoney(clan->treasure())); + + paging_printf(ch, "Number of items in Storage: {}\n", clan->storage().size()); + + paging_printf(ch, "\nRanks:\n"); + for (size_t i = 0; i < clan->ranks().size(); ++i) { + paging_printf(ch, "{:3} {}\n", i + 1, clan->ranks()[i].title()); + } + + if (show_all && !clan->motd().empty()) { + paging_printf(ch, "\nMessage of the Day:\n{}\n\n", clan->motd()); + } + } + + if (GET_LEVEL(ch) >= LVL_IMMORT) { + paging_printf(ch, "Bank Room: {}\n", + clan->bank_room() == NOWHERE ? "Disabled" : std::to_string(clan->bank_room())); + paging_printf(ch, "Chest Room: {}\n", + clan->chest_room() == NOWHERE ? "Disabled" : std::to_string(clan->chest_room())); + paging_printf(ch, "Hall Room: {}\n", + clan->hall_room() == NOWHERE ? "Disabled" : std::to_string(clan->hall_room())); + } + + // Show user's rank and permissions if they're a member of this clan + if (get_clan_id(ch) == clan->id()) { + auto member = get_clan_member(ch); + auto rank = get_clan_rank(ch); + + if (member && rank) { + paging_printf(ch, "\n{:-^70}\n", "[ Your Clan Status ]"); + paging_printf(ch, "Your Rank: {} ({})\n", member->rank_index + 1, rank->title()); + + // Show permissions + paging_printf(ch, "Your Permissions:\n"); + bool has_any_permission = false; + + // List all permissions the user has + for (int i = 0; i < static_cast<int>(ClanPermission::MAX_PERMISSIONS); ++i) { + ClanPermission perm = static_cast<ClanPermission>(i); + if (rank->has_permission(perm)) { + if (!has_any_permission) { + has_any_permission = true; + } + + // Convert permission enum to readable name - only show used/settable permissions + std::string perm_name; + switch (perm) { + case ClanPermission::CLAN_CHAT: + perm_name = "Clan Chat"; + break; + case ClanPermission::INVITE_MEMBERS: + perm_name = "Invite Members"; + break; + case ClanPermission::KICK_MEMBERS: + perm_name = "Kick Members"; + break; + case ClanPermission::PROMOTE_MEMBERS: + perm_name = "Promote Members"; + break; + case ClanPermission::DEMOTE_MEMBERS: + perm_name = "Demote Members"; + break; + case ClanPermission::MANAGE_ALTS: + perm_name = "Manage Alts"; + break; + case ClanPermission::MANAGE_RANKS: + perm_name = "Manage Ranks"; + break; + case ClanPermission::SET_MOTD: + perm_name = "Set MOTD"; + break; + case ClanPermission::SET_DESCRIPTION: + perm_name = "Set Description"; + break; + case ClanPermission::SET_DUES: + perm_name = "Set Dues"; + break; + case ClanPermission::SET_APP_FEES: + perm_name = "Set App Fees"; + break; + case ClanPermission::SET_APP_LEVEL: + perm_name = "Set App Level"; + break; + case ClanPermission::DEPOSIT_FUNDS: + perm_name = "Deposit Funds"; + break; + case ClanPermission::WITHDRAW_FUNDS: + perm_name = "Withdraw Funds"; + break; + case ClanPermission::STORE_ITEMS: + perm_name = "Store Items"; + break; + case ClanPermission::RETRIEVE_ITEMS: + perm_name = "Retrieve Items"; + break; + case ClanPermission::LEADER_OVERRIDE: + perm_name = "Leader Override"; + break; + case ClanPermission::CLAN_ADMIN: + perm_name = "Clan Admin"; + break; + default: + continue; // Skip unused permissions + } + paging_printf(ch, " - {}\n", perm_name); + } + } + + if (!has_any_permission) { + paging_printf(ch, " - None\n"); + } + } + } + + start_paging(ch); +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_info, "clan_info", CommandCategory::CLAN, permissions::PUBLIC, + "info [clan] - Display information about a clan"); + +// SECTION: Main command handlers and entry points + +ACMD(do_clan) { + if (IS_NPC(ch) || !ch->desc) { + char_printf(ch, HUH); + return; + } + + Arguments args(argument); + auto command = args.shift(); + + if (command.empty()) { + display_clan_help(ch); + return; + } + + // Try to call the command using the function registry with permission checking + std::string full_command = "clan_" + std::string(command); + auto permissions = get_clan_permissions(ch); + + if (!FunctionRegistry::call_by_abbrev_with_permissions(full_command, ch, args, permissions)) { + // Check if the function exists but we lack permissions + if (FunctionRegistry::can_call_function(full_command, 0, ch)) { // Check if function exists + char_printf(ch, "You don't have permission to use that clan command.\n"); + } else { + char_printf(ch, "Unknown clan command: '{}'\n", command); + } + char_printf(ch, "Type 'clan' with no arguments to see available commands.\n"); + } +} + +ACMD(do_ctell) { + Arguments args(argument); + if (args.empty()) { + char_printf(ch, "What do you want to tell your clan?\n"); + return; + } + + clan_tell(ch, args); +} + +CLAN_COMMAND(clan_snoop, clan_permissions::RequiresPermissions<ClanPermission::CLAN_ADMIN>) { + if (!check_god_only_operation(ch, "snoop clan communications")) { + return; + } + + if (argument.empty()) { + // Show current snooped clans + auto snooped_clans = get_snooped_clans(ch); + if (snooped_clans.empty()) { + char_printf(ch, "You are not currently snooping any clan communications.\n"); + } else { + char_printf(ch, "You are currently snooping the following clans:\n"); + for (auto clan_id : snooped_clans) { + auto clan = clan_repository.find_by_id(clan_id); + if (clan) { + char_printf(ch, " {} [{}] (ID: {})\n", clan.value()->name(), clan.value()->abbreviation(), + clan_id); + } + } + } + return; + } + + auto subcommand = argument.shift(); + + if (subcommand == "start" || subcommand == "on") { + if (argument.empty()) { + char_printf(ch, "Start snooping which clan? Usage: clan snoop start <clan>\n"); + return; + } + + auto clan_name = argument.get(); + + // Find clan using fuzzy search + auto [clan, used_fuzzy] = find_clan_with_fuzzy_search(clan_name); + + if (!clan) { + char_printf(ch, "No clan found with name or abbreviation '{}'.\n", clan_name); + show_clan_suggestions(ch, clan_name); + return; + } + + if (used_fuzzy) { + char_printf(ch, "Assuming you meant '{}' ({}).\n", strip_ansi((*clan)->name()), + strip_ansi((*clan)->abbreviation())); + } + + auto clan_id = (*clan)->id(); + + if (is_snooping_clan(ch, clan_id)) { + char_printf(ch, "You are already snooping {} communications.\n", (*clan)->name()); + return; + } + + add_clan_snoop(ch, clan_id); + char_printf(ch, "You are now snooping {} [{}] communications.\n", (*clan)->name(), (*clan)->abbreviation()); + log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} starts snooping clan {} communications", GET_NAME(ch), + (*clan)->name()); + + } else if (subcommand == "stop" || subcommand == "off") { + if (argument.empty()) { + char_printf(ch, "Stop snooping which clan? Usage: clan snoop stop <clan>\n"); + return; + } + + auto clan_name = argument.get(); + + // Find clan using fuzzy search + auto [clan, used_fuzzy] = find_clan_with_fuzzy_search(clan_name); + + if (!clan) { + char_printf(ch, "No clan found with name or abbreviation '{}'.\n", clan_name); + show_clan_suggestions(ch, clan_name); + return; + } + + if (used_fuzzy) { + char_printf(ch, "Assuming you meant '{}' ({}).\n", strip_ansi((*clan)->name()), + strip_ansi((*clan)->abbreviation())); + } + + auto clan_id = (*clan)->id(); + + if (!is_snooping_clan(ch, clan_id)) { + char_printf(ch, "You are not currently snooping {} communications.\n", (*clan)->name()); + return; + } + + remove_clan_snoop(ch, clan_id); + char_printf(ch, "You stop snooping {} [{}] communications.\n", (*clan)->name(), (*clan)->abbreviation()); + log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} stops snooping clan {} communications", GET_NAME(ch), + (*clan)->name()); + + } else if (subcommand == "clear" || subcommand == "all") { + auto snooped_clans = get_snooped_clans(ch); + if (snooped_clans.empty()) { + char_printf(ch, "You are not currently snooping any clan communications.\n"); + return; + } + + remove_all_clan_snoops(ch); + char_printf(ch, "You stop snooping all clan communications.\n"); + log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} stops snooping all clan communications", GET_NAME(ch)); + + } else { + char_printf(ch, "Usage:\n"); + char_printf(ch, " clan snoop - Show current snooped clans\n"); + char_printf(ch, " clan snoop start <clan> - Start snooping clan communications\n"); + char_printf(ch, " clan snoop stop <clan> - Stop snooping clan communications\n"); + char_printf(ch, " clan snoop clear - Stop snooping all clans\n"); + } +} + +REGISTER_FUNCTION_WITH_CATEGORY(clan_snoop, "clan_snoop", CommandCategory::CLAN, permissions::GOD_ONLY, + "snoop <start|stop|clear> [clan] - Monitor clan communications (gods only)"); + +CLAN_COMMAND(clan_enroll, clan_permissions::RequiresPermissions<ClanPermission::INVITE_MEMBERS>) { + if (!check_god_only_operation(ch, "enroll clan members")) { + return; + } + + if (argument.empty()) { + char_printf(ch, "Usage: clan enroll <clan> <player> [rank]\n"); + return; + } + + auto clan_name = argument.shift(); + auto player_name = argument.shift(); + auto rank_arg = argument.shift(); + + if (clan_name.empty() || player_name.empty()) { + char_printf(ch, "Usage: clan enroll <clan> <player> [rank]\n"); + return; + } + + // Find the target player (must be online) + auto target = clan_security::find_player_safe(player_name); + if (!target || !target->desc || !IS_PLAYING(target->desc)) { + char_printf(ch, "Player '{}' is not currently online.\n", player_name); + return; + } + + // Get the correct case of the player's name + std::string correct_player_name = GET_NAME(target); + + // Find the clan + auto [clan, used_fuzzy] = find_clan_with_fuzzy_search(clan_name); + if (!clan) { + char_printf(ch, "No such clan exists.\n"); + show_clan_suggestions(ch, clan_name); + return; + } + + if (used_fuzzy) { + char_printf(ch, "Assuming you meant '{}' ({}).\n", strip_ansi((*clan)->name()), + strip_ansi((*clan)->abbreviation())); + } + + // Default to highest rank index (lowest privilege level) if not specified + int rank_index = static_cast<int>((*clan)->ranks().size()) - 1; + if (!rank_arg.empty()) { + // Create a temporary Arguments object to use try_shift_number + Arguments rank_args(rank_arg); + auto rank_opt = rank_args.try_shift_number(); + if (!rank_opt || *rank_opt < 1) { + char_printf(ch, "Invalid rank number. Use a positive integer.\n"); + return; + } + rank_index = *rank_opt - 1; // Convert to 0-based index + } + + // Validate rank index + if (rank_index < 0 || rank_index >= static_cast<int>((*clan)->ranks().size())) { + char_printf(ch, "Invalid rank number. Clan has {} ranks.\n", (*clan)->ranks().size()); + return; + } + + auto rank_title = (*clan)->ranks()[rank_index].title(); + + // Check if player is already a member and update their rank + if ((*clan)->get_member_by_name(correct_player_name).has_value()) { + if ((*clan)->update_member_rank(correct_player_name, rank_index)) { + char_printf(ch, "{} is already a member of {}. Updated their rank to '{}'.\n", correct_player_name, + (*clan)->name(), rank_title); + log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} updates {}'s rank in {} to {}", GET_NAME(ch), + correct_player_name, (*clan)->name(), rank_title); + + // Save clan ID to player file for efficient loading + target->player_specials->clan_id = (*clan)->id(); + + // Player clan_id has already been set above, no additional runtime data needed + + char_printf(target, AFGRN "Your rank in {}&0 has been updated to '{}&0'!\n" ANRM, (*clan)->name(), + rank_title); + + // Save changes + clan_repository.save(); + save_player(target); + } else { + char_printf(ch, "Failed to update {}'s rank in {}.\n", correct_player_name, (*clan)->name()); + } + return; + } + + // Add the member to persistent storage + if ((*clan)->add_member_by_name(correct_player_name, rank_index)) { + char_printf(ch, "{} has been enrolled in {} with rank '{}'.\n", correct_player_name, (*clan)->name(), + rank_title); + log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} enrolls {} in {} as {}", GET_NAME(ch), correct_player_name, + (*clan)->name(), rank_title); + + // Save clan ID to player file for efficient loading + target->player_specials->clan_id = (*clan)->id(); + + // Player clan_id has already been set above, no additional runtime data needed + + char_printf(target, AFGRN "You have been enrolled in {}&0 with rank '{}&0'!\n" ANRM, (*clan)->name(), + rank_title); + + // Save changes + clan_repository.save(); + save_player(target); + } else { + char_printf(ch, "Failed to enroll {} in {}.\n", correct_player_name, (*clan)->name()); + } +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_enroll, "clan_enroll", CommandCategory::CLAN, permissions::GOD_ONLY, + "enroll <clan> <player> [rank] - Enroll a player in a clan (gods only)"); + +CLAN_COMMAND(clan_expel, clan_permissions::RequiresPermissions<ClanPermission::KICK_MEMBERS>) { + if (!check_god_only_operation(ch, "expel clan members")) { + return; + } + + if (argument.empty()) { + char_printf(ch, "Usage: clan expel <clan> <player>\n"); + return; + } + + auto clan_name = argument.shift(); + auto player_name = argument.shift(); + + if (clan_name.empty() || player_name.empty()) { + char_printf(ch, "Usage: clan expel <clan> <player>\n"); + return; + } + + // Try to find the target player if they're online + auto target = clan_security::find_player_safe(player_name); + std::string correct_player_name; + + if (target && target->desc && IS_PLAYING(target->desc)) { + // Player is online, get their exact name + correct_player_name = GET_NAME(target); + } else { + // Player is offline, use the provided name (case-sensitive) + correct_player_name = std::string(player_name); + target = nullptr; // Clear target since they're offline + char_printf(ch, "Note: Player '{}' is offline. Using exact name as provided.\n", correct_player_name); + } + + // Find the clan + auto [clan, used_fuzzy] = find_clan_with_fuzzy_search(clan_name); + if (!clan) { + char_printf(ch, "No such clan exists.\n"); + show_clan_suggestions(ch, clan_name); + return; + } + + if (used_fuzzy) { + char_printf(ch, "Assuming you meant '{}' ({}).\n", strip_ansi((*clan)->name()), + strip_ansi((*clan)->abbreviation())); + } + + // Check if player is a member + if (!(*clan)->get_member_by_name(correct_player_name).has_value()) { + char_printf(ch, "{} is not a member of {}.\n", correct_player_name, (*clan)->name()); + return; + } + + // Remove the member + if ((*clan)->remove_member_by_name(correct_player_name)) { + char_printf(ch, "{} has been expelled from {}.\n", correct_player_name, (*clan)->name()); + log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} expels {} from {}", GET_NAME(ch), correct_player_name, + (*clan)->name()); + + // If player is online, update their runtime data + if (target) { + // Clear clan ID from player file + target->player_specials->clan_id = CLAN_ID_NONE; + + // Player clan_id has already been cleared above, no additional runtime data needed + char_printf(target, AFYEL "You have been expelled from {}&0!\n" ANRM, (*clan)->name()); + } + + // Save changes + clan_repository.save(); + } else { + char_printf(ch, "Failed to expel {} from {}.\n", correct_player_name, (*clan)->name()); + } +} +REGISTER_FUNCTION_WITH_CATEGORY( + clan_expel, "clan_expel", CommandCategory::CLAN, permissions::GOD_ONLY, + "expel <clan> <player> - Expel a player from a clan (online or offline, gods only)"); + +// SECTION: Member information commands (require basic member permissions) + +CLAN_COMMAND(clan_members, clan_permissions::RequiresClanMembership) { + auto clan = find_clan_for_command(ch, argument); + if (!clan) { + return; + } + + const auto &members = clan.value()->members(); + const auto &ranks = clan.value()->ranks(); + + if (members.empty()) { + char_printf(ch, "{} has no members.\n", clan.value()->name()); + return; + } + + std::string title = fmt::format("[ Members of {} ({}) ]", clan.value()->name(), clan.value()->abbreviation()); + paging_printf(ch, "{:-^70}\n", title); + + paging_printf(ch, "{:<20} {:<15} {:<20} {:<10}\n", "Name", "Rank", "Title", "Joined"); + paging_printf(ch, "{:-^70}\n", ""); + + // Sort members by rank (highest rank first) + auto sorted_members = members; + std::ranges::sort(sorted_members, [](const auto &a, const auto &b) { + return a.rank_index > b.rank_index; // Higher rank index = higher rank + }); + + for (const auto &member : sorted_members) { + std::string rank_title = "Unknown"; + if (member.rank_index >= 0 && member.rank_index < static_cast<int>(ranks.size())) { + rank_title = std::string(ranks[member.rank_index].title()); + } + + // Format join time + char join_date[32]; + struct tm *time_info = localtime(&member.join_time); + strftime(join_date, sizeof(join_date), "%m/%d/%Y", time_info); + + paging_printf(ch, "{:<20} {:<15} {:<20} {:<10}\n", member.name, + member.rank_index + 1, // Display as 1-based + rank_title, join_date); + } + + paging_printf(ch, "{:-^70}\n", ""); + paging_printf(ch, "Total members: {}\n", members.size()); + + start_paging(ch); +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_members, "clan_members", CommandCategory::CLAN, permissions::CLAN_MEMBER, + "members [clan] - Show clan member list"); + +CLAN_COMMAND(clan_chest, clan_permissions::RequiresStorageAccess) { + auto clan = find_clan_for_command(ch, argument); + if (!clan) { + return; + } + + // Check chest room access + if (!validate_clan_room_access(ch, clan.value()->chest_room(), "chest")) { + return; + } + + const auto &storage = clan.value()->storage(); + + if (storage.empty()) { + char_printf(ch, "The {} clan chest is empty.\n", clan.value()->name()); + return; + } + + start_paging(ch); + std::string title = fmt::format("[ {} ({}) Clan Chest ]", clan.value()->name(), clan.value()->abbreviation()); + paging_printf(ch, "{:-^70}\n", title); + + bool show_vnums = GET_LEVEL(ch) >= LVL_IMMORT; + + if (show_vnums) { + paging_printf(ch, "{:<6} {:<8} {:<45} {:<8}\n", "VNUM", "Qty", "Item", "Type"); + } else { + paging_printf(ch, "{:<8} {:<53} {:<8}\n", "Qty", "Item", "Type"); + } + paging_printf(ch, "{:-^70}\n", ""); + + int total_items = 0; + + // Sort storage by vnum for consistent display + std::vector<std::pair<ObjectId, int>> sorted_storage(storage.begin(), storage.end()); + std::ranges::sort(sorted_storage); + + for (const auto &[vnum, quantity] : sorted_storage) { + // Try to load the object to get its information + auto obj = read_object(vnum, VIRTUAL); + if (obj) { + std::string item_name = obj->short_description ? obj->short_description : "Unknown Item"; + std::string item_type = "Unknown"; + + // Get item type name + if (GET_OBJ_TYPE(obj) >= 0 && GET_OBJ_TYPE(obj) < NUM_ITEM_TYPES) { + item_type = item_types[GET_OBJ_TYPE(obj)].name; + } + + if (show_vnums) { + paging_printf(ch, "{:<6} {:<8} {:<45} {:<8}\n", vnum, quantity, item_name, item_type); + } else { + paging_printf(ch, "{:<8} {:<53} {:<8}\n", quantity, item_name, item_type); + } + + extract_obj(obj); + } else { + // Object couldn't be loaded (might be deleted from game) + if (show_vnums) { + paging_printf(ch, "{:<6} {:<8} {:<45} {:<8}\n", vnum, quantity, "[MISSING OBJECT]", "N/A"); + } else { + paging_printf(ch, "{:<8} {:<53} {:<8}\n", quantity, "[MISSING OBJECT]", "N/A"); + } + } + + total_items += quantity; + } + + paging_printf(ch, "{:-^70}\n", ""); + paging_printf(ch, "Total unique items: {} Total item count: {}\n", storage.size(), total_items); + start_paging(ch); +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_chest, "clan_chest", CommandCategory::CLAN, + permissions::clan_permission(ClanPermission::VIEW_STORAGE), + "chest [clan] - Show clan chest contents"); + +CLAN_COMMAND(clan_ranks, clan_permissions::RequiresClanMembership) { + auto clan = find_clan_for_command(ch, argument); + if (!clan) { + return; + } + + const auto &ranks = clan.value()->ranks(); + + if (ranks.empty()) { + char_printf(ch, "{} has no ranks defined.\n", clan.value()->name()); + return; + } + + char_printf(ch, "Ranks for {}:\n", clan.value()->name()); + char_printf(ch, "================================================================================\n"); + + for (size_t i = 0; i < ranks.size(); ++i) { + const auto &rank = ranks[i]; + char_printf(ch, "Rank {}: {}\n", i + 1, rank.title()); + + // Show permissions + char_printf(ch, " Permissions: "); + std::vector<std::string> perms; + + // Check each permission + if (rank.has_permission(ClanPermission::SET_DESCRIPTION)) + perms.push_back("Description"); + if (rank.has_permission(ClanPermission::SET_MOTD)) + perms.push_back("MOTD"); + if (rank.has_permission(ClanPermission::LEADER_OVERRIDE)) + perms.push_back("Grant"); + if (rank.has_permission(ClanPermission::MANAGE_RANKS)) + perms.push_back("Ranks"); + if (rank.has_permission(ClanPermission::NONE)) + perms.push_back("Title"); + if (rank.has_permission(ClanPermission::INVITE_MEMBERS)) + perms.push_back("Enroll"); + if (rank.has_permission(ClanPermission::KICK_MEMBERS)) + perms.push_back("Expel"); + if (rank.has_permission(ClanPermission::PROMOTE_MEMBERS)) + perms.push_back("Promote"); + if (rank.has_permission(ClanPermission::DEMOTE_MEMBERS)) + perms.push_back("Demote"); + if (rank.has_permission(ClanPermission::SET_APP_FEES)) + perms.push_back("App_Fees"); + if (rank.has_permission(ClanPermission::SET_APP_LEVEL)) + perms.push_back("App_Level"); + if (rank.has_permission(ClanPermission::SET_DUES)) + perms.push_back("Dues"); + if (rank.has_permission(ClanPermission::DEPOSIT_FUNDS)) + perms.push_back("Deposit"); + if (rank.has_permission(ClanPermission::WITHDRAW_FUNDS)) + perms.push_back("Withdraw"); + if (rank.has_permission(ClanPermission::STORE_ITEMS)) + perms.push_back("Store"); + if (rank.has_permission(ClanPermission::RETRIEVE_ITEMS)) + perms.push_back("Retrieve"); + if (rank.has_permission(ClanPermission::MANAGE_ALTS)) + perms.push_back("Alts"); + if (rank.has_permission(ClanPermission::CLAN_CHAT)) + perms.push_back("Chat"); + + if (perms.empty()) { + char_printf(ch, "None\n"); + } else { + // Print permissions in rows of 4 + for (size_t j = 0; j < perms.size(); ++j) { + if (j > 0 && j % 4 == 0) { + char_printf(ch, "\n "); + } + char_printf(ch, "{:12} ", perms[j]); + } + char_printf(ch, "\n"); + } + char_printf(ch, "\n"); + } +} +REGISTER_FUNCTION_WITH_CATEGORY(clan_ranks, "clan_ranks", CommandCategory::CLAN, permissions::CLAN_MEMBER, + "ranks [clan] - Show clan ranks and their permissions"); + +ACMD(do_csnoop) { + Arguments args(argument); + clan_snoop(ch, args); +} diff --git a/src/act.comm.cpp b/src/act.comm.cpp index f651acf3..a9e19fa1 100644 --- a/src/act.comm.cpp +++ b/src/act.comm.cpp @@ -475,8 +475,8 @@ ACMD(do_order) { act("$n gives $N an order.", false, ch, nullptr, vict, TO_ROOM); if (GET_STANCE(vict) < STANCE_RESTING) { - sprintf(buf, "$N is %s and can't hear you.", stance_types[GET_STANCE(vict)]); - act(buf, false, ch, nullptr, vict, TO_CHAR); + act(std::format("$N is {} and can't hear you.", stance_types[GET_STANCE(vict)]), false, ch, nullptr, + vict, TO_CHAR); } else { sprintf(buf, "$N orders you to '%s'", message); act(buf, false, vict, nullptr, ch, TO_CHAR); diff --git a/src/act.get.cpp b/src/act.get.cpp index 4c8f73d0..46b5b126 100644 --- a/src/act.get.cpp +++ b/src/act.get.cpp @@ -269,9 +269,9 @@ static void perform_get_check_money(GetContext *context, ObjData *obj) { "There were 0 coins.\n" "Must have been an illusion!"); else { - strcpy(buf, prior_value ? "There was a total of " : "There were "); - statemoney(buf + strlen(buf), context->coins); - strcat(buf, "."); + auto msg = fmt::format("{} {} coins.\n", prior_value ? "There was a total of " : "There were ", + statemoney(context->coins)); + strcpy(buf, msg.c_str()); } queue_message(context, obj, false, buf, nullptr); GET_PLATINUM(ch) += GET_OBJ_VAL(obj, VAL_MONEY_PLATINUM); @@ -525,7 +525,7 @@ ACMD(do_palm) { /* No container - palm from room */ else if (!*arg2) { if (!(obj = find_obj_in_list(world[ch->in_room].contents, find_vis_by_name(ch, arg1)))) - char_printf(ch, "You don't see {} {} here.\n", AN(arg1), arg1); + char_printf(ch, "You don't see {} {} here.\n", an(arg1), arg1); else if (!check_get_disarmed_obj(ch, obj->last_to_hold, obj) && can_take_obj(ch, obj) && get_otrigger(obj, ch, nullptr)) { int people = 0; @@ -571,14 +571,14 @@ ACMD(do_palm) { } else { cont_mode = generic_find(arg2, FIND_OBJ_EQUIP | FIND_OBJ_INV | FIND_OBJ_ROOM, ch, &tch, &cont); if (!cont) - char_printf(ch, "You can't find {} {}.\n", AN(arg2), arg2); + char_printf(ch, "You can't find {} {}.\n", an(arg2), arg2); else if (GET_OBJ_TYPE(cont) != ITEM_CONTAINER) act("$p is not a container.", false, ch, cont, nullptr, TO_CHAR); else if (IS_SET(GET_OBJ_VAL(cont, VAL_CONTAINER_BITS), CONT_CLOSED)) act("$p is closed.", false, ch, cont, nullptr, TO_CHAR); else if (!IS_PLR_CORPSE(cont) || has_corpse_consent(ch, cont)) { if (!(obj = find_obj_in_list(cont->contains, find_vis_by_name(ch, arg1)))) { - sprintf(buf, "There doesn't seem to be %s %s in $p.", AN(arg1), arg1); + sprintf(buf, "There doesn't seem to be %s %s in $p.", an(arg1), arg1); act(buf, false, ch, cont, nullptr, TO_CHAR); } else if (cont_mode == FIND_OBJ_INV || can_take_obj(ch, obj)) { if (IS_CARRYING_N(ch) >= CAN_CARRY_N(ch)) diff --git a/src/act.hpp b/src/act.hpp index 55a7603b..3df6c71d 100644 --- a/src/act.hpp +++ b/src/act.hpp @@ -87,7 +87,7 @@ std::string drunken_speech(std::string speech, int drunkenness); bool senses_living(CharData *ch, CharData *vict, int basepct); bool senses_living_only(CharData *ch, CharData *vict, int basepct); const char *relative_location_str(int bits); -void split_coins(CharData *ch, int coins[], unsigned int mode); +void split_coins(CharData *ch, Money coins, unsigned int mode); void print_obj_to_char(ObjData *obj, CharData *ch, int mode, char *additional_args); void list_obj_to_char(ObjData *list, CharData *ch, int mode); @@ -113,7 +113,7 @@ const char *hitdam_message(int value); const char *armor_message(int ac); const char *perception_message(int perception); const char *hiddenness_message(int hiddenness); -const char *ability_message(int value); +std::string_view ability_message(int value); long xp_percentage(CharData *ch); const char *exp_message(CharData *ch); const char *exp_bar(CharData *ch, int length, int gradations, int sub_gradations, bool color); diff --git a/src/act.informative.cpp b/src/act.informative.cpp index 34b15fc6..f81cb18b 100644 --- a/src/act.informative.cpp +++ b/src/act.informative.cpp @@ -12,6 +12,7 @@ #include "act.hpp" +#include "bitflags.hpp" #include "board.hpp" #include "casting.hpp" #include "charsize.hpp" @@ -767,7 +768,7 @@ static void print_char_infra_to_char(CharData *targ, CharData *ch, int mode) { RIDING(targ) == ch ? "you.@0" : "a ", RIDING(targ) == ch ? "" : SIZE_DESC(RIDING(targ)), RIDING(targ) == ch ? "" : "-sized creature"); else - char_printf(ch, "@rThe red shape of a {:c}{}&0&1 living being{} is here.&0\n", LOWER(*SIZE_DESC(targ)), + char_printf(ch, "@rThe red shape of a {:c}{}&0&1 living being{} is here.&0\n", to_lower(*SIZE_DESC(targ)), SIZE_DESC(targ) + 1, EFF_FLAGGED(targ, EFF_INFRAVISION) ? " &bwith glowing red eyes&0&1" : ""); } @@ -1095,9 +1096,9 @@ void print_room_to_char(room_num room_nr, CharData *ch, bool ignore_brief) { /* The lighted version */ if (PRF_FLAGGED(ch, PRF_ROOMFLAGS)) { - sprintflag(buf, ROOM_FLAGS(room_nr), NUM_ROOM_FLAGS, room_bits); - sprintf(buf1, "%s%s", sectors[SECT(room_nr)].color, sectors[SECT(room_nr)].name); - char_printf(ch, "@L[@0{:5}@L]@W {} @L[@0{}@0: {}@L]@0\n", world[room_nr].vnum, world[room_nr].name, buf1, buf); + char_printf(ch, "@L[@0{:5}@L]@W {} @L[@0{}{}@0: {}@L]@0\n", world[room_nr].vnum, world[room_nr].name, + sectors[SECT(room_nr)].color, sectors[SECT(room_nr)].name, + sprintflag(ROOM_FLAGS(room_nr), room_bits)); } else char_printf(ch, "{}{}{}\n", CLR(ch, FCYN), world[room_nr].name, CLR(ch, ANRM)); @@ -1235,7 +1236,7 @@ void look_in_obj(CharData *ch, char *arg) { if (!*arg) char_printf(ch, "Look in what?\n"); else if (!(bits = generic_find(arg, FIND_OBJ_INV | FIND_OBJ_ROOM | FIND_OBJ_EQUIP, ch, &dummy, &obj))) - char_printf(ch, "There doesn't seem to be {} {} here.\n", AN(arg), arg); + char_printf(ch, "There doesn't seem to be {} {} here.\n", an(arg), arg); else if (IS_VIEWABLE_GATE(obj)) look_at_target(ch, arg); else if (GET_OBJ_TYPE(obj) == ITEM_CONTAINER) { @@ -1461,7 +1462,7 @@ ACMD(do_look) { char_printf(ch, "It is too dark to see anything.\n"); } else if (is_abbrev(arg, "in")) look_in_obj(ch, argument); - else if ((look_type = searchblock(arg, dirs, false)) >= 0) + else if ((look_type = search_block(arg, dirs, false)) >= 0) look_in_direction(ch, look_type); else if (is_abbrev(arg, "at")) look_at_target(ch, argument); @@ -1513,18 +1514,15 @@ void identify_obj(ObjData *obj, CharData *ch, int location) { /* Describe any non-wield wearing positions (and not take) */ if ((i = obj->obj_flags.wear_flags & (~ITEM_WEAR_WIELD & ~ITEM_WEAR_2HWIELD & ~ITEM_WEAR_TAKE))) { - sprintbit(i, wear_bits, buf); - char_printf(ch, "Item is worn: {}\n", buf); + char_printf(ch, "Item is worn: {}\n", sprintbit(i, wear_bits)); } /* Describe extra flags (hum, !drop, class/align restrictions, etc.) */ - sprintflag(buf, GET_OBJ_FLAGS(obj), NUM_ITEM_FLAGS, extra_bits); - char_printf(ch, "Item is: {}\n", buf); + char_printf(ch, "Item is: {}\n", sprintflag(GET_OBJ_FLAGS(obj), NUM_ITEM_FLAGS, extra_bits)); /* Tell about spell effects here */ if (HAS_FLAGS(GET_OBJ_EFF_FLAGS(obj), NUM_EFF_FLAGS)) { - sprintflag(buf, GET_OBJ_EFF_FLAGS(obj), NUM_EFF_FLAGS, effect_flags); - char_printf(ch, "Item provides: {}\n", buf); + char_printf(ch, "Item provides: {}\n", sprintflag(GET_OBJ_EFF_FLAGS(obj), NUM_EFF_FLAGS, effect_flags)); } char_printf(ch, "Weight: {:.2f}, Effective Weight: {:.2f}, Value: {:d}, Level: {:d}\n", GET_OBJ_WEIGHT(obj), @@ -1730,7 +1728,7 @@ ACMD(do_weather) { strcat(buf2, wind_message(zone_table[IN_ZONE_RNUM(ch)].wind_speed, zone_table[IN_ZONE_RNUM(ch)].wind_speed)); if (GET_LEVEL(ch) >= LVL_IMMORT && zone_table[IN_ZONE_RNUM(ch)].wind_speed != WIND_NONE) /* Cut off the original newline. */ - sprintf(buf2 + strlen(buf2) - 2, " (%s)\n", dirs[zone_table[IN_ZONE_RNUM(ch)].wind_dir]); + sprintf(buf2 + strlen(buf2) - 2, " (%s)\n", dirs[zone_table[IN_ZONE_RNUM(ch)].wind_dir].data()); strcat(buf2, precipitation_message(&zone_table[IN_ZONE_RNUM(ch)], zone_table[IN_ZONE_RNUM(ch)].precipitation)); @@ -2288,7 +2286,7 @@ ACMD(do_users) { break; case 'i': ipsort = 1; - for (i = 0; i <= 300; i++) { + for (i = 0; i < 300; i++) { *iplist[i] = '\0'; *userlist[i] = '\0'; repeats[i] = 0; @@ -2351,7 +2349,7 @@ ACMD(do_users) { if (!d->connected && d->original) strcpy(state, "Switched"); else - strcpy(state, connected_types[d->connected]); + strcpy(state, connected_types[d->connected].data()); if (d->original && !d->connected) sprintf(idletime, "%3d", d->original->char_specials.timer * SECS_PER_MUD_HOUR / SECS_PER_REAL_MIN); @@ -2683,7 +2681,7 @@ ACMD(do_color) { char_printf(ch, "Your current color level is {}.\n", ctypes[COLOR_LEV(ch)]); return; } - if ((tp = searchblock(arg, ctypes, false) == -1)) { + if ((tp = search_block(arg, ctypes, false) == -1)) { char_printf(ch, "Usage: color { Off | Sparse | Normal | Complete }\n"); return; } @@ -2861,7 +2859,7 @@ const char *hiddenness_message(int hiddenness) { return "godlike"; } -const char *ability_message(int value) { +std::string_view ability_message(int value) { if (value > 90) return rolls_abils_result[0]; else if (value > 80) @@ -3343,7 +3341,6 @@ ACMD(do_score) { sprintf(buf1, " &9&b{&0%s&9&b}&0", CLASS_STARS(tch)); buf += fmt::format("Level: @Y{}@0{} Class: {}", GET_LEVEL(tch), IS_STARSTAR(tch) ? buf1 : "", CLASS_FULL(tch)); - sprinttype(GET_SEX(tch), genders, buf1); if (GET_COMPOSITION(tch) != COMP_FLESH || BASE_COMPOSITION(tch) != COMP_FLESH || GET_LIFEFORCE(tch) != LIFE_LIFE) { if (GET_COMPOSITION(tch) == BASE_COMPOSITION(tch)) *buf2 = '\0'; @@ -3356,11 +3353,12 @@ ACMD(do_score) { " Size: &3{}&0 Gender: &3{}&0\n" "Race: {} Life force: {}{}&0 " "Composition: {}{}&0{}&0{}{}&0\n", - capitalize(SIZE_DESC(tch)), buf1, RACE_ABBR(tch), LIFEFORCE_COLOR(tch), capitalize(LIFEFORCE_NAME(tch)), - COMPOSITION_COLOR(tch), capitalize(COMPOSITION_NAME(tch)), *buf2 ? "(" : "", buf2, *buf2 ? ")" : ""); + capitalize(SIZE_DESC(tch)), sprinttype(GET_SEX(tch), genders), RACE_ABBR(tch), LIFEFORCE_COLOR(tch), + capitalize(LIFEFORCE_NAME(tch)), COMPOSITION_COLOR(tch), capitalize(COMPOSITION_NAME(tch)), + *buf2 ? "(" : "", buf2, *buf2 ? ")" : ""); } else - buf += - fmt::format(" Race: {} Size: &3{}&0 Gender: &3{}&0\n", RACE_ABBR(tch), capitalize(SIZE_DESC(tch)), buf1); + buf += fmt::format(" Race: {} Size: &3{}&0 Gender: &3{}&0\n", RACE_ABBR(tch), capitalize(SIZE_DESC(tch)), + sprinttype(GET_SEX(tch), genders)); buf += fmt::format( "Age: &3&b{}&0&3 year{}&0, &3&b{}&0&3 month{}&0 " @@ -3905,7 +3903,7 @@ ACMD(do_scan) { } any_one_arg(argument, arg); - if (*arg && (only_dir = searchblock(arg, dirs, false)) == -1) { + if (*arg && (only_dir = search_block(arg, dirs, false)) == -1) { char_printf(ch, "That is not a direction.\n"); return; } diff --git a/src/act.item.cpp b/src/act.item.cpp index 589db5d3..714f2b46 100644 --- a/src/act.item.cpp +++ b/src/act.item.cpp @@ -135,7 +135,7 @@ ACMD(do_put) { else { generic_find(arg2, FIND_OBJ_EQUIP | FIND_OBJ_INV | FIND_OBJ_ROOM, ch, &tmp_char, &cont); if (!cont) - char_printf(ch, "You don't see {} {} here.\n", AN(arg2), arg2); + char_printf(ch, "You don't see {} {} here.\n", an(arg2), arg2); else if (GET_OBJ_TYPE(cont) != ITEM_CONTAINER) act("$p is not a container.", false, ch, cont, 0, TO_CHAR); else if (IS_SET(GET_OBJ_VAL(cont, VAL_CONTAINER_BITS), CONT_CLOSED)) @@ -143,7 +143,7 @@ ACMD(do_put) { else { if (obj_dotmode == FIND_INDIV) { /* put <obj> <container> */ if (!(obj = find_obj_in_list(ch->carrying, find_vis_by_name(ch, arg1)))) - char_printf(ch, "You aren't carrying {} {}.\n", AN(arg1), arg1); + char_printf(ch, "You aren't carrying {} {}.\n", an(arg1), arg1); else if (obj == cont) char_printf(ch, "You attempt to fold it into itself, but fail.\n"); else @@ -200,14 +200,14 @@ ACMD(do_stow) { if (*arg2) generic_find(arg2, FIND_OBJ_EQUIP | FIND_OBJ_INV | FIND_OBJ_ROOM, ch, &tch, &cont); if (*arg2 && !cont) - char_printf(ch, "You don't see {} {} here.\n", AN(arg2), arg2); + char_printf(ch, "You don't see {} {} here.\n", an(arg2), arg2); else if (cont && GET_OBJ_TYPE(cont) != ITEM_CONTAINER) act("$p is not a container.", false, ch, cont, 0, TO_CHAR); else if (cont && IS_SET(GET_OBJ_VAL(cont, VAL_CONTAINER_BITS), CONT_CLOSED)) char_printf(ch, "You'd better open it first!\n"); else { if (!(obj = find_obj_in_list(ch->carrying, find_vis_by_name(ch, arg1)))) - char_printf(ch, "You aren't carrying {} {}.\n", AN(arg1), arg1); + char_printf(ch, "You aren't carrying {} {}.\n", an(arg1), arg1); else if (obj == cont) char_printf(ch, "You attempt to fold it into itself, but fail.\n"); else if (!drop_otrigger(obj, ch, cont)) @@ -365,11 +365,12 @@ ACMD(do_drop) { } } - if (parse_money(&argument, coins)) { - if (!CASH_VALUE(coins)) { + if (auto coin_opt = parse_money(std::string_view{argument})) { + if (!coin_opt) { char_printf(ch, "You drop 0 coins. Okaaayy...\n"); return; } + auto coins = *coin_opt; for (type = 0; type < NUM_COIN_TYPES; ++type) if (GET_COINS(ch)[type] < coins[type]) { char_printf(ch, "You don't have enough {}!\n", COIN_NAME(type)); @@ -428,7 +429,7 @@ ACMD(do_drop) { if (!amount) char_printf(ch, "So...you don't want to drop anything?\n"); else if (!(obj = find_obj_in_list(ch->carrying, context))) - char_printf(ch, "You don't seem to have {} {}{}.\n", amount == 1 ? AN(name) : "any", arg, + char_printf(ch, "You don't seem to have {} {}{}.\n", amount == 1 ? an(name) : "any", arg, amount == 1 || isplural(name) ? "" : "s"); else { total = amount; @@ -564,7 +565,7 @@ CharData *give_find_vict(CharData *ch, char *arg) { return vict; } -void perform_give_money(CharData *ch, CharData *vict, int coins[]) { +void perform_give_money(CharData *ch, CharData *vict, Money coins) { bool afford = true; int amount = 0, i; ObjData *obj; @@ -613,16 +614,12 @@ void perform_give_money(CharData *ch, CharData *vict, int coins[]) { if (PRF_FLAGGED(ch, PRF_NOREPEAT)) char_printf(ch, OK); else { - strcpy(buf, "You give $n "); - statemoney(buf + strlen(buf), coins); - strcat(buf, "."); - act(buf, false, vict, 0, ch, TO_VICT); + auto msg = fmt::format("You give $n {} .", statemoney(coins)); + act(msg, false, vict, 0, ch, TO_VICT); } - strcpy(buf, "$n gives you "); - statemoney(buf + strlen(buf), coins); - strcat(buf, "."); - act(buf, false, ch, 0, vict, TO_VICT); + auto msg = fmt::format("$n give you {}.", statemoney(coins)); + act(msg, false, ch, 0, vict, TO_VICT); act("$n gives some coins to $N.", true, ch, 0, vict, TO_NOTVICT); @@ -649,11 +646,12 @@ ACMD(do_give) { std::unordered_map<ObjData *, int> vnums; int vnum; - if (parse_money(&argument, cash)) { + auto cash_opt = parse_money(std::string_view{argument}); + if (!cash_opt) { one_argument(argument, name); if (!(vict = give_find_vict(ch, name))) return; - perform_give_money(ch, vict, cash); + perform_give_money(ch, vict, *cash_opt); return; } @@ -896,7 +894,7 @@ ACMD(do_eat) { return; } if (!(food = find_obj_in_list(ch->carrying, find_vis_by_name(ch, arg)))) { - char_printf(ch, "You don't seem to have {} {}.\n", AN(arg), arg); + char_printf(ch, "You don't seem to have {} {}.\n", an(arg), arg); return; } if (subcmd == SCMD_TASTE && ((GET_OBJ_TYPE(food) == ITEM_DRINKCON) || (GET_OBJ_TYPE(food) == ITEM_FOUNTAIN))) { @@ -1021,7 +1019,7 @@ ACMD(do_pour) { return; } if (!(from_obj = find_obj_in_list(world[ch->in_room].contents, find_vis_by_name(ch, arg2)))) { - char_printf(ch, "There doesn't seem to be {} {} here.\n", AN(arg2), arg2); + char_printf(ch, "There doesn't seem to be {} {} here.\n", an(arg2), arg2); return; } if (GET_OBJ_TYPE(from_obj) != ITEM_FOUNTAIN) { @@ -1273,7 +1271,7 @@ ACMD(do_wear) { /* FIND_INDIV */ else { if (!(obj = find_obj_in_list(ch->carrying, find_vis_by_name(ch, arg1)))) { - char_printf(ch, "You don't seem to have {} {}.\n", AN(arg1), arg1); + char_printf(ch, "You don't seem to have {} {}.\n", an(arg1), arg1); return; } obj = confused_inventory_switch(ch, obj); @@ -1301,7 +1299,7 @@ ACMD(do_wield) { } if (!(obj = find_obj_in_list(ch->carrying, find_vis_by_name(ch, arg)))) { - char_printf(ch, "You don't seem to have {} {}.\n", AN(arg), arg); + char_printf(ch, "You don't seem to have {} {}.\n", an(arg), arg); return; } @@ -1387,7 +1385,7 @@ ACMD(do_light) { if (!(obj = (find_obj_in_eq(ch, nullptr, find_darklights))) && !(obj = find_obj_in_list(ch->carrying, find_vis_by_name(ch, arg))) && !(obj = find_obj_in_list(ch->carrying, find_darklights))) { - char_printf(ch, "You don't seem to have {} {}.\n", AN(arg), arg); + char_printf(ch, "You don't seem to have {} {}.\n", an(arg), arg); return; } @@ -1456,7 +1454,7 @@ ACMD(do_grab) { char_printf(ch, "Hold what?\n"); else if (!(obj = find_obj_in_list(ch->carrying, find_vis_by_name(ch, arg))) && !(obj = find_obj_in_list(ch->carrying, find_darklights))) - char_printf(ch, "You don't seem to have {} {}.\n", AN(arg), arg); + char_printf(ch, "You don't seem to have {} {}.\n", an(arg), arg); else { obj = confused_inventory_switch(ch, obj); if (GET_OBJ_TYPE(obj) == ITEM_LIGHT) @@ -1545,7 +1543,7 @@ ACMD(do_remove) { GET_OBJ_TYPE(obj) == ITEM_BOARD) remove_message(ch, board(GET_OBJ_VAL(obj, VAL_BOARD_NUMBER)), atoi(argument), obj); else if (!(obj = (find_obj_in_eq(ch, &where, find_vis_by_name(ch, name))))) - char_printf(ch, "You don't seem to be using {} {}.\n", AN(name), name); + char_printf(ch, "You don't seem to be using {} {}.\n", an(name), name); else perform_remove(ch, where); @@ -1888,8 +1886,8 @@ ACMD(do_create) { } half_chop(arg, buf, buf2); - while (*minor_creation_items[i] != '\n') { - if (is_abbrev(arg, minor_creation_items[i])) { + while (minor_creation_items[i].front() != '\n') { + if (is_abbrev(arg, minor_creation_items[i].data())) { found = 1; break; } else diff --git a/src/act.movement.cpp b/src/act.movement.cpp index 449ad0f1..1c3fa566 100644 --- a/src/act.movement.cpp +++ b/src/act.movement.cpp @@ -83,11 +83,9 @@ void spill_blood(CharData *ch) { int do_misdirected_move(CharData *actor, int dir) { CharData *people; - char mmsg[MAX_STRING_LENGTH]; - char rmsg[MAX_STRING_LENGTH]; - sprintf(mmsg, "$n %s %s.", movewords(actor, dir, actor->in_room, true), dirs[dir]); - sprintf(rmsg, "$n's &5illusion &0%s %s.", movewords(actor, dir, actor->in_room, true), dirs[dir]); + auto mmsg = fmt::format("$n {} {}.", movewords(actor, dir, actor->in_room, true), dirs[dir]); + auto rmsg = fmt::format("$n's &5illusion &0{} {}.", movewords(actor, dir, actor->in_room, true), dirs[dir]); LOOP_THRU_PEOPLE(people, actor) { if (actor == people) { @@ -121,21 +119,20 @@ bool try_to_sense_departure(CharData *observer, CharData *mover) { } } -void observe_char_leaving(CharData *observer, CharData *mover, CharData *mount, char *msg, int direction) { +void observe_char_leaving(CharData *observer, CharData *mover, CharData *mount, std::string_view msg, int direction) { if (observer == mover || observer == mount || !AWAKE(observer)) return; if (observer == RIDING(mover)) { - sprintf(buf, "%s you.", msg); - act(buf, false, mover, 0, observer, TO_VICT); + act(fmt::format("{} you.", msg), false, mover, 0, observer, TO_VICT); return; } if (mount) { /* MOUNTED MOVEMENT */ if (CAN_SEE(observer, mount) || CAN_SEE(observer, mover)) { - sprintf(buf, "%s %s.", msg, CAN_SEE(observer, mount) ? GET_NAME(mount) : "something"); - act(buf, false, mover, 0, observer, TO_VICT); + act(fmt::format("{} {}.", msg, CAN_SEE(observer, mount) ? GET_NAME(mount) : "something"), false, mover, 0, + observer, TO_VICT); } else if (CAN_SEE_BY_INFRA(observer, mover) || CAN_SEE_BY_INFRA(observer, mount)) { char_printf(observer, "&1&bA {}-sized creature rides {} on a {} mount.&0\n", SIZE_DESC(mover), dirs[direction], SIZE_DESC(mount)); @@ -165,16 +162,16 @@ void observe_char_leaving(CharData *observer, CharData *mover, CharData *mount, try_to_sense_departure(observer, mover); } -void observe_char_arriving(CharData *observer, CharData *mover, CharData *mount, char *mountmsg, char *stdmsg, - int direction) { +void observe_char_arriving(CharData *observer, CharData *mover, CharData *mount, std::string_view mountmsg, + std::string_view stdmsg, int direction) { if (observer == mover || observer == mount || !AWAKE(observer)) return; if (mount) { /* MOUNTED MOVEMENT */ - sprintf(buf2, "%s%s.", mountmsg, CAN_SEE(observer, mount) ? GET_NAME(mount) : "something"); if (CAN_SEE(observer, mount) || CAN_SEE(observer, mover)) { - act(buf2, false, mover, 0, observer, TO_VICT); + auto buf = fmt::format("{}{}.", mountmsg, CAN_SEE(observer, mount) ? GET_NAME(mount) : "something"); + act(buf, false, mover, 0, observer, TO_VICT); } else if (CAN_SEE_BY_INFRA(observer, mover) || CAN_SEE_BY_INFRA(observer, mount)) { char_printf(observer, "&1&bA {}-sized creature arrives from {}{}, riding a {} mount.&0\n", SIZE_DESC(mover), (direction < UP ? "the " : ""), @@ -217,8 +214,6 @@ bool do_simple_move(CharData *ch, int dir, int need_specials_check) { int flying = 0, levitating = 0; bool boat; int need_movement, vnum; - char mmsg[MAX_STRING_LENGTH]; - char tmp[MAX_STRING_LENGTH]; CharData *observer; /* Possible situations: @@ -283,15 +278,15 @@ bool do_simple_move(CharData *ch, int dir, int need_specials_check) { /* Is mount standing and conscious? */ if (mount) { if (GET_STANCE(mount) < STANCE_RESTING) { - sprintf(buf, "You aren't riding $N anywhere while $E's %s.", stance_types[GET_STANCE(mount)]); - act(buf, false, actor, 0, mount, TO_CHAR); - sprintf(buf, "$n tries to ride the %s $D.", stance_types[GET_STANCE(mount)]); - act(buf, true, actor, 0, mount, TO_ROOM); + act(fmt::format("You aren't riding $N anywhere while $E's {}.", stance_types[GET_STANCE(mount)]), false, + actor, 0, mount, TO_CHAR); + act(fmt::format("$n tries to ride the {} $D.", stance_types[GET_STANCE(mount)]), true, actor, 0, mount, + TO_ROOM); return false; } if (GET_POS(mount) < POS_STANDING) { - sprintf(buf, "You can't ride away on $N while $E's %s.", position_types[GET_POS(mount)]); - act(buf, false, actor, 0, mount, TO_CHAR); + act(fmt::format("You can't ride away on $N while $E's {}.", position_types[GET_POS(mount)]), false, actor, + 0, mount, TO_CHAR); return false; } if (GET_STANCE(mount) == STANCE_RESTING) { @@ -503,12 +498,12 @@ bool do_simple_move(CharData *ch, int dir, int need_specials_check) { alter_move(motivator, need_movement); + std::string mmsg; if (mount) { - sprintf(buf2, "You ride %s on %s.\n", dirs[dir], PERS(mount, actor)); - act(buf2, true, actor, 0, 0, TO_CHAR); - sprintf(mmsg, "$n rides %s on", dirs[dir]); + act(fmt::format("You ride {} on {}.\n", dirs[dir], PERS(mount, actor)), true, actor, 0, 0, TO_CHAR); + mmsg = fmt::format("$n rides {} on", dirs[dir]); } else { - sprintf(mmsg, "$n %s %s.", movewords(actor, dir, actor->in_room, true), dirs[dir]); + mmsg = fmt::format("$n {} {}.", movewords(actor, dir, actor->in_room, true), dirs[dir]); } if (IS_HIDDEN(motivator) || EFF_FLAGGED(motivator, EFF_SNEAK)) { @@ -553,23 +548,28 @@ bool do_simple_move(CharData *ch, int dir, int need_specials_check) { /* At last, it is time to actually move */ char_from_room(actor); char_to_room(actor, world[was_in].exits[dir]->to_room); + + std::string mounted_msg, unmounted_msg; if (mount) { char_from_room(mount); char_to_room(mount, actor->in_room); look_at_room(mount, true); - sprintf(tmp, "$n arrives from %s%s, riding ", (dir < UP ? "the " : ""), - (dir == UP ? "below" - : dir == DOWN ? "above" - : dirs[rev_dir[dir]])); + mounted_msg = fmt::format("$n arrives from {}{}, riding ", (dir < UP ? "the " : ""), + (dir == UP ? "below" + : dir == DOWN ? "above" + : dirs[rev_dir[dir]])); } else { - sprintf(buf, "$n %s from %s%s.", movewords(actor, dir, actor->in_room, false), (dir < UP ? "the " : ""), - (dir == UP ? "below" - : dir == DOWN ? "above" - : dirs[rev_dir[dir]])); + unmounted_msg = + fmt::format("$n {} from {}{}.", movewords(actor, dir, actor->in_room, false), (dir < UP ? "the " : ""), + (dir == UP ? "below" + : dir == DOWN ? "above" + : dirs[rev_dir[dir]])); } - LOOP_THRU_PEOPLE(observer, actor) { observe_char_arriving(observer, actor, mount, tmp, buf, dir); } + LOOP_THRU_PEOPLE(observer, actor) { + observe_char_arriving(observer, actor, mount, mounted_msg, unmounted_msg, dir); + } /* If the room is affected by a circle of fire, damage the person */ /* if it dies, don't do anything else */ @@ -651,8 +651,7 @@ bool perform_move(CharData *ch, int dir, int need_specials_check, bool misdirect if (IN_ROOM(k->follower) != IN_ROOM(ch)) act("&3Oops! $n&0&3 seems to have wandered off again.&0", false, k->follower, 0, ch, TO_VICT); } else { - sprintf(buf, "You follow $N %s.\n", dirs[dir]); - act(buf, false, k->follower, 0, ch, TO_CHAR); + act(fmt::format("You follow $N {}.\n", dirs[dir]), false, k->follower, 0, ch, TO_CHAR); if (perform_move(k->follower, dir, 1, false) && IS_MOB(k->follower) && PLAYERALLY(k->follower)) { if (GET_MOVE(k->follower) == 0) { act("$N staggers, gasping for breath. They don't look like they could walk another step.", @@ -745,7 +744,7 @@ ACMD(do_move) { perform_move(ch, subcmd - 1, 0, false); break; default: - if (argument && *arg && (subcmd = searchblock(arg, dirs, 0)) >= 0) + if (argument && *arg && (subcmd = search_block(arg, dirs, false)) >= 0) perform_move(ch, subcmd, 0, false); else char_printf(ch, "Which way do you want to go?\n"); @@ -790,7 +789,7 @@ int find_door(CharData *ch, const char *name, const char *dirname, const char *c return dir; } if (!quiet) { - char_printf(ch, "There doesn't seem to be {} {} here.\n", AN(name), name); + char_printf(ch, "There doesn't seem to be {} {} here.\n", an(name), name); } return -1; } @@ -1266,17 +1265,17 @@ ACMD(do_enter) { /* Display entry message. */ if (GET_OBJ_VAL(obj, VAL_PORTAL_ENTRY_MSG) >= 0) { - for (i = 0; i < GET_OBJ_VAL(obj, VAL_PORTAL_ENTRY_MSG) && *portal_entry_messages[i] != '\n'; ++i) + for (i = 0; i < GET_OBJ_VAL(obj, VAL_PORTAL_ENTRY_MSG) && portal_entry_messages[i].front() != '\n'; ++i) ; - if (*portal_entry_messages[i] != '\n') + if (portal_entry_messages[i].front() != '\n') act(portal_entry_messages[i], true, ch, obj, 0, TO_ROOM); } /* Display character message. */ if (GET_OBJ_VAL(obj, 2) >= VAL_PORTAL_CHAR_MSG) { - for (i = 0; i < GET_OBJ_VAL(obj, VAL_PORTAL_CHAR_MSG) && *portal_character_messages[i] != '\n'; ++i) + for (i = 0; i < GET_OBJ_VAL(obj, VAL_PORTAL_CHAR_MSG) && portal_character_messages[i].front() != '\n'; ++i) ; - if (*portal_character_messages[i] != '\n') + if (portal_character_messages[i].front() != '\n') act(portal_character_messages[i], true, ch, obj, 0, TO_CHAR); } @@ -1285,9 +1284,9 @@ ACMD(do_enter) { /* Display exit message. */ if (GET_OBJ_VAL(obj, VAL_PORTAL_EXIT_MSG) >= 0) { - for (i = 0; i < GET_OBJ_VAL(obj, VAL_PORTAL_EXIT_MSG) && *portal_exit_messages[i] != '\n'; ++i) + for (i = 0; i < GET_OBJ_VAL(obj, VAL_PORTAL_EXIT_MSG) && portal_exit_messages[i].front() != '\n'; ++i) ; - if (*portal_exit_messages[i] != '\n') + if (portal_exit_messages[i].front() != '\n') act(portal_exit_messages[i], true, ch, obj, 0, TO_ROOM); } @@ -1699,13 +1698,13 @@ ACMD(do_drag) { if (!perform_move(ch, dir, false, false)) { if (tch) { char_to_room(tch, from_room); - sprintf(buf, "&3Looking confused, $n&0&3 tried to drag $N&0&3 %s.&0", dirs[dir]); - act(buf, false, ch, 0, tch, TO_NOTVICT); - sprintf(buf, "&3Looking confused, $n&0&3 tried to drag you %s.&0", dirs[dir]); - act(buf, false, ch, 0, tch, TO_VICT); + act(fmt::format("&3Looking confused, $n&0&3 tried to drag $N&0&3 {}.&0", dirs[dir]), false, ch, 0, tch, + TO_NOTVICT); + act(fmt::format("&3Looking confused, $n&0&3 tried to drag you {}.&0", dirs[dir]), false, ch, 0, tch, + TO_VICT); } else if (tobj) { - sprintf(buf, "&3Looking confused, $n&0&3 tried to drag $p&0&3 %s.&0", dirs[dir]); - act(buf, false, ch, tobj, 0, TO_ROOM); + act(fmt::format("&3Looking confused, $n&0&3 tried to drag $p&0&3 {}.&0", dirs[dir]), false, ch, tobj, 0, + TO_ROOM); } return; } @@ -1734,7 +1733,7 @@ ACMD(do_drag) { act("&3Your meditation is interrupted as %N grabs you.&0", false, ch, 0, 0, TO_CHAR); if (IS_NPC(ch)) REMOVE_FLAG(MOB_FLAGS(ch), MOB_MEDITATE); - else + else REMOVE_FLAG(PLR_FLAGS(ch), PLR_MEDITATE); } diff --git a/src/act.offensive.cpp b/src/act.offensive.cpp index 5285a418..ac499f29 100644 --- a/src/act.offensive.cpp +++ b/src/act.offensive.cpp @@ -294,7 +294,7 @@ ACMD(do_roar) { if (SLEEPING(tch)) { if (random_number(0, 1)) { - sprintf(buf, "A loud %s jolts you from your slumber!\n", + snprintf(buf, sizeof(buf), "A loud %s jolts you from your slumber!\n", subcmd == SCMD_HOWL ? "OOOOAAAOAOOHHH howl" : "ROAAARRRRRR"); char_printf(tch, buf); act("$n jumps up dazedly, awakened by the noise!", true, tch, 0, 0, TO_ROOM); @@ -909,7 +909,7 @@ ACMD(do_retreat) { return; } - dir = searchblock(arg, dirs, false); + dir = search_block(arg, dirs, false); if (dir < 0 || !CH_DEST(ch, dir)) { char_printf(ch, "You can't retreat that way!\n"); @@ -931,7 +931,7 @@ ACMD(do_retreat) { !ROOM_FLAGGED(CH_NDEST(ch, dir), ROOM_DEATH) && do_simple_move(ch, dir, true)) { /* Send message back to original room. */ - sprintf(buf, "$n carefully retreats from combat, leaving %s.", dirs[dir]); + snprintf(buf, sizeof(buf), "$n carefully retreats from combat, leaving %s.", dirs[dir].data()); to_room = ch->in_room; ch->in_room = vict->in_room; act(buf, true, ch, 0, 0, TO_ROOM); @@ -986,7 +986,7 @@ ACMD(do_gretreat) { return; } - dir = searchblock(arg, dirs, false); + dir = search_block(arg, dirs, false); if (dir < 0 || !CH_DEST(ch, dir)) { char_printf(ch, "You can't retreat that way!\n"); @@ -1017,11 +1017,9 @@ ACMD(do_gretreat) { else if (GET_SKILL(ch, SKILL_GROUP_RETREAT) > random_number(0, 81) && !ROOM_FLAGGED(CH_NDEST(ch, dir), ROOM_DEATH) && CAN_GO(ch, dir) && do_simple_move(ch, dir, true)) { /* Echo line back to the original room. */ - sprintf(buf, "$n carefully retreats from combat, leading $s group %s.", dirs[dir]); - to_room = ch->in_room; ch->in_room = was_in; - act(buf, true, ch, 0, 0, TO_ROOM); + act(fmt::format("$n carefully retreats from combat, leading $s group %s.", dirs[dir]), true, ch, 0, 0, TO_ROOM); ch->in_room = to_room; char_printf(ch, "\nYou skillfully lead your group {}.\n", dirs[dir]); @@ -1029,8 +1027,7 @@ ACMD(do_gretreat) { next_k = k->next; if (k->follower->in_room == was_in && GET_STANCE(k->follower) >= STANCE_ALERT && k->can_see_master) { abort_casting(k->follower); - sprintf(buf, "You follow $N %s.", dirs[dir]); - act(buf, false, k->follower, 0, ch, TO_CHAR); + act(fmt::format("You follow $N {}.", dirs[dir]), false, k->follower, 0, ch, TO_CHAR); perform_move(k->follower, dir, 1, false); } } @@ -1779,47 +1776,47 @@ ACMD(do_throatcut) { dam = (GET_MAX_HIT(vict) * 3) / 4; expReduction = (GET_EXP(vict) * 3) / 4; /* rip same % exp from the mob... since they're doing less work! */ - sprintf(buf1, "&1&bYou nearly sever the head of $N with %s.&0", weapon->short_description); - sprintf(buf2, "&1&b$n nearly severs your head with %s!&0", weapon->short_description); - sprintf(buf3, "&1&b$n nearly severs the head of $N with %s!&0", weapon->short_description); - sprintf(stop_buf1, "Your profuse bleeding interrupts your chanting!"); - sprintf(stop_buf2, "$n stops chanting abruptly!"); + snprintf(buf1, sizeof(buf1), "&1&bYou nearly sever the head of $N with %s.&0", weapon->short_description); + snprintf(buf2, sizeof(buf2), "&1&b$n nearly severs your head with %s!&0", weapon->short_description); + snprintf(buf3, sizeof(buf3), "&1&b$n nearly severs the head of $N with %s!&0", weapon->short_description); + snprintf(stop_buf1, sizeof(stop_buf1), "Your profuse bleeding interrupts your chanting!"); + snprintf(stop_buf2, sizeof(stop_buf2), "$n stops chanting abruptly!"); break; case 2: case 5: dam = GET_MAX_HIT(vict) / 4; expReduction = GET_EXP(vict) / 4; /* rip same % exp from the mob... since they're doing less work! */ - sprintf(buf1, "&1&bBlood splatters all over you as you cut into $N with %s.&0", weapon->short_description); - sprintf(buf2, "&1&bBlood splatters all over $n as $e cuts into you with %s!&0", weapon->short_description); - sprintf(buf3, "&1&bBlood splatters all over $n as $e dices $N with %s!&0", weapon->short_description); - sprintf(stop_buf1, "Your chanting is interrupted by your coughing up blood!"); - sprintf(stop_buf2, "$n stops chanting abruptly!"); + snprintf(buf1, sizeof(buf1), "&1&bBlood splatters all over you as you cut into $N with %s.&0", weapon->short_description); + snprintf(buf2, sizeof(buf2), "&1&bBlood splatters all over $n as $e cuts into you with %s!&0", weapon->short_description); + snprintf(buf3, sizeof(buf3), "&1&bBlood splatters all over $n as $e dices $N with %s!&0", weapon->short_description); + snprintf(stop_buf1, sizeof(stop_buf1), "Your chanting is interrupted by your coughing up blood!"); + snprintf(stop_buf2, sizeof(stop_buf2), "$n stops chanting abruptly!"); break; case 3: case 4: dam = GET_MAX_HIT(vict) / 8; expReduction = GET_EXP(vict) / 8; /* rip same % exp from the mob... since they're doing less work! */ - sprintf(buf1, "&1&b$N gasps as you slice into $S throat with %s.&0", weapon->short_description); - sprintf(buf2, "&1&bYou gasp with fear as $n slices into your throat with %s!&0", weapon->short_description); - sprintf(buf3, "&1&b$N looks horrified as $n slices into $S throat with %s!&0", weapon->short_description); - sprintf(stop_buf1, "Your gasp abruptly interrupts your chanting!"); - sprintf(stop_buf2, "$n stops chanting abruptly!"); + snprintf(buf1, sizeof(buf1), "&1&b$N gasps as you slice into $S throat with %s.&0", weapon->short_description); + snprintf(buf2, sizeof(buf2), "&1&bYou gasp with fear as $n slices into your throat with %s!&0", weapon->short_description); + snprintf(buf3, sizeof(buf3), "&1&b$N looks horrified as $n slices into $S throat with %s!&0", weapon->short_description); + snprintf(stop_buf1, sizeof(stop_buf1), "Your gasp abruptly interrupts your chanting!"); + snprintf(stop_buf2, sizeof(stop_buf2), "$n stops chanting abruptly!"); break; case 7: dam = (GET_MAX_HIT(vict) * 9) / 10; expReduction = (GET_EXP(vict) * 9) / 10; /* rip same % exp from the mob... since they're doing less work! */ - sprintf(buf1, "&1&bBlood spews everywhere as you nearly incapacitate $N with %s.&0", + snprintf(buf1, sizeof(buf1), "&1&bBlood spews everywhere as you nearly incapacitate $N with %s.&0", weapon->short_description); - sprintf(buf2, "&1&bBlood spews everywhere as $n nearly incapacitates you with %s!&0", + snprintf(buf2, sizeof(buf2), "&1&bBlood spews everywhere as $n nearly incapacitates you with %s!&0", weapon->short_description); - sprintf(buf3, "&1&bBlood spews everywhere as $n nearly incapacitates $N with %s!&0", + snprintf(buf3, sizeof(buf3), "&1&bBlood spews everywhere as $n nearly incapacitates $N with %s!&0", weapon->short_description); - sprintf(stop_buf1, "Your chanting is interrupted by your gurgling of blood!"); - sprintf(stop_buf2, "$n stops chanting abruptly!"); + snprintf(stop_buf1, sizeof(stop_buf1), "Your chanting is interrupted by your gurgling of blood!"); + snprintf(stop_buf2, sizeof(stop_buf2), "$n stops chanting abruptly!"); break; default: dam = expReduction = 0; @@ -1831,9 +1828,9 @@ ACMD(do_throatcut) { expReduction = 0; /* rip same % exp from the mob... since they're doing less work! */ /* If we want silent misses for non-critical misses.. remove the act txt */ - sprintf(buf1, "&3&b$N jumps back before you have a chance to even get close!&0"); - sprintf(buf2, "&3&b$n just tried to cut your throat!&0"); - sprintf(buf3, "&3&b$n misses $N with $s throat cut!&0"); + snprintf(buf1, sizeof(buf1), "&3&b$N jumps back before you have a chance to even get close!&0"); + snprintf(buf2, sizeof(buf2), "&3&b$n just tried to cut your throat!&0"); + snprintf(buf3, sizeof(buf3), "&3&b$n misses $N with $s throat cut!&0"); } if (IS_NPC(vict)) @@ -1842,17 +1839,17 @@ ACMD(do_throatcut) { if (damage_amounts) { if (dam <= 0) - sprintf(buf, " (&1%d&0)", dam); + snprintf(buf, sizeof(buf), " (&1%d&0)", dam); else - sprintf(buf, " (&3%d&0)", dam); + snprintf(buf, sizeof(buf), " (&3%d&0)", dam); - strcat(buf1, buf); + strncat(buf1, buf, sizeof(buf1) - strlen(buf1) - 1); act(buf1, false, ch, nullptr, vict, TO_CHAR); - strcat(buf2, buf); + strncat(buf2, buf, sizeof(buf2) - strlen(buf2) - 1); act(buf2, false, ch, nullptr, vict, TO_VICT); - strcat(buf3, buf); + strncat(buf3, buf, sizeof(buf3) - strlen(buf3) - 1); act(buf3, false, ch, nullptr, vict, TO_NOTVICT); } else { act(buf1, false, ch, nullptr, vict, TO_CHAR); @@ -2731,7 +2728,7 @@ ACMD(do_lure) { return; } - dir = searchblock(argument, dirs, false); + dir = search_block(argument, dirs, false); if (FIGHTING(vict)) { char_printf(ch, "You can't lure someone away from combat!\n"); diff --git a/src/act.other.cpp b/src/act.other.cpp index 756d51db..14c61213 100644 --- a/src/act.other.cpp +++ b/src/act.other.cpp @@ -646,7 +646,7 @@ ACMD(do_shapechange) { char_to_room(mob, ch->in_room); /* Transfer hover slot items to new mob */ - if GET_EQ(ch, WEAR_HOVER) { + if GET_EQ (ch, WEAR_HOVER) { obj = GET_EQ(ch, WEAR_HOVER); unequip_char(ch, WEAR_HOVER); @@ -793,7 +793,8 @@ ACMD(do_save) { /* generic function for commands which are normally overridden by special procedures - i.e., shop commands, mail commands, etc. */ ACMD(do_not_here) { - if (CMD_IS("balance") || CMD_IS("deposit") || CMD_IS("withdraw") || CMD_IS("dump") || CMD_IS("exchange")) + if (CMD_IS("balance") || CMD_IS("deposit") || CMD_IS("withdraw") || CMD_IS("dump") || CMD_IS("exchange") || + CMD_IS("store") || CMD_IS("retrieve") || CMD_IS("items")) char_printf(ch, "Sorry, you can only do that in a bank!\n"); else if (CMD_IS("appear") || CMD_IS("disappear")) char_printf(ch, HUH); @@ -1116,8 +1117,8 @@ ACMD(do_hide) { upper_bound = skill * (3 * GET_DEX(ch) + GET_INT(ch)) / 40; if (group_size(ch, true) > 1 && GET_RACE(ch) == RACE_HALFLING) - GET_HIDDENNESS(ch) = - random_number(lower_bound, upper_bound) + (stat_bonus[GET_DEX(ch)].rogue_skills * ((GET_LEVEL(ch) / 30) + 1)); + GET_HIDDENNESS(ch) = random_number(lower_bound, upper_bound) + + (stat_bonus[GET_DEX(ch)].rogue_skills * ((GET_LEVEL(ch) / 30) + 1)); else GET_HIDDENNESS(ch) = random_number(lower_bound, upper_bound) + stat_bonus[GET_DEX(ch)].rogue_skills; @@ -1284,8 +1285,7 @@ ACMD(do_steal) { GET_GOLD(vict) -= coins[GOLD]; GET_PLATINUM(ch) += coins[PLATINUM]; GET_PLATINUM(vict) -= coins[PLATINUM]; - statemoney(buf, coins); - char_printf(ch, "Woohoo! You stole {}.\n", buf); + char_printf(ch, "Woohoo! You stole {}.\n", statemoney(coins)); } else { char_printf(ch, "You couldn't get any coins...\n"); } @@ -1352,8 +1352,6 @@ ACMD(do_title) { if (GET_PERM_TITLES(ch)) while (GET_PERM_TITLES(ch)[titles]) ++titles; - if (GET_CLAN(ch) && IS_CLAN_MEMBER(ch)) - ++titles; if (titles == 0) { char_printf(ch, "You haven't earned any permanent titles!\n"); if (ch->player.title && *ch->player.title) @@ -1368,8 +1366,6 @@ ACMD(do_title) { char_printf(ch, " {:d}) {}\n", titles + 1, GET_PERM_TITLES(ch)[titles]); ++titles; } - if (GET_CLAN(ch) && IS_CLAN_MEMBER(ch)) - char_printf(ch, " {:d}) {} {}\n", ++titles, GET_CLAN_TITLE(ch), GET_CLAN(ch)->abbreviation); char_printf(ch, "Use 'title <number>' to switch your title.\n"); } } else if (!is_positive_integer(argument)) @@ -1388,10 +1384,6 @@ ACMD(do_title) { set_title(ch, GET_PERM_TITLES(ch)[i]); break; } - if (GET_CLAN(ch) && IS_CLAN_MEMBER(ch)) { - if (++titles == which) - clan_set_title(ch); - } if (which > titles) { char_printf(ch, "You don't have that many titles!\n"); return; @@ -1810,8 +1802,7 @@ ACMD(do_group) { static void split_share(CharData *giver, CharData *receiver, int coins[]) { if (coins[PLATINUM] || coins[GOLD] || coins[SILVER] || coins[COPPER]) { - statemoney(buf, coins); - char_printf(receiver, "You {} {}.\n", giver == receiver ? "keep" : "receive", buf); + char_printf(receiver, "You {} {}.\n", giver == receiver ? "keep" : "receive", statemoney(coins)); } else char_printf(receiver, "You forego your share.\n"); GET_PLATINUM(receiver) += coins[PLATINUM]; @@ -1824,7 +1815,7 @@ static void split_share(CharData *giver, CharData *receiver, int coins[]) { GET_COPPER(giver) -= coins[COPPER]; } -void split_coins(CharData *ch, int coins[], unsigned int mode) { +void split_coins(CharData *ch, Money coins, unsigned int mode) { int i, j, count, share[NUM_COIN_TYPES], remainder_start[NUM_COIN_TYPES]; GroupType *g; CharData *master; @@ -1897,7 +1888,6 @@ void split_coins(CharData *ch, int coins[], unsigned int mode) { } ACMD(do_split) { - int coins[NUM_COIN_TYPES], i; if (IS_NPC(ch)) return; @@ -1914,17 +1904,19 @@ ACMD(do_split) { return; } - if (!parse_money(&argument, coins)) { + auto coin_opt = parse_money(std::string_view{argument}); + if (!coin_opt) { char_printf(ch, "That's not a coin type.\n"); return; } - if (!coins[PLATINUM] && !coins[GOLD] && !coins[SILVER] && !coins[COPPER]) { + auto coins = *coin_opt; + if (coins[PLATINUM] == 0 && coins[GOLD] == 0 && coins[SILVER] == 0 && coins[COPPER] == 0) { char_printf(ch, "Split zero coins? Done.\n"); return; } - for (i = 0; i < NUM_COIN_TYPES; ++i) + for (int i = 0; i < NUM_COIN_TYPES; ++i) if (coins[i] > GET_COINS(ch)[i]) { char_printf(ch, "You don't have enough {}!\n", COIN_NAME(i)); return; @@ -1948,7 +1940,7 @@ ACMD(do_use) { case SCMD_RECITE: case SCMD_QUAFF: if (!(mag_item = find_obj_in_list(ch->carrying, find_vis_by_name(ch, arg)))) { - char_printf(ch, "You don't seem to have {} {}.\n", AN(arg), arg); + char_printf(ch, "You don't seem to have {} {}.\n", an(arg), arg); return; } break; @@ -1957,7 +1949,7 @@ ACMD(do_use) { /* Item isn't in first hand, now check the second. */ mag_item = GET_EQ(ch, WEAR_HOLD2); if (!mag_item || !isname(arg, mag_item->name)) { - char_printf(ch, "You don't seem to be holding {} {}.\n", AN(arg), arg); + char_printf(ch, "You don't seem to be holding {} {}.\n", an(arg), arg); return; } break; @@ -2039,38 +2031,36 @@ ACMD(do_wimpy) { } ACMD(do_display) { - int i, x; one_argument(argument, arg); if (!*arg || !is_number(arg)) { char_printf(ch, "The following pre-set prompts are availible...\n"); - for (i = 0; default_prompts[i][0]; i++) { - char_printf(ch, "{:2d}. {:<20} {}\n", i, default_prompts[i][0], default_prompts[i][1]); + int i = 0; + for (auto prompt : default_prompts) { + char_printf(ch, "{:2d}. {:<20} {}\n", i, prompt[0], prompt[1]); + ++i; } char_printf(ch, "Usage: display <number>\n"); return; } - i = atoi(arg); + int i = atoi(arg); if (i < 0) { char_printf(ch, "The number cannot be negative.\n"); return; } - for (x = 0; default_prompts[x][0]; ++x) - ; - - if (i >= x) { - char_printf(ch, "The range for the prompt number is 0-{}.\n", x - 1); + if (i >= default_prompts.size()) { + char_printf(ch, "The range for the prompt number is 0-{}.\n", default_prompts.size() - 1); return; } if (GET_PROMPT(ch)) free(GET_PROMPT(ch)); - GET_PROMPT(ch) = strdup(default_prompts[i][1]); + GET_PROMPT(ch) = strdup(default_prompts[i][1].data()); char_printf(ch, "Set your prompt to the {} preset prompt.\n", default_prompts[i][0]); } diff --git a/src/act.social.cpp b/src/act.social.cpp index 4ed9fcac..2678317c 100644 --- a/src/act.social.cpp +++ b/src/act.social.cpp @@ -165,7 +165,10 @@ ACMD(do_insult) { char *fread_action(FILE *fl, int nr) { char buf[MAX_STRING_LENGTH], *rslt; - fgets(buf, MAX_STRING_LENGTH, fl); + if (!fgets(buf, MAX_STRING_LENGTH, fl)) { + fprintf(stderr, "Error reading action #%d from file\n", nr); + exit(1); + } if (feof(fl)) { fprintf(stderr, "fread_action - unexpected EOF near action #%d", nr); exit(1); @@ -200,7 +203,10 @@ void boot_social_messages(void) { /* now read 'em */ for (;;) { - fscanf(fl, " %s ", next_soc); + if (fscanf(fl, " %s ", next_soc) != 1) { + fprintf(stderr, "Error reading social name from file\n"); + exit(1); + } if (*next_soc == '$') break; if ((nr = find_command(next_soc)) < 0) { diff --git a/src/act.wizard.cpp b/src/act.wizard.cpp index c5d60c8e..0fe2417a 100644 --- a/src/act.wizard.cpp +++ b/src/act.wizard.cpp @@ -1110,12 +1110,12 @@ ACMD(do_date) { char *tmstr; time_t mytime; int d, h, m; - extern time_t *boot_time; + extern time_t boot_time; if (subcmd == SCMD_DATE) mytime = time(0); else - mytime = boot_time[0]; + mytime = boot_time; tmstr = (char *)asctime(localtime(&mytime)); *(tmstr + strlen(tmstr) - 1) = '\0'; @@ -1123,7 +1123,7 @@ ACMD(do_date) { if (subcmd == SCMD_DATE) char_printf(ch, "Current machine time: {}\n", tmstr); else { - mytime = time(0) - boot_time[0]; + mytime = time(0) - boot_time; d = mytime / 86400; h = (mytime / 3600) % 24; m = (mytime / 60) % 60; @@ -1891,7 +1891,7 @@ ACMD(do_set) { value = atoi(val_arg); } - strcpy(buf, OK); + strcpy(buf, OK.data()); switch (l) { case 0: SET_OR_REMOVE(PRF_FLAGS(vict), PRF_BRIEF); @@ -2100,12 +2100,8 @@ ACMD(do_set) { save = false; break; } - if (GET_CLAN(vict) && IS_CLAN_MEMBER(vict)) - GET_CLAN(vict)->power -= GET_LEVEL(vict); value = std::clamp(value, 0, LVL_IMPL); vict->player.level = (byte)value; - if (GET_CLAN(vict) && IS_CLAN_MEMBER(vict)) - GET_CLAN(vict)->power += GET_LEVEL(vict); break; case 35: if ((i = real_room(value)) < 0) { @@ -2762,8 +2758,8 @@ ACMD(do_copyto) { /* Main stuff */ - if (world[ch->in_room].description) { - world[rroom].description = strdup(world[ch->in_room].description); + if (!world[ch->in_room].description.empty()) { + world[rroom].description = world[ch->in_room].description; /* Only works if you have Oasis OLC */ olc_add_to_save_list((iroom / 100), OLC_SAVE_ROOM); @@ -2877,14 +2873,11 @@ ACMD(do_rclone) { src = &world[ch->in_room]; dest = &world[rnum]; - if (src->description) - dest->description = strdup(src->description); - if (src->description) - dest->description = strdup(src->description); - if (src->name) - dest->name = strdup(src->name); + dest->description = src->description; + dest->description = src->description; + dest->name = src->name; for (i = 0; i < FLAGVECTOR_SIZE(NUM_ROOM_FLAGS); ++i) - dest->room_flags[i] = src->room_flags[i]; + dest->flags[i] = src->flags[i]; if (src->sector_type) dest->sector_type = src->sector_type; @@ -2921,8 +2914,10 @@ ACMD(do_terminate) { return; } /* delete and purge */ - if (GET_CLAN_MEMBERSHIP(victim)) - revoke_clan_membership(GET_CLAN_MEMBERSHIP(victim)); + auto clan = get_clan_membership(victim); + if (clan && victim->player.short_descr) { + clan.value()->remove_member_by_name(victim->player.short_descr); + } SET_FLAG(PLR_FLAGS(victim), PLR_DELETED); save_player_char(victim); delete_player_obj_file(victim); @@ -2975,7 +2970,9 @@ ACMD(do_pfilemaint) { /* copy the player index to a backup file */ sprintf(file_name, "%s/%s", PLR_PREFIX, INDEX_FILE); sprintf(buf, "cp %s %s.`date +%%m%%d.%%H%%M%%S`", file_name, file_name); - system(buf); + if (system(buf) != 0) { + char_printf(ch, "Error: Failed to execute system command: {}.\n", buf); + } CREATE(new_player_table, PlayerIndexElement, top_of_p_table + 1); @@ -3054,116 +3051,6 @@ ACMD(do_pfilemaint) { char_printf(ch, "Done!\n"); } -ACMD(do_hotboot) { - FILE *fp; - bool found = false; - DescriptorData *d, *d_next; - char buf[MAX_INPUT_LENGTH]; - int i; - - extern int num_hotboots; - extern ush_int port; - extern socket_t mother_desc; - extern time_t *boot_time; - extern void ispell_done(void); - - skip_spaces(&argument); - - /* - * You must type 'hotboot yes' to actually hotboot. However, - * if anyone is connected and is not in the game or is editing - * in OLC, a warning will be shown, and no hotboot will occur. - * 'hotboot force' will override this. - */ - if (strcasecmp(argument, "force") != 0) { - if (strcasecmp(argument, "yes") != 0) { - char_printf(ch, "Are you sure you want to do a hotboot? If so, type 'hotboot yes'.\n"); - return; - } - - /* - * First scan the descriptors to see if it would be particularly - * inconvenient for anyone to have a hotboot right now. - */ - for (d = descriptor_list; d; d = d->next) { - if (d->character && STATE(d) == CON_PLAYING) - continue; /* Okay, hopefully they're not too busy. */ - - if (!found) { - char_printf(ch, "Wait! A hotboot might be inconvenient right now for:\n\n"); - found = true; - } - - char_printf(ch, " {}, who is currently busy with: {}{}{}\n", - d->character && GET_NAME(d->character) ? GET_NAME(d->character) : "An unnamed connection", - CLR(ch, FYEL), connected_types[STATE(d)], CLR(ch, ANRM)); - } - - if (found) { - char_printf(ch, "\nIf you still want to do a hotboot, type 'hotboot force'.\n"); - return; - } - } - - fp = fopen(HOTBOOT_FILE, "w"); - if (!fp) { - char_printf(ch, "Hotboot file not writeable, aborted.\n"); - return; - } - - log("(GC) Hotboot initiated by {}.", GET_NAME(ch)); - - sprintf(buf, "\n %s<<< HOTBOOT by %s - please remain seated! >>>%s\n", CLR(ch, HRED), GET_NAME(ch), CLR(ch, ANRM)); - - /* Write boot_time as first line in file */ - fprintf(fp, "%d", num_hotboots + 1); /* num of boots so far */ - for (i = 0; i <= num_hotboots; ++i) - fprintf(fp, " %ld", boot_time[i]); /* time of each boot */ - fprintf(fp, "\n"); - - /* For each playing descriptor, save its state */ - for (d = descriptor_list; d; d = d_next) { - /* We delete from the list, so need to save this. */ - d_next = d->next; - - /* Drop those logging on */ - if (!d->character || !IS_PLAYING(d)) { - write_to_descriptor(d->descriptor, "\nSorry, we are rebooting. Come back in a minute.\n"); - close_socket(d); /* throw 'em out */ - } else { - CharData *tch = d->character; - fprintf(fp, "%d %s %s\n", d->descriptor, GET_NAME(tch), d->host); - /* save tch */ - GET_QUIT_REASON(tch) = QUIT_HOTBOOT; /* Not exactly leaving, but sort of */ - save_player(tch); - write_to_descriptor(d->descriptor, buf); - } - } - - fprintf(fp, "-1\n"); - fclose(fp); - - /* Kill child processes: ispell */ - ispell_done(); - - /* Prepare arguments to call self */ - sprintf(buf, "%d", port); - sprintf(buf2, "-H%d", mother_desc); - - /* Ugh, seems it is expected we are 1 step above lib - this may be dangerous! - */ - chdir(".."); - - /* exec - descriptors are inherited! */ - execl("bin/fiery", "fiery", buf2, buf, (char *)nullptr); - - /* Failed - successful exec will not return */ - perror("do_hotboot: execl"); - write_to_descriptor(ch->desc->descriptor, "Hotboot FAILED!\n"); - - /* Too much trouble to try and recover! */ - exit(1); -} void scan_pfile_objs(CharData *ch, int vnum) { FILE *fl; diff --git a/src/act.wizinfo.cpp b/src/act.wizinfo.cpp index b7740b7c..64d6f756 100644 --- a/src/act.wizinfo.cpp +++ b/src/act.wizinfo.cpp @@ -11,6 +11,7 @@ ***************************************************************************/ #include "ai.hpp" +#include "bitflags.hpp" #include "casting.hpp" #include "charsize.hpp" #include "clan.hpp" @@ -43,6 +44,7 @@ #include "quest.hpp" #include "races.hpp" #include "rogue.hpp" +#include "rooms.hpp" #include "screen.hpp" #include "skills.hpp" #include "sorcerer.hpp" @@ -127,7 +129,7 @@ void list_zone_commands_room(CharData *ch, char *buf, room_num rvnum) { case 'E': sprintf(buf1, "%sEquip with %s [%s%d%s], %s, Max : %d\n", ZOCMD.if_flag ? " then " : "", obj_proto[ZOCMD.arg1].short_description, cyn, obj_index[ZOCMD.arg1].vnum, yel, - equipment_types[ZOCMD.arg3], ZOCMD.arg2); + equipment_types[ZOCMD.arg3].data(), ZOCMD.arg2); break; case 'P': sprintf(buf1, "%sPut %s [%s%d%s] in %s [%s%d%s], Max : %d\n", ZOCMD.if_flag ? " then " : "", @@ -139,7 +141,7 @@ void list_zone_commands_room(CharData *ch, char *buf, room_num rvnum) { obj_proto[ZOCMD.arg2].short_description, cyn, obj_index[ZOCMD.arg2].vnum, yel); break; case 'D': - sprintf(buf1, "%sSet door %s as %s.\n", ZOCMD.if_flag ? " then " : "", dirs[ZOCMD.arg2], + sprintf(buf1, "%sSet door %s as %s.\n", ZOCMD.if_flag ? " then " : "", dirs[ZOCMD.arg2].data(), ZOCMD.arg3 ? ((ZOCMD.arg3 == 1) ? "closed" : ((ZOCMD.arg3 == 2) @@ -211,15 +213,14 @@ void do_stat_room(CharData *ch, int rrnum) { resp += fmt::format("Zone: [{}], VNum: [{}{}{}], RNum: [{}], Sector: {}\n", rm->zone, CLR(ch, FGRN), rm->vnum, CLR(ch, ANRM), rrnum, buf2); - sprintflag(buf2, rm->room_flags, NUM_ROOM_FLAGS, room_bits); - resp += fmt::format("SpecProc: {}, Flags: {}\n", (rm->func == nullptr) ? "None" : "Exists", buf2); + resp += fmt::format("SpecProc: {}, Flags: {}\n", (rm->func == nullptr) ? "None" : "Exists", + sprintflag(rm->flags, room_bits)); - sprintflag(buf2, rm->room_effects, NUM_ROOM_EFF_FLAGS, room_effects); - resp += fmt::format("Room effects: {}\n", buf2); + resp += fmt::format("Room effects: {}\n", sprintflag(rm->effects, room_effects)); resp += fmt::format("Ambient Light : {}\n", rm->light); - resp += fmt::format("Description:\n{}\n", rm->description ? rm->description : " None."); + resp += fmt::format("Description:\n{}\n", !rm->description.empty() ? rm->description : " None."); stat_extra_descs(rm->ex_description, ch, buf, false); resp += buf; @@ -261,10 +262,11 @@ void do_stat_room(CharData *ch, int rrnum) { sprintf(buf1, " %sNONE%s", CLR(ch, FCYN), CLR(ch, ANRM)); else sprintf(buf1, "%s%5d%s", CLR(ch, FCYN), world[rm->exits[i]->to_room].vnum, CLR(ch, ANRM)); - sprintbit(rm->exits[i]->exit_info, exit_bits, buf2); + resp += fmt::format("Exit {}{:5}{}: To: [{}], Key: [{:5}], Keywrd: {}, Type: {}\n", CLR(ch, FCYN), dirs[i], CLR(ch, ANRM), buf1, rm->exits[i]->key, - rm->exits[i]->keyword ? rm->exits[i]->keyword : "None", buf2); + rm->exits[i]->keyword ? rm->exits[i]->keyword : "None", + sprintbit(rm->exits[i]->exit_info, exit_bits)); if (rm->exits[i]->general_description) resp += fmt::format("Extra Desc: {}\n", rm->exits[i]->general_description); } @@ -273,8 +275,8 @@ void do_stat_room(CharData *ch, int rrnum) { /* Mention spells/effects */ for (reff = room_effect_list; reff; reff = reff->next) { if (reff->room == rrnum) { - sprinttype(reff->effect, room_effects, buf2); - resp += fmt::format("SPL: ({:3}) &6{:21}&0, sets {}\n", reff->timer, skills[reff->spell].name, buf2); + resp += fmt::format("SPL: ({:3}) &6{:21}&0, sets {}\n", reff->timer, skills[reff->spell].name, + sprinttype(reff->effect, room_effects)); } } @@ -350,15 +352,14 @@ void do_stat_object(CharData *ch, ObjData *j) { if (j->action_description) resp += fmt::format("Action desc:\n{}{}{}\n", CLR(ch, FYEL), j->action_description, CLR(ch, ANRM)); - sprintbit(j->obj_flags.wear_flags, wear_bits, buf1); - resp += fmt::format("Can be worn on: {}{}{}\n", CLR(ch, FCYN), buf1, CLR(ch, ANRM)); + resp += fmt::format("Can be worn on: {}{}{}\n", CLR(ch, FCYN), sprintbit(j->obj_flags.wear_flags, wear_bits), + CLR(ch, ANRM)); - sprintflag(buf1, GET_OBJ_FLAGS(j), NUM_ITEM_FLAGS, extra_bits); - resp += fmt::format("Extra flags : {}{}{}\n", CLR(ch, FGRN), buf1, CLR(ch, ANRM)); + resp += fmt::format("Extra flags : {}{}{}\n", CLR(ch, FGRN), + sprintflag(GET_OBJ_FLAGS(j), NUM_ITEM_FLAGS, extra_bits), CLR(ch, ANRM)); - *buf1 = '\0'; - sprintflag(buf1, GET_OBJ_EFF_FLAGS(j), NUM_EFF_FLAGS, effect_flags); - resp += fmt::format("Spell Effects : {}{}{}\n", CLR(ch, FYEL), buf1, CLR(ch, ANRM)); + resp += fmt::format("Spell Effects : {}{}{}\n", CLR(ch, FYEL), + sprintflag(GET_OBJ_EFF_FLAGS(j), NUM_EFF_FLAGS, effect_flags), CLR(ch, ANRM)); resp += fmt::format("Weight: {:.2f}, Effective Weight: {:.2f}, Value: {}, Timer: {}, Decomp time: {}, Hiddenness: {}\n", @@ -414,10 +415,10 @@ void do_stat_object(CharData *ch, ObjData *j) { break; case ITEM_CONTAINER: if (!IS_CORPSE(j)) { - sprintbit(GET_OBJ_VAL(j, VAL_CONTAINER_BITS), container_bits, buf2); - resp += fmt::format("Weight capacity: {}, Lock Type: {}, Key Num: {}, Weight Reduction: {}%, Corpse: {}\n", - GET_OBJ_VAL(j, VAL_CONTAINER_CAPACITY), buf2, GET_OBJ_VAL(j, VAL_CONTAINER_KEY), - GET_OBJ_VAL(j, VAL_CONTAINER_WEIGHT_REDUCTION), YESNO(IS_CORPSE(j))); + resp += fmt::format( + "Weight capacity: {}, Lock Type: {}, Key Num: {}, Weight Reduction: {}%, Corpse: {}\n", + GET_OBJ_VAL(j, VAL_CONTAINER_CAPACITY), sprintbit(GET_OBJ_VAL(j, VAL_CONTAINER_BITS), container_bits), + GET_OBJ_VAL(j, VAL_CONTAINER_KEY), GET_OBJ_VAL(j, VAL_CONTAINER_WEIGHT_REDUCTION), YESNO(IS_CORPSE(j))); } else { resp += fmt::format("Weight capacity: {}, Id: {}, Corpse: {}, Player Corpse: {}, Raisable: {}\n", @@ -448,21 +449,21 @@ void do_stat_object(CharData *ch, ObjData *j) { case ITEM_PORTAL: resp += fmt::format("To room: {}\n", GET_OBJ_VAL(j, VAL_PORTAL_DESTINATION)); if (GET_OBJ_VAL(j, VAL_PORTAL_ENTRY_MSG) >= 0) { - for (i = 0; i < GET_OBJ_VAL(j, VAL_PORTAL_ENTRY_MSG) && *portal_entry_messages[i] != '\n'; ++i) + for (i = 0; i < GET_OBJ_VAL(j, VAL_PORTAL_ENTRY_MSG) && portal_entry_messages[i].front() != '\n'; ++i) ; - if (*portal_entry_messages[i] != '\n') + if (portal_entry_messages[i].front() != '\n') resp += fmt::format("Entry-Room message: {}", portal_entry_messages[i]); } if (GET_OBJ_VAL(j, VAL_PORTAL_CHAR_MSG) >= 0) { - for (i = 0; i < GET_OBJ_VAL(j, VAL_PORTAL_CHAR_MSG) && *portal_character_messages[i] != '\n'; ++i) + for (i = 0; i < GET_OBJ_VAL(j, VAL_PORTAL_CHAR_MSG) && portal_character_messages[i].front() != '\n'; ++i) ; - if (*portal_character_messages[i] != '\n') + if (portal_character_messages[i].front() != '\n') resp += fmt::format("To-Char message : {}", portal_character_messages[i]); } if (GET_OBJ_VAL(j, VAL_PORTAL_EXIT_MSG) >= 0) { - for (i = 0; i < GET_OBJ_VAL(j, VAL_PORTAL_EXIT_MSG) && *portal_exit_messages[i] != '\n'; ++i) + for (i = 0; i < GET_OBJ_VAL(j, VAL_PORTAL_EXIT_MSG) && portal_exit_messages[i].front() != '\n'; ++i) ; - if (*portal_exit_messages[i] != '\n') + if (portal_exit_messages[i].front() != '\n') resp += fmt::format("Exit-Room message : {}", portal_exit_messages[i]); } break; @@ -497,7 +498,6 @@ void do_stat_object(CharData *ch, ObjData *j) { resp += "Applies:"; for (i = 0; i < MAX_OBJ_APPLIES; i++) if (j->applies[i].modifier) { - sprinttype(j->applies[i].location, apply_types, buf2); resp += fmt::format("{} {}", found++ ? "," : "", format_apply(j->applies[i].location, j->applies[i].modifier)); } @@ -616,13 +616,7 @@ void do_stat_character(CharData *ch, CharData *k) { /* Various character stats */ resp += fmt::format("Race: {}, Race Align: {}, ", RACE_PLAINNAME(k), RACE_ALIGN_ABBR(k)); - /* - if (!IS_NPC(k)) - resp += fmt::format( "Deity: {}, ", GET_DIETY(ch) >= 0 ? - Dieties[(int) GET_DIETY(ch)].diety_name : "None"); - */ - sprinttype(GET_SEX(k), genders, buf1); - resp += fmt::format("Size: {}, Gender: {}\n", capitalize(SIZE_DESC(k)), buf1); + resp += fmt::format("Size: {}, Gender: {}\n", capitalize(SIZE_DESC(k)), sprinttype(GET_SEX(k), genders)); resp += fmt::format("Life force: {}{}&0, Composition: {}{}&0\n", LIFEFORCE_COLOR(k), capitalize(LIFEFORCE_NAME(k)), COMPOSITION_COLOR(k), capitalize(COMPOSITION_NAME(k))); resp += fmt::format("Class: {}, Lev: [{}{:2d}{}], XP: [{}{:7}{}], Align: [{:4d}]\n", CLASS_FULL(k), CLR(ch, FYEL), @@ -639,8 +633,8 @@ void do_stat_character(CharData *ch, CharData *k) { buf1, buf2, k->player.time.played / 3600, ((k->player.time.played / 3600) % 60), age(k).year, GET_HOMEROOM(k)); - if (GET_CLAN(k)) { - resp += fmt::format(", Clan: [{}], Rank: [{}]", GET_CLAN(k)->abbreviation, GET_CLAN_RANK(k)); + if (auto clan = get_clan_membership(k); clan.has_value()) { + resp += fmt::format(", Guild: [{}]", clan.value()->abbreviation()); } /* Display OLC zones for immorts */ @@ -689,12 +683,9 @@ void do_stat_character(CharData *ch, CharData *k) { GET_RAGE(k)); /* Status data. */ - sprinttype(GET_POS(k), position_types, buf1); - sprinttype(GET_STANCE(k), stance_types, buf2); - resp += fmt::format("Pos: {} ({})", buf1, buf2); + resp += fmt::format("Pos: {} ({})", sprinttype(GET_POS(k), position_types), buf2); if (IS_NPC(k)) { - sprinttype(k->mob_specials.default_pos, position_types, buf1); - resp += fmt::format(", Default Pos: {}", buf1); + resp += fmt::format(", Default Pos: {}", sprinttype(k->mob_specials.default_pos, position_types)); } resp += fmt::format(", Fighting: {}", FIGHTING(k) ? GET_NAME(FIGHTING(k)) : "<none>"); if (k->forward) @@ -702,40 +693,50 @@ void do_stat_character(CharData *ch, CharData *k) { fmt::format(", {} into: {}", GET_LEVEL(k) > LVL_IMMORT ? "Switched" : "Shapechanged", GET_NAME(k->forward)); resp += "\n"; - *buf2 = '\0'; - if (!IS_NPC(k)) - sprintf(buf2, "Idle: [%d tic%s]", k->char_specials.timer, k->char_specials.timer == 1 ? "" : "s"); + std::string status_info; + if (!IS_NPC(k)) { + status_info = fmt::format("Idle: [{} tic{}]", k->char_specials.timer, k->char_specials.timer == 1 ? "" : "s"); + } + if (k->desc) { - sprinttype(k->desc->connected, connected_types, buf1); - sprintf(buf2, "%s%sConnected: %s", buf2, *buf2 ? ", " : "", buf1); + if (!status_info.empty()) + status_info += ", "; + status_info += fmt::format("Connected: {}", sprinttype(k->desc->connected, connected_types)); } - if (POSSESSED(k)) - sprintf(buf2, "%s%s%s into by: %s", buf2, *buf2 ? ", " : "", - GET_LEVEL(POSSESSOR(k)) > LVL_IMMORT ? "Switched" : "Shapechanged", GET_NAME(POSSESSOR(k))); - if (*buf2) - resp += fmt::format("{}\n", buf2); + + if (POSSESSED(k)) { + if (!status_info.empty()) + status_info += ", "; + status_info += fmt::format("{} into by: {}", GET_LEVEL(POSSESSOR(k)) > LVL_IMMORT ? "Switched" : "Shapechanged", + GET_NAME(POSSESSOR(k))); + } + + if (!status_info.empty()) + resp += fmt::format("{}\n", status_info); if (IS_MOB(k)) { + std::string attack_type; if (k->mob_specials.attack_type >= 0 && k->mob_specials.attack_type <= TYPE_STAB - TYPE_HIT) - strcpy(buf2, attack_hit_text[k->mob_specials.attack_type].singular); + attack_type = attack_hit_text[k->mob_specials.attack_type].singular; else - strcpy(buf2, "<&1INVALID&0>"); + attack_type = "<&1INVALID&0>"; + resp += fmt::format("Mob Spec-Proc: {}, NPC Bare Hand Dam: {}d{}, Attack type: {}\n", (mob_index[GET_MOB_RNUM(k)].func ? "Exists" : "None"), k->mob_specials.damnodice, - k->mob_specials.damsizedice, buf2); + k->mob_specials.damsizedice, attack_type); } /* Character flags. */ if (IS_NPC(k)) { - sprintflag(buf1, MOB_FLAGS(k), NUM_MOB_FLAGS, action_bits); - resp += fmt::format("NPC flags: {}{}{}\n", CLR(ch, FCYN), buf1, CLR(ch, ANRM)); + resp += fmt::format("NPC flags: {}{}{}\n", CLR(ch, FCYN), sprintflag(MOB_FLAGS(k), NUM_MOB_FLAGS, action_bits), + CLR(ch, ANRM)); } else { - sprintflag(buf2, PLR_FLAGS(k), NUM_PLR_FLAGS, player_bits); - resp += fmt::format("PLR: {}{}{}\n", CLR(ch, FCYN), buf2, CLR(ch, ANRM)); - sprintflag(buf2, PRF_FLAGS(k), NUM_PRF_FLAGS, preference_bits); - resp += fmt::format("PRF: {}{}{}\n", CLR(ch, FGRN), buf2, CLR(ch, ANRM)); - sprintflag(buf2, PRV_FLAGS(k), NUM_PRV_FLAGS, privilege_bits); - resp += fmt::format("PRV: {}{}{}\n", CLR(ch, FGRN), buf2, CLR(ch, ANRM)); + resp += fmt::format("PLR: {}{}{}\n", CLR(ch, FCYN), sprintflag(PLR_FLAGS(k), NUM_PLR_FLAGS, player_bits), + CLR(ch, ANRM)); + resp += fmt::format("PRF: {}{}{}\n", CLR(ch, FGRN), sprintflag(PRF_FLAGS(k), NUM_PRF_FLAGS, preference_bits), + CLR(ch, ANRM)); + resp += fmt::format("PRV: {}{}{}\n", CLR(ch, FGRN), sprintflag(PRV_FLAGS(k), NUM_PRV_FLAGS, privilege_bits), + CLR(ch, ANRM)); } /* Weight and objects. */ @@ -805,8 +806,8 @@ void do_stat_character(CharData *ch, CharData *k) { k->cornered_by ? GET_NAME(k->cornered_by) : "<none>"); /* Effect bitvectors */ - sprintflag(buf1, EFF_FLAGS(k), NUM_EFF_FLAGS, effect_flags); - resp += fmt::format("EFF: {}{}{}\n", CLR(ch, FYEL), buf1, CLR(ch, ANRM)); + resp += fmt::format("EFF: {}{}{}\n", CLR(ch, FYEL), sprintflag(EFF_FLAGS(k), NUM_EFF_FLAGS, effect_flags), + CLR(ch, ANRM)); /* NPC spell circle status */ if (IS_NPC(k) && MEM_MODE(k) != MEM_NONE) { @@ -835,8 +836,8 @@ void do_stat_character(CharData *ch, CharData *k) { if (eff->modifier) resp += fmt::format("{:+d} to {}", eff->modifier, apply_types[(int)eff->location]); if (HAS_FLAGS(eff->flags, NUM_EFF_FLAGS)) { - sprintflag(buf1, eff->flags, NUM_EFF_FLAGS, effect_flags); - resp += fmt::format("{}sets {}", eff->modifier ? ", " : "", buf1); + resp += fmt::format("{}sets {}", eff->modifier ? ", " : "", + sprintflag(eff->flags, NUM_EFF_FLAGS, effect_flags)); } resp += "\n"; } @@ -1380,14 +1381,14 @@ void do_show_errors(CharData *ch, char *argument) { for (j = 0; j < NUM_OF_DIRS; j++) { if (world[rn].exits[j]) { if (world[rn].exits[j]->to_room == 0) - snprintf(buf2, sizeof(buf2), "%s%s to void, ", buf2, dirs[j]); + snprintf(buf2, sizeof(buf2), "%s%s to void, ", buf2, dirs[j].data()); if (world[rn].exits[j]->to_room == NOWHERE && !world[rn].exits[j]->general_description) - snprintf(buf2, sizeof(buf2), "%s%s to NOWHERE, ", buf2, dirs[j]); + snprintf(buf2, sizeof(buf2), "%s%s to NOWHERE, ", buf2, dirs[j].data()); } } if (buf2[0]) { buf2[strlen(buf2) - 2] = '\0'; /* cut off last comma */ - sprintf(buf, "%s [%5d] %-30s %s\n", buf, world[rn].vnum, world[rn].name, buf2); + sprintf(buf, "%s [%5d] %-30s %s\n", buf, world[rn].vnum, world[rn].name.c_str(), buf2); } } char_printf(ch, buf); @@ -1521,13 +1522,13 @@ void do_show_races(CharData *ch, char *argument) { compositions[race->def_composition].name, race->bonus_damroll, race->bonus_hitroll, race->mweight_lo, race->mweight_hi, race->fweight_lo, race->fweight_hi, race->mheight_lo, race->mheight_hi, race->fheight_lo, race->fheight_hi); - sprintflag(buf2, race->effect_flags, NUM_EFF_FLAGS, effect_flags); resp += fmt::format( "Attribute Scales : Str Dex Int Wis Con Cha\n" " : @c{:3}% {:3}% {:3}% {:3}% {:3}% {:3}%@0\n" "Perm. Effects : @y{}@0\n", race->attrib_scales[0], race->attrib_scales[1], race->attrib_scales[2], race->attrib_scales[3], - race->attrib_scales[4], race->attrib_scales[5], buf2); + race->attrib_scales[4], race->attrib_scales[5], + sprintflag(race->effect_flags, NUM_EFF_FLAGS, effect_flags)); resp += "Skills : @c"; for (i = 0; race->skills[i].skill; ++i) @@ -1829,18 +1830,16 @@ void do_show_skill(CharData *ch, char *argument) { return; } - sprintbit(skill->targets, targets, buf2); - resp += fmt::format( "Skill : @y{}@0 (@g{}@0)\n" "Type : @c{}@0\n" "Target Flags : @c{}@0\n", - skill->name, skill_num, talent_types[type], buf2); + skill->name, skill_num, talent_types[type], sprintbit(skill->targets, targets)); if (type == SKILL) resp += fmt::format("Humanoid only? : @c{}@0\n", YESNO(skill->humanoid)); else { - sprintbit(skill->routines, routines, buf1); + if (VALID_DAMTYPE(skill->damage_type)) sprintf(buf2, "%s%s", damtypes[skill->damage_type].color, damtypes[skill->damage_type].name); else @@ -1853,8 +1852,9 @@ void do_show_skill(CharData *ch, char *argument) { "Damage Type : {}@0\n" "Quest only? : @c{}@0\n" "Wear-off Message : {}@0\n", - position_types[skill->minpos], YESNO(skill->fighting_ok), YESNO(skill->violent), buf1, buf2, - YESNO(skill->quest), skill->wearoff ? skill->wearoff : "@cNone."); + position_types[skill->minpos], YESNO(skill->fighting_ok), YESNO(skill->violent), + sprintbit(skill->routines, routines), buf2, YESNO(skill->quest), + skill->wearoff ? skill->wearoff : "@cNone."); } if (type == SPELL) { @@ -2000,8 +2000,7 @@ ACMD(do_show) { void reboot_info(CharData *ch) { int h, m, s; - extern int num_hotboots; - extern time_t *boot_time; + extern time_t boot_time; h = (reboot_pulse - global_pulse) / (3600 * PASSES_PER_SEC); m = ((reboot_pulse - global_pulse) % (3600 * PASSES_PER_SEC)) / (60 * PASSES_PER_SEC); @@ -2011,15 +2010,9 @@ void reboot_info(CharData *ch) { else char_printf(ch, "Automatic rebooting is &1off&0; would reboot in {:02d}:{:02d}:{:02d}.\n", h, m, s); - if (num_hotboots > 0) { - char_printf(ch, "{:d} hotboot{} since last shutdown. Hotboot history:\n", num_hotboots, - num_hotboots == 1 ? "" : "s"); - for (s = 0; s < num_hotboots; ++s) { - strcpy(buf, ctime(&boot_time[s + 1])); - buf[strlen(buf) - 1] = '\0'; - char_printf(ch, " {}\n", buf); - } - } + strcpy(buf, ctime(&boot_time)); + buf[strlen(buf) - 1] = '\0'; + char_printf(ch, "Game started: {}\n", buf); } ACMD(do_world) { diff --git a/src/arguments.cpp b/src/arguments.cpp new file mode 100644 index 00000000..261b4014 --- /dev/null +++ b/src/arguments.cpp @@ -0,0 +1,124 @@ +#include "arguments.hpp" + +#include <optional> + +std::string_view Arguments::command_shift(bool strict) { + arg_ = trim(arg_); + + if (arg_.empty()) { + return {}; + } + + // If the first character is non-alpha, return just that character. + if (!isalpha(arg_[0])) { + std::string_view output = arg_.substr(0, 1); + arg_ = arg_.substr(1); + return output; + } + + if (strict) + return {}; + + return shift(); +} + +std::string_view Arguments::shift() { + arg_ = trim(arg_); + + if (arg_.empty()) { + return {}; + } + + // If the argument is quoted, return the quoted string. + std::string_view output; + if (arg_[0] == '"' || arg_[0] == '\'') { + size_t end = arg_.find(arg_[0], 1); + if (end == std::string::npos) { + output = arg_.substr(1); + arg_ = std::string_view{}; + } else { + output = arg_.substr(1, end - 1); + arg_ = arg_.substr(end + 1); + } + } else { + size_t end = arg_.find(' '); + if (end == std::string::npos) { + output = arg_; + arg_ = std::string_view{}; + } else { + output = arg_.substr(0, end); + arg_ = arg_.substr(end); + } + } + + return output; +} + +std::string Arguments::shift_clean() { + std::string_view arg_ = shift(); + return replace_string(arg_, "$$", "$"); +} + +std::optional<int> Arguments::try_shift_number() { + // Save the current state in case we need to restore it + std::string_view saved_arg = arg_; + + std::string_view arg_ = shift(); + if (arg_.empty()) { + // Restore state since we couldn't parse anything + this->arg_ = saved_arg; + return std::nullopt; + } + + if (!is_integer(arg_)) { + // Restore state since parsing failed + this->arg_ = saved_arg; + return std::nullopt; + } + + // Parsing succeeded, keep the shifted state + return svtoi(arg_); +} + +std::optional<std::pair<int, std::string_view>> Arguments::try_shift_number_and_arg() { + // Save the current state in case we need to restore it + std::string_view saved_arg = arg_; + + std::string_view arg_ = shift(); + if (arg_.empty()) { + // Restore state since we couldn't parse anything + this->arg_ = saved_arg; + return std::nullopt; + } + + size_t dot_pos = arg_.find('.'); + + // If there is no dot, this is not a number.item format, so restore and fail + if (dot_pos == std::string::npos) { + this->arg_ = saved_arg; + return std::nullopt; + } + + std::string_view number_str = arg_.substr(0, dot_pos); + std::string_view rest = arg_.substr(dot_pos + 1); + + // If either part is missing, this is not a valid number.item format + if (number_str.empty() || rest.empty()) { + this->arg_ = saved_arg; + return std::nullopt; + } + + // If the number is 'all', return the maximum number of items. + if (number_str == "all") { + return std::make_pair(MAX_ITEMS, rest); + } + + // If the number part is not an integer, this is not a valid format + if (!is_integer(number_str)) { + this->arg_ = saved_arg; + return std::nullopt; + } + + auto number = svtoi(number_str); + return std::make_pair(number, rest); +} \ No newline at end of file diff --git a/src/arguments.hpp b/src/arguments.hpp new file mode 100644 index 00000000..e705fb41 --- /dev/null +++ b/src/arguments.hpp @@ -0,0 +1,63 @@ +/*************************************************************************** + * File: arguments.h Part of FieryMUD * + * Usage: Allow easily parsing arguments from the user. * + * * + * All rights reserved. See license.doc for complete information. * + * * + * FieryMUD Copyright (C) 1998, 1999, 2000 by the Fiery Consortium * + ***************************************************************************/ + +#pragma once + +#include "string_utils.hpp" + +#include <limits> +#include <optional> +#include <string> + +class Arguments { + std::string static_args_; // This maintains the life of the string + std::string_view arg_; + + public: + // Constructors + // This constructor is used for strings that will be owned by the Arguments class. + explicit Arguments(std::string argument) noexcept : static_args_(std::move(argument)), arg_(static_args_) {} + // This is for strings not owned by the Arguments class. + explicit Arguments(std::string_view argument) : arg_(argument) {} + // This is for C-style strings. + explicit Arguments(const char *argument) noexcept : arg_(argument) {} + + // std::string copy constructor + Arguments(const std::string &argument) : arg_(argument) {} + + static constexpr int MAX_ITEMS = std::numeric_limits<int>::max(); + + // Get the ramaining argument list. + [[nodiscard]] std::string_view get() const { return trim(arg_); } + + [[nodiscard]] bool empty() const { return trim(arg_).empty(); } + + // When parsing commands, we want to allow things like ";hi" or ".gossip" to allow aliases for say/gossip, etc. + // This command will return the single character if it's not alpha, and shift the argument list. + // If strict is true, it will only return the single character if it's not alpha. Otherwise, it will + // return the first word or quoted string like shift(). + [[nodiscard]] std::string_view command_shift(bool strict = false); + + // Shift out one argument from the argument list. This will return the first word or quoted string + // and remove it from the argument list. + [[nodiscard]] std::string_view shift(); + + // Shift out one argument from the argument list, and replace any "$$" with "$". + // This should follow the same rules as remove_double_dollars. + [[nodiscard]] std::string shift_clean(); + + // Try to shift out a number from the argument list. If the first argument is a positive int, it will be shifted + // and returned. Otherwise, nothing will be shifted and an empty optional will be returned. + [[nodiscard]] std::optional<int> try_shift_number(); + + // Try to shift a number off the beginning of the next argument. It will return a pair of the number and the + // remaining argument list. If the number portion is not there, it will default to 1 item. + // If the number is 'all', it will return the maximum number of items. + [[nodiscard]] std::optional<std::pair<int, std::string_view>> try_shift_number_and_arg(); +}; \ No newline at end of file diff --git a/src/ban.cpp b/src/ban.cpp index 18d427da..8a990fa0 100644 --- a/src/ban.cpp +++ b/src/ban.cpp @@ -69,7 +69,7 @@ int isbanned(char *hostname) { i = 0; for (nextchar = hostname; *nextchar; nextchar++) - *nextchar = LOWER(*nextchar); + *nextchar = to_lower(*nextchar); for (banned_node = ban_list; banned_node; banned_node = banned_node->next) if (strcasestr(hostname, banned_node->site)) /* if hostname is a substring */ @@ -143,7 +143,7 @@ ACMD(do_ban) { CREATE(ban_node, BanListElement, 1); strncpy(ban_node->site, site, BANNED_SITE_LENGTH); for (nextchar = ban_node->site; *nextchar; nextchar++) - *nextchar = LOWER(*nextchar); + *nextchar = to_lower(*nextchar); ban_node->site[BANNED_SITE_LENGTH] = '\0'; strncpy(ban_node->name, GET_NAME(ch), MAX_NAME_LENGTH); ban_node->name[MAX_NAME_LENGTH] = '\0'; @@ -224,7 +224,7 @@ int Valid_Name(char *newname) { /* change to lowercase */ strcpy(tempname, newname); for (i = 0; tempname[i]; i++) - tempname[i] = LOWER(tempname[i]); + tempname[i] = to_lower(tempname[i]); /* Does the desired name contain a string in the invalid list? */ @@ -326,7 +326,7 @@ void send_to_xnames(char *name) { strcpy(tempname, name); for (i = 0; tempname[i]; i++) - tempname[i] = LOWER(tempname[i]); + tempname[i] = to_lower(tempname[i]); /* print it to the xnames file with # prepended and a \n appended */ fprintf(xnames, "#%s\n", tempname); diff --git a/src/bitflags.cpp b/src/bitflags.cpp new file mode 100644 index 00000000..4c4bae97 --- /dev/null +++ b/src/bitflags.cpp @@ -0,0 +1,107 @@ +#include "bitflags.hpp" + +#include "logging.hpp" +#include "structs.hpp" +#include "utils.hpp" + +#include <string_view> + +std::string sprintbit(long bitvector, const std::string_view names[]) { + std::string result; + + /* Assuming 8 bits to a byte... */ + for (long i = 0; names[i].data()[0] != '\n'; i++) { + if (IS_SET(bitvector, (1 << i))) { + result += names[i]; + result += ' '; + } + } + + if (result.empty()) + result = "NO BITS"; + else + result.pop_back(); /* Remove the trailing space */ + + return result; +} + +// template <std::size_t N> std::string sprinttype(int type, const std::array<std::string_view, N> &names) { +// return std::string{names[type]}; +// } + +std::string sprinttype(int type, const std::string_view names[]) { + + std::string result; + int nr = 0; + + while (type && names[nr][0] != '\n') { + type--; + nr++; + } + + if (names[nr] != "\n") + result = names[nr]; + else { + result = "UNDEFINED"; + log("SYSERR: Unknown type {} in sprinttype.", type); + } + return result; +} + +// template <std::size_t N> std::string sprintflag(flagvector flags[], const std::array<std::string_view, N> &names) { +// std::string result; + +// for (int i = 0; i < names.size(); ++i) { +// if (IS_FLAGGED(flags, i)) { +// result += names[i]; +// result += ' '; +// } +// } + +// if (result.empty()) +// result = "NO FLAGS"; +// else +// result.pop_back(); /* Remove the trailing space */ + +// return result; +// } + +std::string sprintflag(flagvector flags[], int num_flags, const std::string_view names[]) { + int nr = 0; + std::string result; + + for (int i = 0; i < num_flags; ++i) { + if (IS_FLAGGED(flags, i)) { + if (names[nr] != "\n") + result += names[nr]; + else + result += "UNDEFINED"; + result += ' '; + } + if (names[nr][0] != '\n') + ++nr; + } + + if (result.empty()) + result = "NO FLAGS"; + else + result.pop_back(); /* Remove the trailing space */ + + return result; +} + +std::string sprintascii(flagvector bits) { + int i, j = 0; + /* 32 bits, don't just add letters to try to get more unless flagvector is also as large. */ + const std::string_view flags = "abcdefghijklmnopqrstuvwxyzABCDEF"; + std::string out; + + for (i = 0; flags[i]; ++i) + if (bits & (1 << i)) + out += flags[i]; + + if (j == 0) /* Didn't write anything. */ + out += '0'; + + return out; +} \ No newline at end of file diff --git a/src/bitflags.hpp b/src/bitflags.hpp new file mode 100644 index 00000000..64c15432 --- /dev/null +++ b/src/bitflags.hpp @@ -0,0 +1,47 @@ +/*************************************************************************** + * File: bitflags.hpp Part of FieryMUD * + * Usage: for bitflags and such * + * * + * All rights reserved. See license.doc for complete information. * + * * + * FieryMUD Copyright (C) 1998, 1999, 2000 by the Fiery Consortium * + * FieryMUD is based on CircleMUD Copyright (C) 1993, 94 by the Trustees * + * of the Johns Hopkins University * + * CircleMUD is based on DikuMUD, Copyright (C) 1990, 1991. * + ***************************************************************************/ + +#pragma once + +#include "structs.hpp" +#include "utils.hpp" + +#include <string> +#include <string_view> + +[[nodiscard]] std::string sprintbit(long bitvector, const std::string_view names[]); +[[nodiscard]] std::string sprinttype(int type, const std::string_view names[]); +[[nodiscard]] std::string sprintflag(flagvector flags[], int num_flags, const std::string_view names[]); +[[nodiscard]] std::string sprintascii(flagvector bits); + +template <std::size_t N> [[nodiscard]] std::string sprinttype(int type, const std::array<std::string_view, N> &names) { + return std::string{names[type]}; +} + +template <std::size_t N> +[[nodiscard]] std::string sprintflag(flagvector flags[], const std::array<std::string_view, N> &names) { + std::string result; + + for (int i = 0; i < names.size(); ++i) { + if (IS_FLAGGED(flags, i)) { + result += names[i]; + result += ' '; + } + } + + if (result.empty()) + result = "NO FLAGS"; + else + result.pop_back(); /* Remove the trailing space */ + + return result; +} \ No newline at end of file diff --git a/src/board.cpp b/src/board.cpp index 4a5c262c..4ae12142 100644 --- a/src/board.cpp +++ b/src/board.cpp @@ -749,7 +749,7 @@ ACMD(do_boardadmin) { ellipsis(board->title, 19))); // for (j = 0; j < NUM_BPRIV; ++j) { // rule_abbr(buf, board->privileges[j]); - // char_printf(ch, " " FGRN "%c" ANRM "%3s", UPPER(*rule_name(board->privileges[j])), buf); + // char_printf(ch, " " FGRN "%c" ANRM "%3s", to_upper(*rule_name(board->privileges[j])), buf); // } char_printf(ch, "\n"); } @@ -776,7 +776,7 @@ ACMD(do_boardadmin) { board->alias, board->number, board->title, board->message_count, YESNO(board->locked)); // for (i = 0; i < NUM_BPRIV; ++i) { // rule_verbose(buf, sizeof(buf), board->privileges[i]); - // char_printf(ch, " %c%-11s : {}\n", UPPER(*privilege_data[i].alias), privilege_data[i].alias + 1, + // char_printf(ch, " %c%-11s : {}\n", to_upper(*privilege_data[i].alias), privilege_data[i].alias + 1, // buf); // } } diff --git a/src/board.hpp b/src/board.hpp index 94641b7a..0b94b561 100644 --- a/src/board.hpp +++ b/src/board.hpp @@ -37,7 +37,7 @@ #define VALID_BOARD_NUM(num) ((num) > 0) #define VALID_BOARD_INDEX(idx) ((idx) >= 0 && (idx) < num_boards) #define VALID_PRIV_NUM(num) ((num) >= 0 && (num) < NUM_BPRIV) -#define VALID_ALIAS_CHAR(c) (IS_UPPER(c) || IS_LOWER(c) || isdigit(c) || (c) == '_') +#define VALID_ALIAS_CHAR(c) (is_upper(c) || is_lower(c) || isdigit(c) || (c) == '_') /* * These structures are private to board.c diff --git a/src/clan.cpp b/src/clan.cpp index 7e03793c..5af7c655 100644 --- a/src/clan.cpp +++ b/src/clan.cpp @@ -1,6 +1,6 @@ /*************************************************************************** - * File: clan.c Part of FieryMUD * - * Usage: Front-end for the clan system * + * File: clan.cpp Part of FieryMUD * + * Usage: Implementation of clan class and operations * * * * FieryMUD Copyright (C) 1998, 1999, 2000 by the Fiery Consortium * * FieryMUD is based on HubisMUD Copyright (C) 1997, 1998. * @@ -9,1268 +9,982 @@ #include "clan.hpp" -#include "act.hpp" +#include "chars.hpp" #include "comm.hpp" #include "conf.hpp" #include "db.hpp" -#include "editor.hpp" -#include "handler.hpp" -#include "interpreter.hpp" #include "logging.hpp" -#include "math.hpp" -#include "messages.hpp" -#include "modify.hpp" -#include "players.hpp" #include "screen.hpp" -#include "string_utils.hpp" -#include "structs.hpp" -#include "sysdep.hpp" #include "utils.hpp" +#include "find.hpp" -#include <fmt/format.h> +#include <format> +#include <expected> -#define CLANCMD(name) void(name)(CharData * ch, ClanMembership * member, Clan * clan, char *argument) +// Global clan repository instance +ClanRepository clan_repository; -CLANCMD(clan_list) { - clan_iter iter; +// Clan snoop system - maps clan ID to set of character pointers +std::unordered_map<ClanID, std::unordered_set<CharData*>> clan_snoop_table; - if (clan_count() == 0) { - char_printf(ch, "No clans have formed yet.\n"); - return; - } +// Clan class implementation - /* List clans, # of members, power, and app fee */ - paging_printf(ch, AUND " Num Clan Members/Power App Fee/Lvl\n" ANRM); - for (iter = clans_start(); iter != clans_end(); ++iter) { - paging_printf(ch, fmt::format("[{:3}] {:<{}} {:3}/{:5} {:5}p/{:3}\n", (*iter)->number, (*iter)->abbreviation, - 18 + count_color_chars((*iter)->abbreviation) * 2, (*iter)->member_count, - (*iter)->power, (*iter)->app_fee, (*iter)->app_level)); - } - - start_paging(ch); -} - -CLANCMD(clan_bank) { - int coins[NUM_COIN_TYPES]; - bool deposit; - const char *verb, *preposition; - - argument = any_one_arg(argument, arg); - deposit = is_abbrev(arg, "deposit"); - verb = deposit ? "deposit" : "withdraw"; - preposition = deposit ? "into" : "from"; - - if (!parse_money(&argument, coins)) { - char_printf(ch, "How much do you want to {}?\n", verb); - return; +bool Clan::add_member(CharacterPtr character, int rank_index) { + if (!character || !character->player.short_descr) { + return false; } - - if (deposit) { - /* Gods have bottomless pockets */ - if (GET_LEVEL(ch) < LVL_GOD) { - if (GET_PLATINUM(ch) < coins[PLATINUM] || GET_GOLD(ch) < coins[GOLD] || GET_SILVER(ch) < coins[SILVER] || - GET_COPPER(ch) < coins[COPPER]) { - char_printf(ch, "You do not have that kind of money!\n"); - return; - } - - GET_PLATINUM(ch) -= coins[PLATINUM]; - GET_GOLD(ch) -= coins[GOLD]; - GET_SILVER(ch) -= coins[SILVER]; - GET_COPPER(ch) -= coins[COPPER]; - save_player_char(ch); - } - } else { - if (clan->treasure[PLATINUM] < coins[PLATINUM] || clan->treasure[GOLD] < coins[GOLD] || - clan->treasure[SILVER] < coins[SILVER] || clan->treasure[COPPER] < coins[COPPER]) { - char_printf(ch, "The clan is not wealthy enough for your needs!\n"); - return; - } - GET_PLATINUM(ch) += coins[PLATINUM]; - GET_GOLD(ch) += coins[GOLD]; - GET_SILVER(ch) += coins[SILVER]; - GET_COPPER(ch) += coins[COPPER]; - save_player_char(ch); + + if (rank_index < 0 || rank_index >= static_cast<int>(ranks_.size())) { + return false; } - - statemoney(buf, coins); - char_printf(ch, "You {} {} {}'s account: {}\n", verb, preposition, clan->abbreviation, buf); - - if (deposit) { - clan->treasure[PLATINUM] += coins[PLATINUM]; - clan->treasure[GOLD] += coins[GOLD]; - clan->treasure[SILVER] += coins[SILVER]; - clan->treasure[COPPER] += coins[COPPER]; - } else { - clan->treasure[PLATINUM] -= coins[PLATINUM]; - clan->treasure[GOLD] -= coins[GOLD]; - clan->treasure[SILVER] -= coins[SILVER]; - clan->treasure[COPPER] -= coins[COPPER]; + + // Add to persistent storage + bool success = add_member_by_name(character->player.short_descr, rank_index); + if (success) { + // Update character's clan_id + character->player_specials->clan_id = id_; + invalidate_rank_cache(); } - save_clan(clan); + + return success; } -static bool is_snooping(CharData *ch, const Clan *clan) { - ClanSnoop *snoop; - - for (snoop = GET_CLAN_SNOOP(ch); snoop; snoop = snoop->next) - if (snoop->clan == clan) - return true; - - return false; +void Clan::remove_member(const CharacterPtr &character) { + if (!character) return; + + // Clear character's clan_id + character->player_specials->clan_id = CLAN_ID_NONE; + + // Also remove from persistent member storage by name + if (character->player.short_descr) { + remove_member_by_name(character->player.short_descr); + } + + invalidate_rank_cache(); } -CLANCMD(clan_tell) { - DescriptorData *d; - CharData *tch; - CharData *me = REAL_CHAR(ch); - std::string speech{trim(argument)}; - - if (EFF_FLAGGED(ch, EFF_SILENCE)) { - char_printf(ch, "Your lips move, but no sound forms.\n"); - return; +bool Clan::is_member(const CharacterPtr &character) const { + if (!character || !character->player.short_descr) { + return false; } - - if (!speech_ok(ch, 0)) - return; - - if (speech.empty()) { - char_printf(ch, "What do you want to tell the clan?\n"); - return; - } - - speech = drunken_speech(speech, GET_COND(ch, DRUNK)); - - char_printf(ch, AFMAG "You tell {}" AFMAG ", '" AHMAG "{}" AFMAG "'\n" ANRM, - member ? "your clan" : clan->abbreviation, speech); - - for (d = descriptor_list; d; d = d->next) { - if (!IS_PLAYING(d) || !d->character) - continue; - tch = REAL_CHAR(d->character); - if (!tch || tch == me) - continue; - if (STATE(d) != CON_PLAYING || PLR_FLAGGED(tch, PLR_WRITING) || PLR_FLAGGED(tch, PLR_MAILING) || EDITING(d)) - if (!PRF_FLAGGED(tch, PRF_OLCCOMM)) - continue; - if (PRF_FLAGGED(tch, PRF_NOCLANCOMM)) - continue; - if ((IS_CLAN_SUPERADMIN(tch) && is_snooping(tch, clan)) || - (GET_CLAN(tch) == clan && !OUTRANKS(MIN_ALT_RANK, GET_CLAN_RANK(tch)))) - char_printf(FORWARD(tch), AFMAG "{} tells {}" AFMAG ", '" AHMAG "{}" AFMAG "'\n" ANRM, - GET_INVIS_LEV(me) > GET_LEVEL(tch) ? "Someone" : GET_NAME(me), - member && !IS_CLAN_SUPERADMIN(tch) ? "your clan" : clan->abbreviation, speech); + + // Check if character's clan_id matches this clan + if (character->player_specials->clan_id != id_) { + return false; } + + // Verify the character is in our member list + return get_member_by_name(character->player.short_descr).has_value(); } -CLANCMD(clan_set) { - argument = any_one_arg(argument, arg); - - if (is_abbrev(arg, "abbr")) { - char *old_abbr = clan->abbreviation; - skip_spaces(&argument); - if (ansi_strlen(argument) > MAX_CLAN_ABBR_LEN) { - char_printf(ch, "Clan abbreviationss may be at most {} characters in length.\n", MAX_CLAN_ABBR_LEN); - return; - } - clan->abbreviation = nullptr; /* so find_clan doesn't find this clan */ - if (find_clan(argument)) { - char_printf(ch, "A clan with that name or abbreviation already exists!\n"); - clan->abbreviation = old_abbr; /* revert */ - return; - } - clan->abbreviation = strdup(fmt::format("{}&0", argument).c_str()); - char_printf(ch, "{} is now abbreviated {}.\n", clan->name, clan->abbreviation); - log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} changes {}'s to {}", GET_NAME(ch), clan->name, clan->abbreviation); - free(old_abbr); - } - - else if (is_abbrev(arg, "addrank")) { - int i; +std::optional<ClanMember> Clan::get_member_by_name(const std::string_view name) const { + auto it = std::ranges::find_if(members_, [&name](const ClanMember &m) { return m.name == name; }); + return it != members_.end() ? std::optional{*it} : std::nullopt; +} - if (clan->rank_count >= MAX_CLAN_RANKS) { - char_printf(ch, "{} already has the maximum number of ranks.\n", clan->name); - return; +std::vector<ClanMember> Clan::get_members_by_rank_index(int rank_index) const { + std::vector<ClanMember> result; + for (const auto &member : members_) { + if (member.rank_index == rank_index) { + result.push_back(member); } - - ++clan->rank_count; - /* Warning! RECREATE does not initialize to zero... */ - RECREATE(clan->ranks, ClanRank, clan->rank_count); - clan->ranks[clan->rank_count - 1].title = strdup("Member"); - for (i = 0; i < NUM_CLAN_PRIVS; ++i) - if (clan_privileges[i].default_on) - SET_FLAG(clan->ranks[clan->rank_count - 1].privileges, i); - else - REMOVE_FLAG(clan->ranks[clan->rank_count - 1].privileges, i); - - char_printf(ch, "You add a new rank ({}) to {}.\n", clan->rank_count, clan->name); - clan_notification(clan, ch, "%s adds a new rank to your clan.", GET_NAME(ch)); } + return result; +} - else if (is_abbrev(arg, "appfee")) { - any_one_arg(argument, arg); - if (!is_number(arg)) { - char_printf(ch, "How much platinum should the clan's application fee be?\n"); - return; - } - clan->app_fee = atoi(arg); - char_printf(ch, "{}'s application fee is now {} platinum.\n", clan->name, clan->app_fee); +bool Clan::add_member_by_name(const std::string& name, int rank_index, time_t join_time, std::vector<std::string> alts) { + // Check if member already exists + if (get_member_by_name(name).has_value()) { + return false; } - - else if (is_abbrev(arg, "applev")) { - unsigned int level; - any_one_arg(argument, arg); - if (!is_number(arg)) { - char_printf(ch, "What should the clan's minimum application level be?\n"); - return; - } - level = atoi(arg); - if (level < 1 || level > LVL_IMPL) { - char_printf(ch, "The minimum application level must be between 1 and {}.\n", LVL_IMPL); - return; - } - clan->app_level = level; - char_printf(ch, "{}'s minimum application level is now {}.\n", clan->name, clan->app_level); + + if (join_time == 0) { + join_time = time(nullptr); } + + members_.emplace_back(name, rank_index, join_time, std::move(alts)); + invalidate_rank_cache(); + return true; +} - else if (is_abbrev(arg, "delrank")) { - if (clan->rank_count <= MIN_CLAN_RANKS) { - char_printf(ch, "{} already has the minimum number of ranks.\n", clan->name); - return; - } - - --clan->rank_count; - free(clan->ranks[clan->rank_count].title); - - char_printf(ch, "You remove a rank ({}) from {}.\n", clan->rank_count + 1, clan->name); - clan_notification(clan, ch, "%s removes a rank from your clan.", GET_NAME(ch)); - - for (member = clan->members; member; member = member->next) - if (member->rank == clan->rank_count + 1) { - --member->rank; - if (member->player) - char_printf(FORWARD(member->player), - AFMAG "You have been automatically promoted to rank {}.\n" ANRM, member->rank); - } +bool Clan::remove_member_by_name(const std::string& name) { + auto it = std::ranges::find_if(members_, [&name](const ClanMember &m) { return m.name == name; }); + if (it != members_.end()) { + members_.erase(it); + invalidate_rank_cache(); + return true; } + return false; +} - else if (is_abbrev(arg, "dues")) { - any_one_arg(argument, arg); - if (!is_number(arg)) { - char_printf(ch, "How much platinum should the clan's dues be?\n"); - return; - } - clan->dues = atoi(arg); - char_printf(ch, "{}'s monthly dues are now {} platinum.\n", clan->name, clan->dues); +bool Clan::update_member_rank(const std::string& name, int new_rank_index) { + if (new_rank_index < 0 || new_rank_index >= static_cast<int>(ranks_.size())) { + return false; } - - else if (is_abbrev(arg, "name")) { - char *old_name = clan->name; - skip_spaces(&argument); - if (ansi_strlen(argument) > MAX_CLAN_NAME_LEN) { - char_printf(ch, "Clan names may be at most {} characters in length.\n", MAX_CLAN_NAME_LEN); - return; - } - clan->name = nullptr; /* so find_clan doesn't find this clan */ - if (find_clan(argument)) { - char_printf(ch, "A clan with that name already exists!\n"); - clan->name = old_name; /* revert */ - return; - } - clan->name = strdup(fmt::format("{}&0", argument).c_str()); - char_printf(ch, "{} is now named {}.\n", old_name, clan->name); - log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} renames {} to {}", GET_NAME(ch), old_name, clan->name); - free(old_name); + + auto it = std::ranges::find_if(members_, [&name](ClanMember &m) { return m.name == name; }); + if (it != members_.end()) { + it->rank_index = new_rank_index; + invalidate_rank_cache(); + return true; } + return false; +} - else if (is_abbrev(arg, "title")) { - unsigned int rank; - argument = any_one_arg(argument, arg); - if (!*arg) { - char_printf(ch, "For which rank do you want to set a title?\n"); - return; +void Clan::build_rank_cache() const { + if (rank_cache_valid_) return; + + rank_cache_.clear(); + + // Build cache of online characters by rank + for (CharData *ch = character_list; ch; ch = ch->next) { + // Skip NPCs and characters without descriptors (not online) + if (IS_NPC(ch) || !ch->desc) { + continue; } - rank = atoi(arg); - if (!is_number(arg) || rank < 1 || rank > clan->rank_count) { - char_printf(ch, "'{}' is an invalid rank. Valid ranks are 1-{}.\n", arg, clan->rank_count); - return; + + // Check if this character is a member of this clan + if (ch->player_specials->clan_id != id_) { + continue; } - skip_spaces(&argument); - if (!IS_CLAN_SUPERADMIN(ch) && !IS_CLAN_ADMIN(ch) && OUTRANKS(rank, GET_CLAN_RANK(ch))) { - char_printf(ch, "You cannot set the title for a rank above your own.\n"); - return; + + // Get their clan membership info + auto member_opt = get_member_by_name(ch->player.short_descr); + if (!member_opt.has_value()) { + continue; } - if (ansi_strlen(argument) > MAX_CLAN_TITLE_LEN) { - char_printf(ch, "Clan titles may be at most {} characters long.\n", MAX_CLAN_TITLE_LEN); - return; + + // Get their rank + int rank_index = member_opt->rank_index; + if (rank_index < 0 || rank_index >= static_cast<int>(ranks_.size())) { + continue; } - free(clan->ranks[rank - 1].title); - clan->ranks[rank - 1].title = strdup(fmt::format("{}&0", argument).c_str()); - char_printf(ch, "Rank {}'s title is now: {}\n", rank, argument); - clan_notification(clan, ch, "%s has changed rank %u's title to %s.", GET_NAME(ch), rank, argument); - } - - else { - log(LogSeverity::Error, LVL_GOD, "SYSERR: clan_set: unknown subcommand '{}'", arg); - return; - } - - save_clan(clan); + + const auto &rank = ranks_[rank_index]; + + // Create a shared_ptr wrapper for the character + // Note: This is a temporary wrapper for the cache - the actual character + // lifetime is managed by the game engine, not by these shared_ptrs + CharacterPtr char_ptr(ch, [](CharData*) { /* no-op deleter */ }); + + // Add to the cache + rank_cache_[rank].push_back(char_ptr); + } + + rank_cache_valid_ = true; } -CLANCMD(clan_alt) { - CharData *tch; - ClanMembership *alt; +std::vector<CharacterPtr> Clan::get_members_by_rank(const ClanRank &rank) const { + build_rank_cache(); - argument = any_one_arg(argument, arg); - - if (!*arg) { - char_printf(ch, "Whom do you want to add or remove as an alt?\n"); - return; - } - - /* - * First, let's see if we're trying to remove an alt: they don't have - * to be online for that. - */ - for (alt = member->relation.alts; alt; alt = alt->next) { - if (!strcasecmp(alt->name, arg)) { - char_printf(ch, "You remove {} as one of {}{} clan alts.\n", alt->name, - ch == member->player ? "your" : member->name, ch == member->player ? "" : "'s"); - if (ch != member->player && member->player) - char_printf(FORWARD(member->player), AFMAG "{} removes {} as one of your clan alts.\n" ANRM, - GET_NAME(ch), alt->name); - if (alt->player) - char_printf(FORWARD(alt->player), AFMAG "You are no longer one of {}'s clan alts.\n" ANRM, - member->name); - revoke_clan_membership(alt); - return; - } - } - - if (!(tch = find_char_by_desc(find_vis_by_name(ch, arg)))) - char_printf(ch, "There's no one online by the name of {}.\n", arg); - else if (ch == tch) - char_printf(ch, "You want to be your own alt?\n"); - else if (GET_CLAN_MEMBERSHIP(tch)) - char_printf(ch, "{} is already in a clan!\n", GET_NAME(tch)); - else if (!IS_CLAN_SUPERADMIN(ch) && !IS_CLAN_ADMIN(ch) && strcasecmp(ch->desc->host, tch->desc->host)) - char_printf(ch, "{} was not found logged in as your alt.\n", GET_NAME(tch)); - else if (IS_CLAN_SUPERADMIN(tch)) - char_printf(ch, "{} is already a clan super-admin!\n", GET_NAME(tch)); - else { - char_printf(ch, "You make {} one of {}{} clan alts.\n", GET_NAME(tch), - ch == member->player ? "your" : member->name, ch == member->player ? "" : "'s"); - char_printf(tch, AFMAG "{} makes you one of {}{} clan alts.\n" ANRM, GET_NAME(ch), - ch == member->player ? HSHR(member->player) : member->name, ch == member->player ? "" : "'s"); - if (ch != member->player && member->player) - char_printf(FORWARD(member->player), AFMAG "{} makes {} one of your clan alts.\n" ANRM, GET_NAME(ch), - GET_NAME(tch)); - CREATE(alt, ClanMembership, 1); - alt->name = strdup(GET_NAME(tch)); - alt->rank = ALT_RANK_OFFSET + member->rank; - alt->since = member->since; - alt->relation.member = member; - alt->next = member->relation.alts; - member->relation.alts = alt; - alt->clan = clan; - alt->player = tch; - GET_CLAN_MEMBERSHIP(tch) = alt; - save_player_char(tch); - save_clan(clan); + auto it = rank_cache_.find(rank); + if (it != rank_cache_.end()) { + return it->second; } + return {}; } -CLANCMD(clan_apply) { - if (GET_LEVEL(ch) < LVL_GOD && GET_PLATINUM(ch) < clan->app_fee) { - char_printf(ch, "You don't have enough money to cover the {} platinum application fee.\n", clan->app_fee); - return; +bool Clan::has_permission(const CharacterPtr &character, ClanPermission permission) const { + if (!is_member(character)) { + return false; } - - if (GET_LEVEL(ch) < clan->app_level) { - char_printf(ch, "{} does not accept players beneath level {}.\n", clan->name, clan->app_level); - return; + + auto member = get_member_by_name(character->player.short_descr); + if (!member.has_value()) { + return false; } - - char_printf(ch, "You apply to {}.\n", clan->name); - - if (GET_LEVEL(ch) < LVL_GOD) { - GET_PLATINUM(ch) -= clan->app_fee; - clan->treasure[PLATINUM] += clan->app_fee; + + if (member->rank_index < 0 || member->rank_index >= static_cast<int>(ranks_.size())) { + return false; } - - clan_notification(clan, nullptr, "%s has applied to your clan.", GET_NAME(ch)); - - CREATE(member, ClanMembership, 1); - member->name = strdup(GET_NAME(ch)); - member->rank = RANK_APPLICANT; - member->since = time(0); - member->player = ch; - GET_CLAN_MEMBERSHIP(ch) = member; - add_clan_membership(clan, member); - save_player_char(ch); - save_clan(clan); + + return ranks_[member->rank_index].has_permission(permission); } -CLANCMD(clan_create) { - unsigned int i = 0; - - fetch_word(argument, buf, sizeof(buf)); - - if (!*buf) - char_printf(ch, "What is the abbreviation for the new clan?\n"); - else if (ansi_strlen(buf) > 10) - char_printf(ch, "Clan abbreviations can be at most 10 visible characters long.\n"); - else if (find_clan_by_abbr(strip_ansi(buf).c_str())) - char_printf(ch, "A clan with a similar abbreviation already exists.\n"); - else { - strcat(buf, "&0"); - clan = alloc_clan(); - clan->name = strdup(buf); - clan->abbreviation = strdup(buf); - clan->description = nullptr; - clan->motd = nullptr; - - clan->dues = 0; - clan->app_fee = 0; - clan->app_level = 0; - clan->power = 0; - for (i = 0; i < NUM_COIN_TYPES; ++i) - clan->treasure[i] = 0; - - clan->rank_count = 2; - CREATE(clan->ranks, ClanRank, 2); - clan->ranks[0].title = strdup("Leader"); - clan->ranks[1].title = strdup("Member"); - for (i = 0; i < NUM_CLAN_PRIVS; ++i) { - SET_FLAG(clan->ranks[0].privileges, i); - if (clan_privileges[i].default_on) - SET_FLAG(clan->ranks[1].privileges, i); - } - - clan->people_count = 0; - clan->people = nullptr; - clan->member_count = 0; - clan->members = nullptr; - clan->admin_count = 0; - clan->admins = nullptr; - clan->applicant_count = 0; - clan->applicants = nullptr; - - char_printf(ch, "New clan created.\n"); - log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} creates new clan: {}", GET_NAME(ch), buf); - - save_clan(clan); - } +bool Clan::grant_permission(const CharacterPtr &character, ClanPermission permission) { + // Individual permissions are no longer supported - only rank-based permissions + // To grant permissions, promote the character to a rank that has the permission + return false; } -CLANCMD(update_clan_rank) { - unsigned int rank; - const char *action = nullptr; - - argument = any_one_arg(argument, arg); +void Clan::notify(const CharacterPtr &skip, const std::string_view messg) { + notify(skip ? skip.get() : nullptr, messg); +} - if (is_abbrev(arg, "demote")) { - if (!IS_CLAN_SUPERADMIN(ch) && ch != member->player && !OUTRANKS(GET_CLAN_RANK(ch), member->rank)) - char_printf(ch, "You cannot demote someone at or above your rank.\n"); - else if (member->rank == clan->rank_count) - char_printf(ch, "{} is already the minimum rank.\n", member->name); - else if (!IS_MEMBER_RANK(member->rank)) - char_printf(ch, "{} isn't a clan member.\n", member->name); - else { - rank = member->rank + 1; - action = "demote"; - } - } else if (is_abbrev(arg, "promote")) { - if (!IS_CLAN_SUPERADMIN(ch) && !OUTRANKS(GET_CLAN_RANK(ch), member->rank)) - char_printf(ch, "You cannot promote someone at or above your rank.\n"); - else if (member->rank == RANK_LEADER) - char_printf(ch, "{} is already the maximum rank.\n", member->name); - else if (!IS_MEMBER_RANK(member->rank)) - char_printf(ch, "{} isn't a clan member.\n", member->name); - else { - rank = member->rank - 1; - action = "promote"; - } - } else { - log("SYSERR: update_clan_rank: invalid subcommand '{}'", arg); +void Clan::notify(const CharData *skip, const std::string_view messg) { + if (messg.empty()) { return; } - - /* action only gets set if all checks above were successful. */ - if (!action) - return; - - if (ch == member->player) - char_printf(ch, "You {} yourself to rank {}: {}\n", action, rank, clan->ranks[rank - 1].title); - else { - if (member->player) - char_printf(FORWARD(member->player), AFMAG "{} has {}d you to rank {}: " ANRM "{}\n", GET_NAME(ch), action, - rank, clan->ranks[rank - 1].title); - char_printf(ch, "You {} {} to rank {}: {}\n", action, member->name, rank, clan->ranks[rank - 1].title); + + // Iterate through all online characters and notify clan members + for (CharData *ch = character_list; ch; ch = ch->next) { + // Skip NPCs and characters without descriptors (not online) + if (IS_NPC(ch) || !ch->desc) { + continue; + } + + // Skip the character we don't want to notify + if (skip && ch == skip) { + continue; + } + + // Check if this character is a member of this clan + if (ch->player_specials->clan_id != id_) { + continue; + } + + // Verify they're actually in our member list + if (!get_member_by_name(ch->player.short_descr).has_value()) { + continue; + } + + // Send the message + char_printf(ch, "{}", messg); } - member->rank = RANK_NONE; /* Temporary so they don't get the notification */ - clan_notification(clan, ch, "%s has %sd %s to rank %u: " ANRM "%s", GET_NAME(ch), action, member->name, rank, - clan->ranks[rank - 1].title); - member->rank = rank; - /* Shift alts' ranks too */ - for (member = member->relation.alts; member; member = member->next) - member->rank = rank + ALT_RANK_OFFSET; - save_clan(clan); } -CLANCMD(clan_destroy) { - clan_notification(clan, ch, "Your clan has been disbanded!"); - char_printf(ch, AFMAG "You have deleted the clan {}.\n" ANRM, clan->name); - log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} has destroyed the clan {}.", GET_NAME(ch), clan->name); - dealloc_clan(clan); -} - -CLANCMD(clan_enroll) { - int num; - - member->since = time(0); - member->rank = clan->member_count ? clan->rank_count : RANK_LEADER; - --clan->applicant_count; - ++clan->member_count; - if (member->player) - clan->power += GET_LEVEL(member->player); - else if ((num = get_ptable_by_name(member->name))) - clan->power += player_table[num].level; - - update_clan(clan); - save_clan(clan); - - char_printf(ch, "You {} {} {} {}.\n", member->rank == RANK_LEADER ? "appoint" : "enroll", member->name, - member->rank == RANK_LEADER ? "the leader of" : "in", clan->name); - if (member->player) - char_printf(FORWARD(member->player), AFMAG "You've been {} {}" AFMAG "!\n" ANRM, - member->rank == RANK_LEADER ? "appointed the leader of" : "enrolled in", clan->name); +// JSON serialization implementations - log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} enrolls {} in {}.", GET_NAME(ch), member->name, clan->name); +void to_json(nlohmann::json &j, const ClanRank &rank) { + j = nlohmann::json{{"title", rank.title()}, {"privileges", rank.privileges().to_string()}}; } -CLANCMD(clan_expel) { - char *name = strdup(member->name); +void from_json(const nlohmann::json &j, ClanRank &rank) { + std::string title = j["title"].get<std::string>(); + std::string privileges_str = j["privileges"].get<std::string>(); + PermissionSet permissions(privileges_str); - if (!IS_CLAN_SUPERADMIN(ch) && !OUTRANKS(GET_CLAN_RANK(ch), member->rank)) { - char_printf(ch, "{} outranks you!\n", name); - free(name); - return; - } - - clan = member->clan; - - char_printf(ch, "You expel {} from {}.\n", name, clan->name); - - if (member->player) - char_printf(FORWARD(member->player), AFMAG "{} has expelled you from {}" AFMAG ".\n" ANRM, GET_NAME(ch), - clan->name); - - log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} expels {} from {}.", GET_NAME(ch), name, clan->name); - revoke_clan_membership(member); - - clan_notification(clan, ch, "%s has expelled %s from your clan.", GET_NAME(ch), name); - free(name); + // Use proper setters instead of placement new + rank.set_title(std::move(title)); + rank.set_privileges(std::move(permissions)); } -CLANCMD(clan_priv) { - enum { GRANT, REVOKE } action; - int rank, priv; - - argument = any_one_arg(argument, arg); - - if (is_abbrev(arg, "grant")) - action = GRANT; - else if (is_abbrev(arg, "revoke")) - action = REVOKE; - else { - log("SYSERR: clan_priv: invalid subcommand '{}'", arg); - return; - } - - argument = any_one_arg(argument, arg); - rank = atoi(arg); - - if (rank < RANK_LEADER || rank > clan->rank_count) { - char_printf(ch, "'{}' is an invalid rank. Valid ranks are 1-{}.\n", arg, clan->rank_count); - return; - } - - argument = any_one_arg(argument, arg); - for (priv = 0; priv < NUM_CLAN_PRIVS; ++priv) - if (is_abbrev(arg, clan_privileges[priv].abbr)) - break; - if (priv >= NUM_CLAN_PRIVS) { - char_printf(ch, "'{}' is an invalid privilege. Valid privileges are listed on clan info.\n", arg); - return; - } - - if (!IS_CLAN_SUPERADMIN(ch) && !IS_CLAN_ADMIN(ch) && !HAS_CLAN_PRIV(ch, priv)) { - char_printf(ch, "You cannot grant or revoke a privilege you do not have!\n"); - return; - } +void to_json(nlohmann::json &j, const ClanMember &member) { + j = nlohmann::json{{"name", member.name}, + {"rank_index", member.rank_index}, + {"join_time", member.join_time}, + {"alts", member.alts}}; +} - if (IS_CLAN_MEMBER(ch) && !OUTRANKS(GET_CLAN_RANK(ch), rank)) { - char_printf(ch, "You may only grant or revoke privileges on ranks below yours.\n"); - return; +void from_json(const nlohmann::json &j, ClanMember &member) { + member.name = j["name"].get<std::string>(); + member.rank_index = j["rank_index"].get<int>(); + member.join_time = j["join_time"].get<time_t>(); + if (j.contains("alts") && j["alts"].is_array()) { + member.alts = j["alts"].get<std::vector<std::string>>(); } +} - if (action == GRANT) { - if (IS_FLAGGED(clan->ranks[rank - 1].privileges, priv)) - char_printf(ch, "Rank {} already has the {} privilege.\n", rank, clan_privileges[priv].desc); - else { - SET_FLAG(clan->ranks[rank - 1].privileges, priv); - char_printf(ch, "Granted rank {} access to the {} privilege.\n", rank, clan_privileges[priv].desc); - } - } else if (action == REVOKE) { - if (IS_FLAGGED(clan->ranks[rank - 1].privileges, priv)) { - REMOVE_FLAG(clan->ranks[rank - 1].privileges, priv); - char_printf(ch, "Revoked rank {} access to the {} privilege.\n", rank, clan_privileges[priv].desc); - } else - char_printf(ch, "Rank {} doesn't have the {} privilege.\n", rank, clan_privileges[priv].desc); +void to_json(nlohmann::json &j, const Clan &clan) { + j = nlohmann::json{{"id", clan.id()}, + {"name", clan.name()}, + {"abbreviation", clan.abbreviation()}, + {"description", clan.description()}, + {"motd", clan.motd()}, + {"dues", clan.dues()}, + {"app_fee", clan.app_fee()}, + {"min_application_level", clan.min_application_level()}, + {"treasure", clan.treasure().to_json()}, + {"storage", clan.storage()}, + {"bank_room", clan.bank_room()}, + {"chest_room", clan.chest_room()}, + {"hall_room", clan.hall_room()}, + {"ranks", clan.ranks()}, + {"members", clan.members()}}; + + // Add storage as a separate object + j["storage"] = nlohmann::json::object(); + for (const auto &[obj_id, amount] : clan.storage()) { + j["storage"][std::to_string(obj_id)] = amount; } - - save_clan(clan); } -static void show_clan_info(CharData *ch, const Clan *clan) { - const ClanMembership *member = GET_CLAN_MEMBERSHIP(ch); - size_t i, j; - bool show_all = - ((member && member->clan == clan && OUTRANKS(member->rank, RANK_APPLICANT)) || IS_CLAN_SUPERADMIN(ch)); - - strcpy(buf, "----------------------------------------------------------------------\n"); - sprintf(buf1, "[ Clan %u: %s ]", clan->number, clan->name); - memcpy(buf + 29 - strlen(clan->name) / 2, buf1, strlen(buf1)); - paging_printf(ch, buf); - - paging_printf(ch, - "Nickname: " AFYEL "{}" ANRM - " " - "Ranks: " AFYEL "{}" ANRM - " " - "Members: " AFYEL "{}" ANRM - " " - "Power: " AFYEL "{}" ANRM - "\n" - "Applicants: " AFYEL "{}" ANRM - " " - "App Fee: " AFCYN "{}" ANRM - " " - "App Level: " AFYEL "{}" ANRM - " " - "Dues: " AFCYN "{}" ANRM "\n", - clan->abbreviation, clan->rank_count, clan->member_count, clan->power, clan->applicant_count, - clan->app_fee, clan->app_level, clan->dues); - - if (show_all) { - statemoney(buf, clan->treasure); - paging_printf(ch, "Treasure: {}\n", buf); - - paging_printf(ch, "\nRanks:\n"); - for (i = 0; i < clan->rank_count; ++i) - paging_printf(ch, "{:3} {}\n", i + 1, clan->ranks[i].title); - - paging_printf(ch, "\nPrivileges:\n"); - for (j = 1; j <= clan->rank_count; ++j) - paging_printf(ch, "{:3}", j); - for (i = 0; i < NUM_CLAN_PRIVS; ++i) { - paging_printf(ch, "\n{:<9}", clan_privileges[i].abbr); - for (j = 0; j < clan->rank_count; ++j) - paging_printf(ch, " {}{}" ANRM, IS_FLAGGED(clan->ranks[j].privileges, i) ? AFGRN : AFRED, - IS_FLAGGED(clan->ranks[j].privileges, i) ? 'Y' : 'N'); +void from_json(const nlohmann::json &j, Clan &clan) { + clan.id_ = j["id"].get<ClanID>(); + clan.name_ = j["name"].get<std::string>(); + clan.abbreviation_ = j["abbreviation"].get<std::string>(); + clan.description_ = j["description"].get<std::string>(); + clan.motd_ = j["motd"].get<std::string>(); + clan.dues_ = j["dues"].get<unsigned int>(); + clan.app_fee_ = j["app_fee"].get<unsigned int>(); + clan.min_application_level_ = j["min_application_level"].get<unsigned int>(); + clan.treasure_ = Money(j["treasure"]); + clan.bank_room_ = j["bank_room"].get<room_num>(); + clan.chest_room_ = j["chest_room"].get<room_num>(); + clan.hall_room_ = j["hall_room"].get<room_num>(); + clan.ranks_ = j["ranks"].get<std::vector<ClanRank>>(); + clan.members_ = j["members"].get<std::vector<ClanMember>>(); + + // Load storage + clan.storage_.clear(); + if (j.contains("storage") && j["storage"].is_object()) { + for (const auto &[key, value] : j["storage"].items()) { + clan.storage_[std::stoul(key)] = value.get<int>(); } - paging_printf(ch, "\n"); } - if (clan->description) - paging_printf(ch, "\nDescription:\n{}", clan->description); - - if (show_all) - if (clan->motd) - paging_printf(ch, "\nMessage of the Day:\n{}", clan->motd); - - start_paging(ch); + clan.invalidate_rank_cache(); } -CLANCMD(clan_info) { - argument = any_one_arg(argument, arg); +// ClanRepository implementation - if (!*arg) { - if (clan) - show_clan_info(ch, clan); - else - char_printf(ch, "Which clan's info do you want to view?\n"); - } else if ((clan = find_clan(arg))) - show_clan_info(ch, clan); - else - char_printf(ch, "'{}' does not refer to a valid clan.\n", arg); +std::optional<ClanPtr> ClanRepository::find_by_name(const std::string_view name) const { + build_caches(); + auto it = name_to_id_.find(std::string(name)); + return it != name_to_id_.end() ? find_by_id(it->second) : std::nullopt; } -static void show_clan_member_status(CharData *ch, CharData *tch) { - if (IS_CLAN_SUPERADMIN(tch)) - char_printf(ch, "{} {} a clan super-administrator.\n", ch == tch ? "You" : GET_NAME(tch), - ch == tch ? "are" : "is"); - else if (IS_CLAN_REJECT(tch)) { - unsigned int days = days_until_reapply(GET_CLAN_MEMBERSHIP(tch)); - char_printf(ch, "{} {} rejected from {} and may re-apply in {:d} day{}.\n", ch == tch ? "You" : GET_NAME(tch), - ch == tch ? "were" : "was", GET_CLAN(tch)->name, days, days == 1 ? "" : "s"); - } else if (IS_CLAN_ADMIN(tch)) - char_printf(ch, "{} {} an administrator for {}.\n", ch == tch ? "You" : GET_NAME(tch), ch == tch ? "are" : "is", - GET_CLAN(tch)->name); - else if (IS_CLAN_MEMBER(tch) || IS_CLAN_ALT(tch) || IS_CLAN_APPLICANT(tch)) { - strftime(buf, sizeof(buf), "%a, %d %b %Y", localtime(&GET_CLAN_MEMBERSHIP(tch)->since)); - paging_printf(ch, - "Clan membership status for {}:\n" - " Clan: {}\n", - GET_NAME(tch), GET_CLAN(tch)->name); - if (IS_CLAN_MEMBER(tch)) - paging_printf(ch, " Rank: {:d} - {}\n", GET_CLAN_RANK(tch), GET_CLAN_TITLE(tch), - IS_CLAN_LEADER(tch) ? " (Leader)" : ""); - else if (IS_CLAN_ALT(tch)) - paging_printf(ch, " Alt rank: {:d} ({})\n", GET_CLAN_RANK(tch) - ALT_RANK_OFFSET, - GET_CLAN_MEMBERSHIP(tch)->relation.member->name); - else if (IS_CLAN_APPLICANT(tch)) - paging_printf(ch, " Rank: Applicant\n"); - paging_printf(ch, " Member since: {}\n", buf); - if (IS_CLAN_MEMBER(tch)) { - if (HAS_FLAGS(GET_CLAN(tch)->ranks[GET_CLAN_RANK(tch) - 1].privileges, NUM_CLAN_PRIVS)) { - ScreenBuf *sb = new_screen_buf(); - int i, seen = 0; - const size_t len = strlen(" Privileges: "); - sb_set_first_indentation(sb, len); - sb_set_other_indentation(sb, len); - for (i = 0; i < NUM_CLAN_PRIVS; ++i) - if (HAS_CLAN_PRIV(tch, i)) - sb_append(sb, "%s%s", seen++ ? ", " : "", clan_privileges[i].abbr); - /* skip over the first 14 spaces (dummy indentation) */ - paging_printf(ch, " Privileges: {}\n", sb_get_buffer(sb) + len); - free_screen_buf(sb); - } - if (GET_CLAN_MEMBERSHIP(tch)->relation.alts) { - ClanMembership *alt; - ScreenBuf *sb = new_screen_buf(); - int seen = 0; - const size_t len = strlen(" Alts: "); - sb_set_first_indentation(sb, len); - sb_set_other_indentation(sb, len); - for (alt = GET_CLAN_MEMBERSHIP(tch)->relation.alts; alt; alt = alt->next) - sb_append(sb, "%s%s", seen++ ? ", " : "", alt->name); - /* skip over the first 8 spaces (dummy indentation) */ - paging_printf(ch, " Alts: {}\n", sb_get_buffer(sb) + len); - free_screen_buf(sb); - } - } - start_paging(ch); - } else - char_printf(ch, "{} {} not associated with any clan.\n", ch == tch ? "You" : GET_NAME(tch), - ch == tch ? "are" : "is"); +std::optional<ClanPtr> ClanRepository::find_by_abbreviation(const std::string_view abbr) const { + build_caches(); + auto it = abbr_to_id_.find(std::string(abbr)); + return it != abbr_to_id_.end() ? find_by_id(it->second) : std::nullopt; } -CLANCMD(clan_status) { - CharData *tch; - - argument = any_one_arg(argument, arg); - - if (IS_CLAN_SUPERADMIN(ch)) { - if ((tch = find_char_around_char(ch, find_vis_by_name(ch, arg)))) - show_clan_member_status(ch, tch); - else - char_printf(ch, "Couldn't find a player by the name of '{}'.\n", arg); - } else - show_clan_member_status(ch, ch); -} +std::expected<void, std::string> ClanRepository::save_to_file(const std::filesystem::path &filepath) const { + try { + nlohmann::json j = nlohmann::json::array(); + for (const auto &clan : all()) { + j.push_back(*clan); + } -struct ClanEdit { - Clan *clan; - char string[20]; -}; + std::ofstream file(filepath); + if (!file) { + return std::unexpected("Could not open file for writing"); + } -static EDITOR_FUNC(clan_edit_done) { - DescriptorData *d = edit->descriptor; - ClanEdit *data = (ClanEdit *)edit->data; + file << j.dump(4); - if (edit->command == ED_EXIT_SAVE) { - editor_default_exit(edit); - log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} edits {}'s {}", GET_NAME(d->character), data->clan->name, - data->string); - save_clan(data->clan); + log(LogSeverity::Info, LVL_IMMORT, "Saved clan data to file: {}", filepath.string()); + return {}; + } catch (const std::exception &e) { + return std::unexpected(e.what()); } - - act("$n stops writing on the large scroll.", true, d->character, 0, 0, TO_ROOM); - - return ED_PROCESSED; } -CLANCMD(clan_edit) { - char **message; - const char *editing; - ClanEdit *data; - - if (!ch->desc) - return; +std::expected<void, std::string> ClanRepository::load_from_file(const std::filesystem::path &filepath) { + try { + std::ifstream file(filepath); + if (!file) { + return std::unexpected("Could not open file for reading"); + } - any_one_arg(argument, arg); + nlohmann::json j; + file >> j; - if (is_abbrev(arg, "motd")) { - message = &clan->motd; - editing = "message of the day"; - } else if (is_abbrev(arg, "desc")) { - message = &clan->description; - editing = "description"; - } else { - log("SYSECR: E unknown string specified"); - return; - } - - CREATE(data, ClanEdit, 1); - data->clan = clan; - strcpy(data->string, arg); + clans_.clear(); + for (const auto &clan_json : j) { + auto clan = std::make_shared<Clan>(0, "", ""); + from_json(clan_json, *clan); + clans_[clan->id()] = clan; + } - if (editor_edited_by(message)) { - char_printf(ch, "{}'s {} is already being edited.", clan->name, editing); - return; + caches_valid_ = false; + return {}; + } catch (const std::exception &e) { + return std::unexpected(e.what()); } - - act("$n begins writing on a large scroll.", true, ch, 0, 0, TO_ROOM); - - editor_init(ch->desc, message, MAX_DESC_LENGTH); - editor_set_begin_string(ch->desc, "Edit %s's %s below.", clan->name, editing); - editor_set_callback_data(ch->desc, data, ED_FREE_DATA); - editor_set_callback(ch->desc, ED_EXIT_SAVE, clan_edit_done); - editor_set_callback(ch->desc, ED_EXIT_ABORT, clan_edit_done); } -CLANCMD(clan_quit) { - if (IS_CLAN_APPLICANT(ch)) - char_printf(ch, "You are no longer applying to {}.\n", clan->name); - else if (IS_CLAN_ALT(ch)) - char_printf(ch, "You are no longer a clan alt in {}.\n", clan->name); - else if (IS_CLAN_ADMIN(ch)) - char_printf(ch, "You are no longer an administrator for {}.\n", clan->name); - else if (IS_CLAN_MEMBER(ch)) - char_printf(ch, "You are no longer a member of {}.\n", clan->name); - else - char_printf(ch, "You are no longer in {}.\n", clan->name); - if (!IS_CLAN_ALT(ch)) { - log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} quits {}.", GET_NAME(ch), clan->name); - clan_notification(clan, ch, "%s has quit your clan.", GET_NAME(ch)); +void ClanRepository::init_clans() { + auto result = load(); + if (!result) { + log(LogSeverity::Info, LVL_IMMORT, "JSON clan data not found, trying legacy format: {}", result.error()); + + // Try to load legacy clan files + std::filesystem::path clans_dir = "etc/clans"; + if (std::filesystem::exists(clans_dir)) { + // Look for .clan files and load them + int loaded_count = 0; + for (const auto& entry : std::filesystem::directory_iterator(clans_dir)) { + if (entry.is_regular_file() && entry.path().extension() == ".clan") { + std::string filename = entry.path().stem().string(); + if (load_legacy(filename)) { + loaded_count++; + log(LogSeverity::Info, LVL_IMMORT, "Loaded legacy clan: {}", filename); + } else { + log(LogSeverity::Warn, LVL_IMMORT, "Failed to load legacy clan: {}", filename); + } + } + } + + if (loaded_count > 0) { + log(LogSeverity::Info, LVL_IMMORT, "Loaded {} legacy clan(s). Consider converting to JSON format.", loaded_count); + + // Optionally save the loaded clans in JSON format for future use + auto save_result = save(); + if (save_result) { + log(LogSeverity::Info, LVL_IMMORT, "Converted legacy clans to JSON format"); + } else { + log(LogSeverity::Warn, LVL_IMMORT, "Failed to save converted clans: {}", save_result.error()); + } + } else { + log(LogSeverity::Warn, LVL_IMMORT, "No valid clan files found"); + } + } else { + log(LogSeverity::Warn, LVL_IMMORT, "Clans directory not found: {}", clans_dir.string()); + } } - revoke_clan_membership(GET_CLAN_MEMBERSHIP(ch)); } -CLANCMD(clan_reject) { - member->since = time(0); - member->rank = RANK_REJECT; - --clan->applicant_count; - ++clan->reject_count; - - save_clan(clan); - - if (member->player) - char_printf(FORWARD(member->player), - AFMAG "You have been rejected from {} and may reapply in {} " AFMAG "days.\n" ANRM, - member->clan->name, days_until_reapply(member)); - - log(LogSeverity::Stat, LVL_GOD, "(CLAN) {} rejects {}'s application to {}.", GET_NAME(ch), member->name, - member->clan->name); - char_printf(ch, "You reject {}'s application to {}.\n", member->name, clan->name); - clan_notification(clan, ch, "%s rejects %s's application to your clan.", GET_NAME(ch), member->name); -} - -CLANCMD(clan_snoop) { - ClanSnoop *snoop, *temp; - clan_iter iter; - - fetch_word(argument, arg, sizeof(arg)); - - if (!*arg) { - if (GET_CLAN_SNOOP(ch)) { - char_printf(ch, "You are currently snooping:\n"); - for (snoop = GET_CLAN_SNOOP(ch); snoop; snoop = snoop->next) - char_printf(ch, " {}\n", snoop->clan->name); - } else - char_printf(ch, "You are not currently snooping any clan channels.\n"); - } - - else if (!strcasecmp(arg, "off")) { - if (GET_CLAN_SNOOP(ch)) { - while (GET_CLAN_SNOOP(ch)) { - snoop = GET_CLAN_SNOOP(ch)->next; - free(GET_CLAN_SNOOP(ch)); - GET_CLAN_SNOOP(ch) = snoop; +bool ClanRepository::load_legacy(const std::string_view clan_num) { + std::filesystem::path clan_file = std::filesystem::path("etc/clans") / (std::string(clan_num) + ".clan"); + + std::ifstream file(clan_file); + if (!file) { + return false; + } + + // Create a temporary clan object to populate + ClanID clan_id = 0; + std::string clan_name; + std::string clan_abbr; + std::string clan_motd; + unsigned int dues = 0; + unsigned int app_fee = 0; + unsigned int min_app_level = 0; + Money treasure; + std::vector<ClanRank> ranks; + std::vector<ClanMember> members; + + // Maps to build ranks and permissions + std::map<int, std::string> rank_titles; + std::map<int, std::vector<std::string>> rank_privileges; + + std::string line; + while (std::getline(file, line)) { + if (line.empty()) continue; + + std::istringstream iss(line); + std::string key; + if (!(iss >> key)) continue; + + if (key == "number:") { + iss >> clan_id; + } else if (key == "name:") { + std::string rest; + std::getline(iss, rest); + clan_name = rest.substr(1); // Remove leading space + } else if (key == "abbr:") { + std::string rest; + std::getline(iss, rest); + clan_abbr = rest.substr(1); // Remove leading space + } else if (key == "motd:") { + // Handle multi-line motd that ends with ~ + std::string motd_line; + while (std::getline(file, motd_line)) { + if (motd_line == "~") { + break; + } + if (!clan_motd.empty()) { + clan_motd += "\n"; + } + clan_motd += motd_line; } - char_printf(ch, "You are no longer snooping any clan channels.\n"); - } else - char_printf(ch, "You are not currently snooping any clan channels.\n"); - } - - else if (!strcasecmp(arg, "all")) { - for (iter = clans_start(); iter != clans_end(); ++iter) - if (!is_snooping(ch, *iter)) { - CREATE(snoop, ClanSnoop, 1); - snoop->clan = *iter; - snoop->next = GET_CLAN_SNOOP(ch); - GET_CLAN_SNOOP(ch) = snoop; + } else if (key == "dues:") { + iss >> dues; + } else if (key == "appfee:") { + iss >> app_fee; + } else if (key == "applevel:") { + iss >> min_app_level; + } else if (key == "copper:") { + int copper; + iss >> copper; + treasure[COPPER] = copper; + } else if (key == "silver:") { + int silver; + iss >> silver; + treasure[SILVER] = silver; + } else if (key == "gold:") { + int gold; + iss >> gold; + treasure[GOLD] = gold; + } else if (key == "platinum:") { + int platinum; + iss >> platinum; + treasure[PLATINUM] = platinum; + } else if (key == "title:") { + int rank_num; + iss >> rank_num; + std::string rest; + std::getline(iss, rest); + rank_titles[rank_num] = rest.substr(1); // Remove leading space + } else if (key == "privilege:") { + int rank_num; + iss >> rank_num; + std::string privilege; + std::vector<std::string> privileges; + while (iss >> privilege) { + privileges.push_back(privilege); + } + rank_privileges[rank_num] = privileges; + } else if (key == "member:") { + std::string name; + int rank_index; + time_t join_time; + iss >> name >> rank_index >> join_time; + + // Read optional alts + std::vector<std::string> alts; + std::string alt; + while (iss >> alt) { + alts.push_back(alt); } - char_printf(ch, "You are now snooping all clan channels.\n"); + + // Convert rank_index from 1-based to 0-based + rank_index = rank_index > 0 ? rank_index - 1 : 0; + + members.emplace_back(name, rank_index, join_time, std::move(alts)); + } } - - else if ((clan = find_clan(arg))) { - if (is_snooping(ch, clan)) { - snoop = GET_CLAN_SNOOP(ch); - if (snoop->clan == clan) { - GET_CLAN_SNOOP(ch) = snoop->next; - free(snoop); - } else { - for (; snoop && snoop->next; snoop = snoop->next) { - if (snoop->next->clan == clan) { - temp = snoop->next; - snoop->next = snoop->next->next; - free(temp); - } + + // Build ranks from the collected data + for (const auto& [rank_num, title] : rank_titles) { + PermissionSet permissions; + + auto priv_it = rank_privileges.find(rank_num); + if (priv_it != rank_privileges.end()) { + for (const auto& priv : priv_it->second) { + if (priv == "desc" || priv == "setdesc") { + permissions.set(static_cast<size_t>(ClanPermission::SET_DESCRIPTION)); + } else if (priv == "motd") { + permissions.set(static_cast<size_t>(ClanPermission::SET_MOTD)); + } else if (priv == "grant") { + permissions.set(static_cast<size_t>(ClanPermission::LEADER_OVERRIDE)); + } else if (priv == "ranks") { + permissions.set(static_cast<size_t>(ClanPermission::MANAGE_RANKS)); + } else if (priv == "title" || priv == "settitle") { + // Title setting is deprecated, ignore + } else if (priv == "enroll") { + permissions.set(static_cast<size_t>(ClanPermission::INVITE_MEMBERS)); + } else if (priv == "expel") { + permissions.set(static_cast<size_t>(ClanPermission::KICK_MEMBERS)); + } else if (priv == "promote") { + permissions.set(static_cast<size_t>(ClanPermission::PROMOTE_MEMBERS)); + } else if (priv == "demote") { + permissions.set(static_cast<size_t>(ClanPermission::DEMOTE_MEMBERS)); + } else if (priv == "appfee" || priv == "setappfee") { + permissions.set(static_cast<size_t>(ClanPermission::SET_APP_FEES)); + } else if (priv == "applev" || priv == "setapplev") { + permissions.set(static_cast<size_t>(ClanPermission::SET_APP_LEVEL)); + } else if (priv == "dues" || priv == "setdues") { + permissions.set(static_cast<size_t>(ClanPermission::SET_DUES)); + } else if (priv == "deposit") { + permissions.set(static_cast<size_t>(ClanPermission::DEPOSIT_FUNDS)); + } else if (priv == "withdraw") { + permissions.set(static_cast<size_t>(ClanPermission::WITHDRAW_FUNDS)); + } else if (priv == "store") { + permissions.set(static_cast<size_t>(ClanPermission::STORE_ITEMS)); + } else if (priv == "retrieve") { + permissions.set(static_cast<size_t>(ClanPermission::RETRIEVE_ITEMS)); + } else if (priv == "alts" || priv == "setalt") { + permissions.set(static_cast<size_t>(ClanPermission::MANAGE_ALTS)); + } else if (priv == "chat") { + permissions.set(static_cast<size_t>(ClanPermission::CLAN_CHAT)); } } - char_printf(ch, "You are no longer snooping {}.\n", clan->name); - } else { - CREATE(snoop, ClanSnoop, 1); - snoop->clan = clan; - snoop->next = GET_CLAN_SNOOP(ch); - GET_CLAN_SNOOP(ch) = snoop; - char_printf(ch, "You are now snooping {}.\n", clan->name); } - } else - char_printf(ch, "'{}' does not refer to a valid clan.\n", arg); -} - -static void send_clan_who_line(CharData *ch, const ClanMembership *member) { - long num; - char level_buf[4]; - char alt_buf[50]; - char logon_buf[30]; - const char *level, *title, *last_logon, *name_color; - - num = get_ptable_by_name(member->name); - if (num >= 0) { - snprintf(level_buf, sizeof(level_buf), "%d", player_table[num].level); - level = level_buf; - strftime(logon_buf, sizeof(logon_buf), "%a, %d %b %Y %H:%M", localtime(&player_table[num].last)); - last_logon = logon_buf; - } else { - level = "??"; - last_logon = ""; + + // Special case: if a rank has 'grant' privilege, give it LEADER_OVERRIDE + // This handles legacy admin ranks that may be missing some newer permissions + if (permissions.test(static_cast<size_t>(ClanPermission::LEADER_OVERRIDE))) { + // Leader override allows access to all clan commands + } + + ranks.emplace_back(title, permissions); + } + + // Create the clan and add it to the repository + auto clan = std::make_shared<Clan>(clan_id, clan_name, clan_abbr); + + // Set clan properties using admin methods + clan->admin_set_dues(dues); + clan->admin_set_app_fee(app_fee); + clan->admin_set_min_application_level(min_app_level); + if (auto result = clan->admin_add_treasure(treasure); !result) { + log(LogSeverity::Warn, LVL_IMMORT, "Failed to add treasure to clan {}: {}", clan_name, result.error()); + } + if (!clan_motd.empty()) { + clan->admin_set_motd(clan_motd); + } + + // Add ranks + for (auto& rank : ranks) { + if (auto result = clan->admin_add_rank(std::move(rank)); !result) { + log(LogSeverity::Warn, LVL_IMMORT, "Failed to add rank to clan {}: {}", clan_name, result.error()); + } } - - if (IS_MEMBER_RANK(member->rank)) - title = member->clan->ranks[member->rank - 1].title; - else if (IS_APPLICANT_RANK(member->rank)) - title = "(applicant)"; - else if (IS_ALT_RANK(member->rank)) { - snprintf(alt_buf, sizeof(alt_buf), "(%s's alt)", member->relation.member->name); - title = alt_buf; - } else - title = ""; - - if (IS_ALT_RANK(member->rank)) - name_color = AFYEL; - else if (member->player) - name_color = AFGRN; - else - name_color = ""; - - char_printf(ch, fmt::format("{:>3s} {}{:15} {:25s} {}\n", level, name_color, member->name, ellipsis(title, 25), - last_logon)); + + // Add members + for (const auto& member : members) { + if (!clan->add_member_by_name(member.name, member.rank_index, member.join_time, member.alts)) { + log(LogSeverity::Warn, LVL_IMMORT, "Failed to add member {} to clan {}", member.name, clan_name); + } + } + + // Add to repository + clans_[clan_id] = clan; + caches_valid_ = false; + + return true; } -static void send_clan_who_header(CharData *ch) { - char_printf(ch, - "Lvl " ANRM "Name " ANRM "Rank " ANRM "Last Login " ANRM "\n"); +// Clan snoop management functions +void add_clan_snoop(CharData *ch, ClanID clan_id) { + if (!ch) return; + clan_snoop_table[clan_id].insert(ch); } -CLANCMD(clan_who) { - DescriptorData *d; - CharData *tch; - bool found = false; - - char_printf(ch, AHYEL "Members in " ANRM "{}" AHYEL ":" ANRM "\n", clan->name); - - for (d = descriptor_list; d; d = d->next) { - if (!IS_PLAYING(d)) - continue; - tch = d->character; - if (!CAN_SEE(ch, tch) || clan != GET_CLAN(tch)) - continue; - if (IS_CLAN_MEMBER(tch) || IS_CLAN_ALT(tch)) { - if (!found) { - send_clan_who_header(ch); - found = true; - } - send_clan_who_line(ch, GET_CLAN_MEMBERSHIP(tch)); +void remove_clan_snoop(CharData *ch, ClanID clan_id) { + if (!ch) return; + auto it = clan_snoop_table.find(clan_id); + if (it != clan_snoop_table.end()) { + it->second.erase(ch); + if (it->second.empty()) { + clan_snoop_table.erase(it); } } +} - for (member = clan->members; member && IS_MEMBER_RANK(member->rank); member = member->next) - if (!member->player || !member->player->desc) { - if (!found) { - send_clan_who_header(ch); - found = true; - } - send_clan_who_line(ch, member); - } - - if (!clan->member_count) - char_printf(ch, " None!\n"); - - if (clan->applicant_count) { - found = false; - char_printf(ch, AHYEL "\nApplicants to " ANRM "{}" AHYEL ":" ANRM "\n", clan->name); - for (member = clan->applicants; member && IS_APPLICANT_RANK(member->rank); member = member->next) { - if (!found) { - send_clan_who_header(ch); - found = true; - } - send_clan_who_line(ch, member); +void remove_all_clan_snoops(CharData *ch) { + if (!ch) return; + for (auto it = clan_snoop_table.begin(); it != clan_snoop_table.end();) { + it->second.erase(ch); + if (it->second.empty()) { + it = clan_snoop_table.erase(it); + } else { + ++it; } } } -/* Clan command permission modes */ -#define NONE (1 << 0) -#define PRIV (1 << 1) /* RANK and PRIV are mutually exclusive */ -#define ADMIN (1 << 2) /* because the clan_subcommand data field */ -#define RANK (1 << 3) /* holds either the minimum rank for RANK */ -#define SUPER (1 << 4) /* or the privilege for PRIV, not both */ - -/* Clan command argument modes */ -#define IGNORE 0 -#define CLAN (1 << 0) /* CLAN and MEMBER are not mutually */ -#define MEMBER (1 << 1) /* exclusive but if they are encountered */ -#define REPEAT (1 << 2) /* together, the clan is parsed first */ -#define APPLICANT (1 << 3) - -/* Clan command groupings - for aesthetics only */ -#define GENERAL 0 -#define MGMT 1 /* Hack alert: the PRIV/ADMIN constants */ -#define PRIV (1 << 1) /* are already used above for a bitfield */ -#define MODIFY 3 /* but for convience we're reusing them */ -#define ADMIN (1 << 2) /* here as their actual values of 2 and 4 */ -#define NUM_GROUPS 5 - -static const char *clan_cmdgroup[NUM_GROUPS] = {"General", "Management", "Privileged", "Modifier", "Administrative"}; - -/* Clan command information structure */ -static const struct clan_subcommand { - const char *name; - unsigned int group; - unsigned int type; - unsigned int data; - unsigned int args; - const char *more_args; - CLANCMD(*handler); -} commands[] = { - /* KEEP THIS LIST ALPHABETIZED */ - {"abbr", ADMIN, ADMIN, 0, IGNORE | REPEAT, "<name>", clan_set}, - {"abbr", ADMIN, SUPER, 0, CLAN | REPEAT, "<name>", clan_set}, - {"addrank", MODIFY, PRIV | ADMIN, CPRIV_RANKS, IGNORE | REPEAT, "", clan_set}, - {"addrank", MODIFY, SUPER, CPRIV_RANKS, CLAN | REPEAT, "", clan_set}, - {"alt", PRIV, PRIV, CPRIV_ALTS, IGNORE, "<player>", clan_alt}, - {"alt", PRIV, ADMIN | SUPER, 0, MEMBER, "<player>", clan_alt}, - {"appfee", MODIFY, PRIV | ADMIN, CPRIV_APP_FEE, IGNORE | REPEAT, "<platinum>", clan_set}, - {"appfee", MODIFY, SUPER, CPRIV_APP_FEE, CLAN | REPEAT, "<platinum>", clan_set}, - {"applev", MODIFY, PRIV | ADMIN, CPRIV_APP_LEV, IGNORE | REPEAT, "<level>", clan_set}, - {"applev", MODIFY, SUPER, CPRIV_APP_LEV, CLAN | REPEAT, "<level>", clan_set}, - {"apply", GENERAL, NONE, 0, CLAN, "", clan_apply}, - {"create", ADMIN, SUPER, 0, IGNORE, "<abbr>", clan_create}, - {"delrank", MODIFY, PRIV | ADMIN, CPRIV_RANKS, IGNORE | REPEAT, "", clan_set}, - {"delrank", MODIFY, SUPER, CPRIV_RANKS, CLAN | REPEAT, "", clan_set}, - {"demote", MGMT, PRIV | ADMIN | SUPER, CPRIV_DEMOTE, MEMBER | REPEAT, "", update_clan_rank}, - {"deposit", GENERAL, RANK, MIN_ALT_RANK, REPEAT, "<money>", clan_bank}, - {"deposit", GENERAL, SUPER, 0, CLAN | REPEAT, "<money>", clan_bank}, - {"desc", MODIFY, PRIV | ADMIN, CPRIV_DESC, IGNORE | REPEAT, "", clan_edit}, - {"desc", MODIFY, SUPER, CPRIV_DESC, CLAN | REPEAT, "", clan_edit}, - {"destroy", ADMIN, SUPER, 0, CLAN, "", clan_destroy}, - {"dues", MODIFY, PRIV | ADMIN, CPRIV_DUES, IGNORE | REPEAT, "<platinum>", clan_set}, - {"dues", MODIFY, SUPER, CPRIV_DUES, CLAN | REPEAT, "<platinum>", clan_set}, - {"enroll", MGMT, PRIV | ADMIN | SUPER, CPRIV_ENROLL, APPLICANT, "", clan_enroll}, - {"expel", MGMT, PRIV | ADMIN | SUPER, CPRIV_EXPEL, MEMBER, "", clan_expel}, - {"grant", MGMT, PRIV | ADMIN, CPRIV_GRANT, IGNORE | REPEAT, "<rank> <priv>", clan_priv}, - {"grant", MGMT, SUPER, CPRIV_GRANT, CLAN | REPEAT, "<rank> <priv>", clan_priv}, - {"info", GENERAL, RANK, RANK_NONE, IGNORE, "[<clan>]", clan_info}, - {"list", GENERAL, RANK, RANK_NONE, IGNORE, "", clan_list}, - {"motd", MODIFY, PRIV | ADMIN, CPRIV_MOTD, IGNORE | REPEAT, "", clan_edit}, - {"motd", MODIFY, SUPER, CPRIV_MOTD, CLAN | REPEAT, "", clan_edit}, - {"name", ADMIN, ADMIN, 0, IGNORE | REPEAT, "<name>", clan_set}, - {"name", ADMIN, SUPER, 0, CLAN | REPEAT, "<name>", clan_set}, - {"promote", MGMT, PRIV | ADMIN | SUPER, CPRIV_PROMOTE, MEMBER | REPEAT, "", update_clan_rank}, - {"quit", GENERAL, RANK, RANK_APPLICANT, IGNORE, "", clan_quit}, - {"reject", MGMT, PRIV | ADMIN | SUPER, CPRIV_ENROLL, APPLICANT, "", clan_reject}, - {"revoke", MGMT, PRIV | ADMIN, CPRIV_GRANT, IGNORE | REPEAT, "<rank> <privilege>", clan_priv}, - {"revoke", MGMT, SUPER, CPRIV_GRANT, CLAN | REPEAT, "<rank> <privilege>", clan_priv}, - {"snoop", GENERAL, SUPER, 0, IGNORE, "{off | all | <clan>}", clan_snoop}, - {"status", GENERAL, RANK, RANK_REJECT, IGNORE, "", clan_status}, - {"status", GENERAL, SUPER, 0, IGNORE, "[<player>]", clan_status}, - {"tell", GENERAL, PRIV | ADMIN, CPRIV_CHAT, IGNORE, "<message>", clan_tell}, - {"tell", GENERAL, SUPER, CPRIV_CHAT, CLAN, "<message>", clan_tell}, - {"title", MODIFY, PRIV | ADMIN, CPRIV_TITLE, IGNORE | REPEAT, "<rank> <title>", clan_set}, - {"title", MODIFY, SUPER, CPRIV_TITLE, CLAN | REPEAT, "<rank> <title>", clan_set}, - {"who", GENERAL, RANK, MIN_ALT_RANK, IGNORE, "", clan_who}, - {"who", GENERAL, SUPER, 0, CLAN, "", clan_who}, - {"withdraw", PRIV, PRIV | ADMIN, CPRIV_WITHDRAW, REPEAT, "<money>", clan_bank}, - {"withdraw", PRIV, SUPER, 0, CLAN | REPEAT, "<money>", clan_bank}, - {0, 0, 0, 0, 0, 0, 0}, -}; - -static bool can_use_clan_command(CharData *ch, const clan_subcommand *command) { - if IS_SET (command->type, NONE) - if (GET_CLAN_RANK(ch) == RANK_NONE && !IS_CLAN_SUPERADMIN(ch)) - return true; - if (IS_SET(command->type, RANK)) - if (!OUTRANKS(command->data, GET_CLAN_RANK(ch))) - return true; - if (IS_SET(command->type, PRIV)) - if (HAS_CLAN_PRIV(ch, command->data)) - return true; - if (IS_SET(command->type, ADMIN)) - if (IS_CLAN_ADMIN(ch)) - return true; - if (IS_SET(command->type, SUPER)) - if (IS_CLAN_SUPERADMIN(ch)) - return true; +bool is_snooping_clan(CharData *ch, ClanID clan_id) { + if (!ch) return false; + auto it = clan_snoop_table.find(clan_id); + if (it != clan_snoop_table.end()) { + return it->second.count(ch) > 0; + } return false; } -static const clan_subcommand *determine_command(CharData *ch, const char *cmd) { - const clan_subcommand *command = nullptr; - - for (command = commands; command->name; ++command) { - if (*command->name < *cmd) - continue; - else if (*command->name > *cmd) - break; - else if (is_abbrev(cmd, command->name)) - if (can_use_clan_command(ch, command)) - return command; +std::vector<ClanID> get_snooped_clans(CharData *ch) { + std::vector<ClanID> result; + if (!ch) return result; + + for (const auto& [clan_id, snoops] : clan_snoop_table) { + if (snoops.count(ch) > 0) { + result.push_back(clan_id); + } } - - return nullptr; + return result; } -ACMD(do_clan) { - const clan_subcommand *command = nullptr; - Clan *clan; - ClanMembership *member; - char arg[MAX_INPUT_LENGTH]; - unsigned int group; - - if (IS_NPC(ch) || !ch->desc) { - char_printf(ch, HUH); - return; +// Function for use with function registry - checks clan permission or god status +bool has_clan_permission_or_god(const CharData *ch, ClanPermission permission) { + // Null character check + if (!ch) { + return false; } + + // Gods have all permissions + if (GET_LEVEL(ch) >= LVL_IMMORT) { + return true; + } + return has_clan_permission(ch, permission); +} - clan = GET_CLAN(ch); - member = GET_CLAN_MEMBERSHIP(ch); - - /* Determine which command to invoke */ - argument = any_one_arg(argument, arg); - if (strlen(arg) >= 3) - if ((command = determine_command(ch, arg))) { - if (IS_SET(command->args, CLAN)) { - argument = any_one_arg(argument, arg); - if (!*arg) { - char_printf(ch, "Which clan?\n"); - return; - } else if (!(clan = find_clan(arg))) { - char_printf(ch, "'{}' does not refer to a valid clan.\n", arg); - return; +// Modern C++23 permission checking implementation +namespace clan_permissions { + + PermissionResult check_permission(const CharData *ch, ClanPermission permission) { + using enum ClanPermission; + + // Null character check + if (!ch) { + return std::unexpected(PermissionError{"Invalid character.", permission}); + } + + // Check if character has a clan + auto clan = get_clan(ch); + if (!clan) { + return std::unexpected(PermissionError{"You are not a member of any clan.", permission}); + } + + // Get character's rank + auto rank = get_clan_rank(ch); + if (!rank) { + return std::unexpected(PermissionError{"Unable to determine your clan rank.", permission}); + } + + // Check permission + if (!rank->has_permission(permission)) { + auto permission_name = std::string(magic_enum::enum_name(permission)); + return std::unexpected(PermissionError{ + fmt::format("You need the '{}' permission to do that.", permission_name), + permission, true + }); + } + + return {}; // Success + } + + PermissionResult check_clan_member(const CharData *ch) { + auto clan = get_clan(ch); + if (!clan) { + return std::unexpected(PermissionError{"You are not a member of any clan."}); + } + + auto member = get_clan_member(ch); + if (!member) { + return std::unexpected(PermissionError{"Unable to verify your clan membership."}); + } + + return {}; // Success + } + + // Simple permission checking helpers + bool check_god_override(const CharData *ch) { + return GET_LEVEL(ch) >= LVL_IMMORT; + } + + // Enhanced permission checking with detailed error messages + std::string get_permission_error_message(ClanPermission permission) { + auto permission_name = std::string(magic_enum::enum_name(permission)); + return fmt::format("You need the '{}' permission to do that.", permission_name); + } + + // Command wrapper functions (simpler than templates) + bool execute_with_clan_permission(CharData *ch, ClanPermission permission, + std::function<void()> command_func) { + // Gods bypass all permission checks + if (check_god_override(ch)) { + command_func(); + return true; + } + + // Check permission + auto result = check_permission(ch, permission); + if (!result) { + char_printf(ch, "{}", result.error().reason); + return false; + } + + // Execute command + command_func(); + return true; + } + + bool execute_with_clan_membership(CharData *ch, std::function<void()> command_func) { + // Gods bypass all permission checks + if (check_god_override(ch)) { + command_func(); + return true; + } + + // Check clan membership + auto result = check_clan_member(ch); + if (!result) { + char_printf(ch, "{}", result.error().reason); + return false; + } + + // Execute command + command_func(); + return true; + } + +} // namespace clan_permissions + + + +// Legacy permission conversion for existing clan files +namespace legacy_conversion { + + // Convert old ClanPrivilege values to new ClanPermission values + std::optional<ClanPermission> convert_legacy_privilege(int legacy_value) { + switch (legacy_value) { + case 1: return ClanPermission::SET_DESCRIPTION; // Description + case 2: return ClanPermission::SET_MOTD; // Motd + case 3: return ClanPermission::LEADER_OVERRIDE; // Grant (legacy admin) + case 4: return ClanPermission::MANAGE_RANKS; // Ranks + case 5: return ClanPermission::NONE; // Title (deprecated) + case 6: return ClanPermission::INVITE_MEMBERS; // Enroll + case 7: return ClanPermission::KICK_MEMBERS; // Expel + case 8: return ClanPermission::PROMOTE_MEMBERS; // Promote + case 9: return ClanPermission::DEMOTE_MEMBERS; // Demote + case 10: return ClanPermission::SET_APP_FEES; // App_Fees + case 11: return ClanPermission::SET_APP_LEVEL; // App_Level + case 12: return ClanPermission::SET_DUES; // Dues + case 13: return ClanPermission::DEPOSIT_FUNDS; // Deposit + case 14: return ClanPermission::WITHDRAW_FUNDS; // Withdraw + case 15: return ClanPermission::STORE_ITEMS; // Store + case 16: return ClanPermission::RETRIEVE_ITEMS; // Retrieve + case 17: return ClanPermission::MANAGE_ALTS; // Alts + case 18: return ClanPermission::CLAN_CHAT; // Chat + default: return std::nullopt; + } + } + + // Convert legacy bitset to new permission set + PermissionSet convert_legacy_permissions(const std::bitset<64>& legacy_bits) { + PermissionSet new_permissions; + + // Convert each bit position + for (size_t i = 0; i < 64 && i < NUM_PERMISSIONS; ++i) { + if (legacy_bits.test(i)) { + auto new_perm = convert_legacy_privilege(static_cast<int>(i)); + if (new_perm) { + new_permissions.set(static_cast<size_t>(*new_perm)); } } - if (IS_SET(command->args, MEMBER | APPLICANT)) { - argument = any_one_arg(argument, arg); - cap_by_color(arg); - if (!*arg) { - char_printf(ch, "Whom do you want to {}?\n", arg); - return; - } else if (!(member = find_clan_membership(arg))) { - char_printf(ch, "{} is not a member or applicant of any clan.\n", arg); - return; - } else if (IS_SET(command->args, APPLICANT) && member->rank != RANK_APPLICANT) { - char_printf(ch, "{} is not an applicant of any clan.\n", arg); - return; - } else if (!IS_CLAN_SUPERADMIN(ch) && GET_CLAN(ch) != member->clan) { - char_printf(ch, "{} is not a member of your clan.\n", arg); - return; - } - if (!IS_SET(command->args, CLAN)) - clan = member->clan; + } + + return new_permissions; + } + +} // namespace legacy_conversion + +// Security and validation utilities implementation +namespace clan_security { + + ValidationResult validate_clan_name(std::string_view name) { + if (name.empty()) { + return std::unexpected(ValidationError{"Clan name cannot be empty.", "name", std::string(name)}); + } + if (name.length() > MAX_CLAN_NAME_LENGTH) { + return std::unexpected(ValidationError{ + fmt::format("Clan name too long (max {} characters).", MAX_CLAN_NAME_LENGTH), + "name", std::string(name) + }); + } + if (contains_unsafe_characters(name)) { + return std::unexpected(ValidationError{"Clan name contains invalid characters.", "name", std::string(name)}); + } + return {}; + } + + ValidationResult validate_clan_abbreviation(std::string_view abbr) { + if (abbr.empty()) { + return std::unexpected(ValidationError{"Clan abbreviation cannot be empty.", "abbreviation", std::string(abbr)}); + } + if (abbr.length() > MAX_CLAN_ABBR_LENGTH) { + return std::unexpected(ValidationError{ + fmt::format("Clan abbreviation too long (max {} characters).", MAX_CLAN_ABBR_LENGTH), + "abbreviation", std::string(abbr) + }); + } + if (contains_unsafe_characters(abbr)) { + return std::unexpected(ValidationError{"Clan abbreviation contains invalid characters.", "abbreviation", std::string(abbr)}); + } + return {}; + } + + ValidationResult validate_clan_description(std::string_view desc) { + if (desc.length() > MAX_CLAN_DESCRIPTION_LENGTH) { + return std::unexpected(ValidationError{ + fmt::format("Clan description too long (max {} characters).", MAX_CLAN_DESCRIPTION_LENGTH), + "description", std::string(desc) + }); + } + return {}; // Descriptions can be empty and have more flexible character restrictions + } + + ValidationResult validate_player_name(std::string_view name) { + if (name.empty()) { + return std::unexpected(ValidationError{"Player name cannot be empty.", "player_name", std::string(name)}); + } + if (name.length() > 20) { // Standard player name limit + return std::unexpected(ValidationError{"Player name too long.", "player_name", std::string(name)}); + } + // Basic character validation for player names + for (char c : name) { + if (!std::isalnum(c) && c != '_' && c != '-') { + return std::unexpected(ValidationError{ + "Player name contains invalid characters (alphanumeric, _, - only).", + "player_name", std::string(name) + }); } - if (IS_SET(command->args, REPEAT)) { - strcat(arg, argument); - argument = arg; - } else - skip_spaces(&argument); - - command->handler(ch, member, clan, argument); - return; } - - for (group = 0; group < NUM_GROUPS; ++group) { - strcpy(buf, "--------------------------------------------------------------------"); - sprintf(buf1, "[ %s commands ]", clan_cmdgroup[group]); - strncpy(buf + 2, buf1, strlen(buf1)); - for (command = commands; command->name; ++command) { - if (group != command->group) - continue; - if (can_use_clan_command(ch, command)) { - if (*buf) { - char_printf(ch, "\n{}", buf); - *buf = '\0'; + return {}; + } + + CharData* find_player_safe(std::string_view name) { + // Validate input first + auto validation = validate_player_name(name); + if (!validation) { + return nullptr; + } + + // Create a safe copy for the legacy API + std::string name_copy(name); + return find_char_in_world(find_by_name(name_copy.data())); + } + + CharData* find_clan_member_safe(std::string_view name) { + // Same implementation as find_player_safe for now + // Could be extended with clan-specific validation + return find_player_safe(name); + } + + bool check_member_limit(const Clan& clan) { + return clan.member_count() < MAX_CLAN_MEMBERS; + } + + bool check_rank_limit(const Clan& clan) { + return clan.rank_count() < MAX_CLAN_RANKS; + } + + bool check_storage_limit(const Clan& clan) { + return clan.storage().size() < MAX_CLAN_STORAGE_ITEMS; + } + + std::expected<Money, std::string> safe_add_money(const Money& base, const Money& addition) { + Money result = base; + + // Check for overflow in each denomination + for (int denom = COPPER; denom <= PLATINUM; ++denom) { + if (base[denom] > 0 && addition[denom] > 0) { + // Check if addition would cause overflow + if (base[denom] > std::numeric_limits<int>::max() - addition[denom]) { + return std::unexpected("Money addition would cause overflow."); } - char_printf(ch, "\n clan {:<8s}", command->name); - if (IS_SET(command->args, CLAN)) - char_printf(ch, " <clan>"); - if (IS_SET(command->args, APPLICANT)) - char_printf(ch, " <applicant>"); - else if (IS_SET(command->args, MEMBER)) - char_printf(ch, " <member>"); - if (command->more_args) - char_printf(ch, " {}", command->more_args); } + result[denom] = base[denom] + addition[denom]; + } + + return result; + } + + std::expected<Money, std::string> safe_subtract_money(const Money& base, const Money& subtraction) { + Money result = base; + + // Check for underflow in each denomination + for (int denom = COPPER; denom <= PLATINUM; ++denom) { + if (base[denom] < subtraction[denom]) { + return std::unexpected("Insufficient coins for withdrawal."); + } + result[denom] = base[denom] - subtraction[denom]; } + + return result; + } + + std::expected<int, std::string> safe_add_quantity(int base, int addition) { + if (base > 0 && addition > 0) { + if (base > std::numeric_limits<int>::max() - addition) { + return std::unexpected("Quantity addition would cause overflow."); + } + } + return base + addition; + } + + bool contains_unsafe_characters(std::string_view input) { + // Basic safety check - reject null bytes and some dangerous chars + for (char c : input) { + if (c == '\0' || c == '<' || c == '>' || c == '&' || c == '"' || c == '\'') { + return true; + } + } + return false; } - char_printf(ch, "\n"); -} - -ACMD(do_ctell) { - CharData *me = REAL_CHAR(ch); - Clan *clan = GET_CLAN(me); - if (IS_CLAN_SUPERADMIN(me)) { - /* Only snooping one clan: auto send to that one */ - if (GET_CLAN_SNOOP(me) && !GET_CLAN_SNOOP(me)->next) - clan_tell(ch, nullptr, GET_CLAN_SNOOP(me)->clan, argument); - else { - argument = any_one_arg(argument, arg); - if (!*arg) - char_printf(ch, "Which clan do you want to talk to?\n"); - else if (!(clan = find_clan(arg))) - char_printf(ch, - "'{}' does not refer to a valid clan.\nYou can " - "only omit the clan if you are snooping just one " - "clan.\n", - arg); - else if (!is_snooping(me, clan)) - char_printf(ch, "You must be snooping {} first.\n", clan->name); - else - clan_tell(ch, nullptr, clan, argument); - } - } else if (!clan || IS_CLAN_REJECT(me)) - char_printf(ch, "You're not part of a clan.\n"); - else if (IS_CLAN_APPLICANT(me)) - char_printf(ch, "You're not part of a clan.\n"); - else if (CAN_DO_PRIV(me, CPRIV_CHAT) || - (IS_CLAN_ALT(me) && MEMBER_CAN(GET_CLAN_MEMBERSHIP(me)->relation.member, CPRIV_CHAT))) - clan_tell(ch, GET_CLAN_MEMBERSHIP(me), clan, argument); - else - char_printf(ch, "You don't have access to clan chat.\n"); -} +} // namespace clan_security \ No newline at end of file diff --git a/src/clan.hpp b/src/clan.hpp index d2376bee..b648866b 100644 --- a/src/clan.hpp +++ b/src/clan.hpp @@ -9,185 +9,753 @@ #pragma once -#include "clan.hpp" +#include "comm.hpp" #include "money.hpp" -#include "privileges.hpp" +#include "pfiles.hpp" #include "structs.hpp" -#include "sysdep.hpp" - -/* Defaults */ -#define ALLOW_CLAN_LINKLOAD true -#define ALLOW_CLAN_ALTS true -#define ALLOW_CLAN_QUIT true -#define BACKUP_CLAN_ON_DELETE true - -#define CPRIV_DESC 0 -#define CPRIV_MOTD 1 -#define CPRIV_GRANT 2 -#define CPRIV_RANKS 3 -#define CPRIV_TITLE 4 -#define CPRIV_ENROLL 5 -#define CPRIV_EXPEL 6 -#define CPRIV_PROMOTE 7 -#define CPRIV_DEMOTE 8 -#define CPRIV_APP_FEE 9 -#define CPRIV_APP_LEV 10 -#define CPRIV_DUES 11 -#define CPRIV_WITHDRAW 12 -#define CPRIV_ALTS 13 -#define CPRIV_CHAT 14 -#define NUM_CLAN_PRIVS 15 /* Number of clan privileges */ - -#define MIN_CLAN_RANKS 2 -#define MAX_CLAN_RANKS 100 -#define RANK_ADMIN 0 -#define RANK_LEADER 1 -#define ALT_RANK_OFFSET MAX_CLAN_RANKS -#define MAX_ALT_RANK (ALT_RANK_OFFSET + RANK_LEADER) -#define MIN_ALT_RANK (ALT_RANK_OFFSET + MAX_CLAN_RANKS) -#define RANK_APPLICANT (ALT_RANK_OFFSET + MAX_CLAN_RANKS + 1) -#define RANK_REJECT (RANK_APPLICANT + 1) -#define RANK_NONE (RANK_REJECT + 1) - -#define OUTRANKS(a, b) ((a) < (b)) - -#define IS_REJECT_RANK(rank) ((rank) == RANK_REJECT) -#define IS_APPLICANT_RANK(rank) ((rank) == RANK_APPLICANT) -#define IS_ALT_RANK(rank) ((rank) > ALT_RANK_OFFSET && (rank) <= ALT_RANK_OFFSET + MAX_CLAN_RANKS) -#define IS_MEMBER_RANK(rank) ((rank) >= RANK_LEADER && (rank) <= MAX_CLAN_RANKS) -#define IS_LEADER_RANK(rank) ((rank) == RANK_LEADER) -#define IS_ADMIN_RANK(rank) ((rank) == RANK_ADMIN) - -#define IS_CLAN_REJECT(ch) IS_REJECT_RANK(GET_CLAN_RANK(ch)) -#define IS_CLAN_APPLICANT(ch) IS_APPLICANT_RANK(GET_CLAN_RANK(ch)) -#define IS_CLAN_MEMBER(ch) IS_MEMBER_RANK(GET_CLAN_RANK(ch)) -#define IS_CLAN_ALT(ch) IS_ALT_RANK(GET_CLAN_RANK(ch)) -#define IS_CLAN_LEADER(ch) IS_LEADER_RANK(GET_CLAN_RANK(ch)) -#define IS_CLAN_ADMIN(ch) IS_ADMIN_RANK(GET_CLAN_RANK(ch)) -#define IS_CLAN_SUPERADMIN(ch) PRV_FLAGGED(ch, PRV_CLAN_ADMIN) - -#define DEFAULT_APP_LVL 25 -#define MAX_CLAN_DESC_LENGTH 5000 -#define REJECTION_WAIT_DAYS 5 -#define CLAN_SNOOP_OFF 0 -#define MAX_CLAN_ABBR_LEN 10 -#define MAX_CLAN_NAME_LEN 30 -#define MAX_CLAN_TITLE_LEN 30 - -struct ClanMembership; -struct ClanRank { - char *title; - flagvector privileges[FLAGVECTOR_SIZE(NUM_CLAN_PRIVS)]; + +#include <bitset> +#include <ctime> +#include <expected> +#include <filesystem> +#include <fmt/format.h> +#include <fstream> +#include <functional> +#include <magic_enum/magic_enum.hpp> +#include <map> +#include <memory> +#include <nlohmann/json.hpp> +#include <optional> +#include <ranges> +#include <span> +#include <string> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +// Forward declarations +class CharData; + + + +// Error Messages with string for user. +struct AccessError { + static constexpr std::string_view ClanNotFound = "Clan not found."; + static constexpr std::string_view PermissionDenied = "You do not have permission to do that."; + static constexpr std::string_view InvalidOperation = "Invalid operation"; }; -struct Clan { - /* Identifying characteristics */ - unsigned int number; - char *name; - char *abbreviation; - char *description; - char *motd; +enum class ClanPermission : std::uint32_t { + // Basic member permissions (0-10) + NONE = 0, + CLAN_CHAT = 1, // Send messages to clan channel + CLAN_WHO = 2, // View clan member list + VIEW_MOTD = 3, // View clan message of the day + VIEW_RANKS = 4, // View clan rank structure + + // Invitation and membership management (11-20) + INVITE_MEMBERS = 11, // Invite new members + KICK_MEMBERS = 12, // Remove members from clan + PROMOTE_MEMBERS = 13, // Promote members to higher ranks + DEMOTE_MEMBERS = 14, // Demote members to lower ranks + MANAGE_ALTS = 15, // Add/remove alt characters + + // Clan administration (21-30) + MANAGE_RANKS = 21, // Create/modify/delete ranks + SET_MOTD = 22, // Set clan message of the day + SET_DESCRIPTION = 23, // Set clan description + SET_DUES = 24, // Set clan dues amounts + SET_APP_FEES = 25, // Set application fees + SET_APP_LEVEL = 26, // Set minimum application level + + // Financial management (31-40) + VIEW_FINANCES = 31, // View clan treasury and financial info + DEPOSIT_FUNDS = 32, // Add money to clan treasury + WITHDRAW_FUNDS = 33, // Remove money from clan treasury + MANAGE_BANK = 34, // Set bank room and access rules + + // Storage and equipment (41-50) + VIEW_STORAGE = 41, // View clan storage contents + STORE_ITEMS = 42, // Add items to clan storage + RETRIEVE_ITEMS = 43, // Remove items from clan storage + MANAGE_STORAGE = 44, // Set storage room and access rules + + // Special privileges (51-60) + LEADER_OVERRIDE = 51, // Can use any clan command regardless of other permissions + CLAN_ADMIN = 52, // Administrative powers (gods only) + + // Marker for bitset size + MAX_PERMISSIONS, +}; - /* Status variables */ - unsigned int dues; - unsigned int app_fee; - unsigned int app_level; - unsigned int power; - int treasure[NUM_COIN_TYPES]; +constexpr size_t NUM_PERMISSIONS = static_cast<size_t>(ClanPermission::MAX_PERMISSIONS); - /* Lists */ - size_t rank_count; /* array length */ - ClanRank *ranks; /* dynamically-allocated array */ - ClanMembership *people; /* linked list */ - size_t people_count; /* cached total number of people */ +// Forward declarations +class Clan; + +// Type aliases for better readability +using ClanID = unsigned int; +using CharacterPtr = std::shared_ptr<CharData>; +using ClanPtr = std::shared_ptr<Clan>; +using WeakCharacterPtr = std::weak_ptr<CharData>; +using WeakClanPtr = std::weak_ptr<Clan>; +using PermissionSet = std::bitset<NUM_PERMISSIONS>; +using ObjectId = unsigned int; // VNUM of the object +using Storage = std::unordered_map<ObjectId, int>; + +// Structure to store member information persistently +struct ClanMember { + std::string name; + int rank_index; + time_t join_time; + std::vector<std::string> alts; + + ClanMember() = default; + ClanMember(std::string n, int r, time_t t, std::vector<std::string> a = {}) + : name(std::move(n)), rank_index(r), join_time(t), alts(std::move(a)) {} +}; + +class ClanRank { + private: + std::string title_; + PermissionSet privileges_; - ClanMembership *members; /* start of members in people list */ - size_t member_count; /* cached number of members */ + public: + ClanRank(std::string title, PermissionSet privileges) + : title_(std::move(title)), privileges_(std::move(privileges)) {} - ClanMembership *admins; /* start of admins in people list */ - size_t admin_count; /* cached number of admins */ + // Default constructor for deserialization + ClanRank() = default; - ClanMembership *applicants; /* start of applicants in people list */ - size_t applicant_count; /* cached number of applicants */ + [[nodiscard]] std::string_view title() const { return title_; } + [[nodiscard]] PermissionSet privileges() const { return privileges_; } + [[nodiscard]] bool has_permission(ClanPermission permission) const { + // CLAN_ADMIN grants all privileges + if (permission == ClanPermission::CLAN_ADMIN) { + return privileges_.test(static_cast<size_t>(ClanPermission::CLAN_ADMIN)); + } + // LEADER_OVERRIDE allows any clan command + if (privileges_.test(static_cast<size_t>(ClanPermission::LEADER_OVERRIDE))) { + return true; + } + // Normal permission check + if (static_cast<size_t>(permission) >= NUM_PERMISSIONS) { + return false; + } + return privileges_.test(static_cast<size_t>(permission)); + } - ClanMembership *rejects; /* start of rejects in people list */ - size_t reject_count; /* cached number of rejects */ + // Add setter methods + void set_title(std::string title) { title_ = std::move(title); } + void set_privileges(PermissionSet privileges) { privileges_ = std::move(privileges); } + + void set_permission(ClanPermission permission, bool value) { + if (static_cast<size_t>(permission) < NUM_PERMISSIONS) { + privileges_.set(static_cast<size_t>(permission), value); + } + } + void add_permission(ClanPermission permission) { + if (static_cast<size_t>(permission) < NUM_PERMISSIONS) { + privileges_.set(static_cast<size_t>(permission)); + } + } + void remove_permission(ClanPermission permission) { + if (static_cast<size_t>(permission) < NUM_PERMISSIONS) { + privileges_.reset(static_cast<size_t>(permission)); + } + } + + bool operator==(const ClanRank &other) const { return title_ == other.title_ && privileges_ == other.privileges_; } + bool operator<(const ClanRank &other) const { + if (title_ != other.title_) + return title_ < other.title_; + return privileges_.to_string() < other.privileges_.to_string(); + } }; -struct ClanMembership { - char *name; - unsigned int rank; - time_t since; - union { - ClanMembership *alts; - ClanMembership *member; - } relation; - ClanMembership *next; - Clan *clan; - CharData *player; +// Clan class with restricted direct access +class Clan : public std::enable_shared_from_this<Clan> { + private: + ClanID id_; + std::string name_; + std::string abbreviation_; + std::string description_; + std::string motd_; + + unsigned int dues_; + unsigned int app_fee_; + unsigned int min_application_level_; + Money treasure_; + Storage storage_; + + // Room vnums for clan bank, chest, and hall access (NOWHERE = any room) + room_num bank_room_; + room_num chest_room_; + room_num hall_room_; + + std::vector<ClanRank> ranks_; + + // Store members by name and rank + std::vector<ClanMember> members_; + + // Cache for performance (mutable to allow modification in const methods) + mutable std::map<ClanRank, std::vector<CharacterPtr>> rank_cache_; + mutable bool rank_cache_valid_ = false; + + // Make mutation methods private + void set_name(std::string new_name) { name_ = std::move(new_name); } + void set_abbreviation(std::string new_abbreviation) { abbreviation_ = std::move(new_abbreviation); } + void set_description(std::string new_description) { description_ = std::move(new_description); } + void set_motd(std::string new_motd) { motd_ = std::move(new_motd); } + void set_dues(unsigned int new_dues) { dues_ = new_dues; } + void set_app_fee(unsigned int new_app_fee) { app_fee_ = new_app_fee; } + void set_min_application_level(unsigned int new_level) { min_application_level_ = new_level; } + void set_treasure(Money new_treasure) { treasure_ = std::move(new_treasure); } + void set_bank_room(room_num room) { bank_room_ = room; } + void set_chest_room(room_num room) { chest_room_ = room; } + void set_hall_room(room_num room) { hall_room_ = room; } + [[nodiscard]] std::expected<void, std::string> add_storage_item(ObjectId id, int amount) { + if (storage_.size() >= 1000) { // Simple limit check + return std::unexpected("Storage limit reached."); + } + + auto it = storage_.find(id); + if (it != storage_.end()) { + // Simple overflow check + if (amount > 0 && it->second > std::numeric_limits<int>::max() - amount) { + return std::unexpected("Quantity addition would cause overflow."); + } + it->second += amount; + return {}; + } else { + if (amount > 0) { + storage_[id] = amount; + return {}; + } + return std::unexpected("Cannot add negative quantity."); + } + } + void remove_storage_item(ObjectId id, int amount) { + auto it = storage_.find(id); + if (it != storage_.end()) { + it->second -= amount; + if (it->second <= 0) { + storage_.erase(it); + } + } + } + + // Method to add money to the clan's treasure with overflow protection + [[nodiscard]] std::expected<void, std::string> add_treasure(const Money &coins) { + // Simple overflow check for each denomination + Money result = treasure_; + for (int denom = PLATINUM; denom <= COPPER; ++denom) { + if (treasure_[denom] > 0 && coins[denom] > 0) { + if (treasure_[denom] > std::numeric_limits<int>::max() - coins[denom]) { + return std::unexpected("Money addition would cause overflow."); + } + } + result[denom] = treasure_[denom] + coins[denom]; + } + treasure_ = result; + return {}; + } + + [[nodiscard]] std::expected<void, std::string> subtract_treasure(const Money &coins) { + // Simple underflow check + for (int denom = PLATINUM; denom <= COPPER; ++denom) { + if (treasure_[denom] < coins[denom]) { + return std::unexpected("Insufficient coins for withdrawal."); + } + } + Money result = treasure_; + for (int denom = PLATINUM; denom <= COPPER; ++denom) { + result[denom] = treasure_[denom] - coins[denom]; + } + treasure_ = result; + return {}; + } + + // Add a character with a rank - now returns success status + bool add_member(CharacterPtr character, int rank_index); + + // Remove a character + void remove_member(const CharacterPtr &character); + + // Cache management + void invalidate_rank_cache() const { rank_cache_valid_ = false; } + void build_rank_cache() const; + + // Allow Repository to access private methods for loading + friend class ClanRepository; + + // Allow test access to private methods + friend class ClanTestFixture; + + public: + Clan(ClanID id, std::string name, std::string abbreviation) + : id_(std::move(id)), name_(std::move(name)), abbreviation_(std::move(abbreviation)), dues_(0), app_fee_(0), + min_application_level_(0), bank_room_(NOWHERE), chest_room_(NOWHERE), hall_room_(NOWHERE) {} + + // Constants + static constexpr int MAX_CLAN_ABBR_LEN = 10; + static constexpr int MAX_CLAN_NAME_LEN = 30; + static constexpr int MAX_CLAN_TITLE_LEN = 30; + + // Public access is read-only + [[nodiscard]] ClanID id() const { return id_; } + [[nodiscard]] std::string_view name() const { return name_; } + [[nodiscard]] std::string_view abbreviation() const { return abbreviation_; } + [[nodiscard]] std::string_view description() const { return description_; } + [[nodiscard]] std::string_view motd() const { return motd_; } + [[nodiscard]] unsigned int dues() const { return dues_; } + [[nodiscard]] unsigned int app_fee() const { return app_fee_; } + [[nodiscard]] unsigned int min_application_level() const { return min_application_level_; } + [[nodiscard]] Money treasure() const { return treasure_; } + [[nodiscard]] const Storage &storage() const { return storage_; } + [[nodiscard]] const std::vector<ClanRank> &ranks() const { return ranks_; } + [[nodiscard]] room_num bank_room() const { return bank_room_; } + [[nodiscard]] room_num chest_room() const { return chest_room_; } + [[nodiscard]] room_num hall_room() const { return hall_room_; } + [[nodiscard]] const std::vector<ClanMember> &members() const { return members_; } + [[nodiscard]] std::size_t member_count() const { return members_.size(); } + [[nodiscard]] std::size_t rank_count() const { return ranks_.size(); } + + // Check if character is a member + [[nodiscard]] bool is_member(const CharacterPtr &character) const; + + // Member management by name + [[nodiscard]] std::optional<ClanMember> get_member_by_name(const std::string_view name) const; + [[nodiscard]] std::vector<ClanMember> get_members_by_rank_index(int rank_index) const; + [[nodiscard]] bool add_member_by_name(const std::string &name, int rank_index, time_t join_time = 0, + std::vector<std::string> alts = {}); + bool remove_member_by_name(const std::string &name); // Remove nodiscard - cleanup operation + [[nodiscard]] bool update_member_rank(const std::string &name, int new_rank_index); + + // Get all members with a specific rank + [[nodiscard]] std::vector<CharacterPtr> get_members_by_rank(const ClanRank &rank) const; + + // Check if a character has a specific permission + [[nodiscard]] bool has_permission(const CharacterPtr &character, ClanPermission permission) const; + + // Grant specific permission to a member + bool grant_permission(const CharacterPtr &character, ClanPermission permission); + + // Administrative methods for gods (bypass membership requirements) + void admin_set_name(std::string new_name) { set_name(std::move(new_name)); } + void admin_set_abbreviation(std::string new_abbreviation) { set_abbreviation(std::move(new_abbreviation)); } + void admin_set_description(std::string new_description) { set_description(std::move(new_description)); } + void admin_set_motd(std::string new_motd) { set_motd(std::move(new_motd)); } + void admin_set_dues(unsigned int new_dues) { set_dues(new_dues); } + void admin_set_app_fee(unsigned int new_app_fee) { set_app_fee(new_app_fee); } + void admin_set_min_application_level(unsigned int new_level) { set_min_application_level(new_level); } + void admin_set_bank_room(room_num room) { set_bank_room(room); } + void admin_set_chest_room(room_num room) { set_chest_room(room); } + void admin_set_hall_room(room_num room) { set_hall_room(room); } + [[nodiscard]] std::expected<void, std::string> admin_add_rank(ClanRank rank) { + if (ranks_.size() >= 20) { // Simple limit check + return std::unexpected("Maximum rank limit reached."); + } + ranks_.push_back(std::move(rank)); + invalidate_rank_cache(); + return {}; + } + bool admin_update_rank_permissions(size_t rank_index, PermissionSet permissions) { + if (rank_index >= ranks_.size()) + return false; + ranks_[rank_index].set_privileges(std::move(permissions)); + return true; + } + [[nodiscard]] std::expected<void, std::string> admin_add_treasure(const Money &coins) { + return add_treasure(coins); + } + [[nodiscard]] std::expected<void, std::string> admin_subtract_treasure(const Money &coins) { + return subtract_treasure(coins); + } + [[nodiscard]] std::expected<void, std::string> admin_add_storage_item(ObjectId id, int amount) { + return add_storage_item(id, amount); + } + void admin_remove_storage_item(ObjectId id, int amount) { + remove_storage_item(id, amount); + } + + // Notify all members of the clan + void notify(const CharacterPtr &skip, const std::string_view str); + void notify(const CharData *skip, const std::string_view str); + template <typename... Args> void notify(const CharacterPtr &skip, std::string_view str, Args &&...args) { + notify(skip, fmt::vformat(str, fmt::make_format_args(args...))); + } + template <typename... Args> void notify(const CharData *skip, std::string_view str, Args &&...args) { + notify(skip, fmt::vformat(str, fmt::make_format_args(args...))); + } + + // Make JSON functions friends of the Clan class + friend void to_json(nlohmann::json &j, const Clan &clan); + friend void from_json(const nlohmann::json &j, Clan &clan); +}; + +// Forward declare JSON serialization functions +void to_json(nlohmann::json &j, const ClanRank &rank); +void from_json(const nlohmann::json &j, ClanRank &rank); +void to_json(nlohmann::json &j, const ClanMember &member); +void from_json(const nlohmann::json &j, ClanMember &member); +void to_json(nlohmann::json &j, const Clan &clan); +void from_json(const nlohmann::json &j, Clan &clan); + +// Repository class for Clan +class ClanRepository { + private: + std::unordered_map<ClanID, ClanPtr> clans_; + // Performance caches for faster lookups + mutable std::unordered_map<std::string, ClanID> name_to_id_; + mutable std::unordered_map<std::string, ClanID> abbr_to_id_; + mutable bool caches_valid_ = false; + + public: + ClanRepository() = default; + + ClanPtr create(ClanID id, std::string name, std::string abbreviation) { + auto clan = std::make_shared<Clan>(std::move(id), std::move(name), std::move(abbreviation)); + clans_[clan->id()] = clan; + caches_valid_ = false; // Invalidate caches + return clan; + } + void remove(ClanID id) { + clans_.erase(id); + caches_valid_ = false; // Invalidate caches + } + + [[nodiscard]] std::optional<ClanPtr> find_by_id(ClanID id) const { + auto it = clans_.find(id); + return it != clans_.end() ? std::optional{it->second} : std::nullopt; + } + [[nodiscard]] std::optional<ClanPtr> find_by_name(const std::string_view name) const; + [[nodiscard]] std::optional<ClanPtr> find_by_abbreviation(const std::string_view abbr) const; + + [[nodiscard]] auto all() const { return clans_ | std::views::values; } + [[nodiscard]] std::size_t count() const { return clans_.size(); } + + constexpr std::string_view default_file_path() const { return "etc/clans/clans.json"; } + std::expected<void, std::string> save() const { return save_to_file(default_file_path()); } + std::expected<void, std::string> save_to_file(const std::filesystem::path &filepath) const; + std::expected<void, std::string> load() { return load_from_file(default_file_path()); } + std::expected<void, std::string> load_from_file(const std::filesystem::path &filepath); + + // Load clan membership for character on login + void load_clan_membership(const CharacterPtr &ch) { + if (!ch || !ch->player_specials) { + return; + } + + // Migration case: if clan_id is CLAN_ID_NONE, try legacy lookup and update clan_id + if (ch->player_specials->clan_id == CLAN_ID_NONE) { + auto clan_id = find_clan_by_member_name(ch); + if (clan_id != CLAN_ID_NONE) { + ch->player_specials->clan_id = clan_id; + // Save immediately to ensure clan membership is persistent + save_player(ch.get()); + } + return; + } + + // Verify player is still in their clan (handles expulsion/clan deletion) + if (!verify_clan_membership(ch)) { + ch->player_specials->clan_id = CLAN_ID_NONE; + } + } + + // Legacy migration: find clan by searching all clans for member name + [[nodiscard]] ClanID find_clan_by_member_name(const CharacterPtr &ch) { + if (!ch || !ch->player.short_descr) { + return CLAN_ID_NONE; + } + + std::string player_name = ch->player.short_descr; + + // Search all clans for this player + for (const auto &clan : all()) { + auto member_opt = clan->get_member_by_name(player_name); + if (member_opt.has_value()) { + return clan->id(); + } + } + + return CLAN_ID_NONE; + } + + // Verify character is still a valid member of their clan + [[nodiscard]] bool verify_clan_membership(const CharacterPtr &ch) { + if (!ch || !ch->player.short_descr || ch->player_specials->clan_id == CLAN_ID_NONE) { + return false; + } + + auto clan_opt = find_by_id(ch->player_specials->clan_id); + if (!clan_opt) { + return false; // Clan no longer exists + } + + auto member_opt = clan_opt.value()->get_member_by_name(ch->player.short_descr); + return member_opt.has_value(); + } + + // Legacy loading function for migration from .clan files to JSON format + bool load_legacy(const std::string_view clan_num); + + // Initialize clans from legacy files or JSON + void init_clans(); + + private: + void build_caches() const { + if (caches_valid_) + return; + + name_to_id_.clear(); + abbr_to_id_.clear(); + + for (const auto &[id, clan] : clans_) { + name_to_id_[std::string(clan->name())] = id; + abbr_to_id_[std::string(clan->abbreviation())] = id; + } + + caches_valid_ = true; + } }; -struct ClanSnoop { - Clan *clan; - ClanSnoop *next; +// ClanRepository instance +extern ClanRepository clan_repository; + +// Clan snooping system - maps clan ID to set of character pointers +extern std::unordered_map<ClanID, std::unordered_set<CharData *>> clan_snoop_table; + +// Clan snoop management functions +void add_clan_snoop(CharData *ch, ClanID clan_id); +void remove_clan_snoop(CharData *ch, ClanID clan_id); +void remove_all_clan_snoops(CharData *ch); +bool is_snooping_clan(CharData *ch, ClanID clan_id); +std::vector<ClanID> get_snooped_clans(CharData *ch); + +// Direct clan access functions (replacing ClanMembership system) +[[nodiscard]] inline ClanID get_clan_id(const CharData *ch) { + return (!ch || !ch->player_specials) ? CLAN_ID_NONE : ch->player_specials->clan_id; +} + +[[nodiscard]] inline std::optional<ClanPtr> get_clan(const CharData *ch) { + if (!ch || !ch->player_specials || ch->player_specials->clan_id == CLAN_ID_NONE) { + return std::nullopt; + } + return clan_repository.find_by_id(ch->player_specials->clan_id); +} + +[[nodiscard]] inline std::optional<ClanMember> get_clan_member(const CharData *ch) { + auto clan = get_clan(ch); + if (!clan || !ch->player.short_descr) { + return std::nullopt; + } + return clan.value()->get_member_by_name(ch->player.short_descr); +} + +[[nodiscard]] inline std::optional<ClanRank> get_clan_rank(const CharData *ch) { + auto clan = get_clan(ch); + auto member = get_clan_member(ch); + if (!clan || !member) { + return std::nullopt; + } + + const auto &ranks = clan.value()->ranks(); + if (member->rank_index < 0 || member->rank_index >= static_cast<int>(ranks.size())) { + return std::nullopt; + } + + return ranks[member->rank_index]; +} + +[[nodiscard]] inline bool has_clan_permission(const CharData *ch, ClanPermission permission) { + if (!ch) { + return false; + } + auto rank = get_clan_rank(ch); + if (!rank) { + return false; + } + return rank->has_permission(permission); +} + +// Function for use with function registry - checks clan permission or god status +[[nodiscard]] bool has_clan_permission_or_god(const CharData *ch, ClanPermission permission); + +// Get clan permissions for function registry (PermissionFlags defined in function_registration.hpp) +[[nodiscard]] uint32_t get_clan_permissions(const CharData *ch); + +inline void set_clan_id(CharData *ch, ClanID clan_id) { ch->player_specials->clan_id = clan_id; } + +inline void clear_clan_membership(CharData *ch) { ch->player_specials->clan_id = CLAN_ID_NONE; } + +// Legacy compatibility function +[[nodiscard]] inline std::optional<ClanPtr> get_clan_membership(const CharData *ch) { return get_clan(ch); } + +// Modern C++23 permission checking system with std::expected +namespace clan_permissions { + + // Permission check result with detailed error information + struct PermissionError { + std::string reason; + ClanPermission required_permission; + bool is_clan_member; + + PermissionError(std::string r, ClanPermission perm = ClanPermission::NONE, bool member = false) + : reason(std::move(r)), required_permission(perm), is_clan_member(member) {} + }; + + using PermissionResult = std::expected<void, PermissionError>; + + // Check if character has specific clan permission + [[nodiscard]] PermissionResult check_permission(const CharData *ch, ClanPermission permission); + + // Check if character is a clan member (any rank) + [[nodiscard]] PermissionResult check_clan_member(const CharData *ch); + + // Simple permission checking helpers + [[nodiscard]] bool check_god_override(const CharData *ch); + + // Enhanced permission checking with detailed error messages + [[nodiscard]] std::string get_permission_error_message(ClanPermission permission); + + // Enhanced permission decorator system for clean, readable command implementation + + // Public command decorator (no permissions or clan membership required) + struct RequiresNoPermissions { + template<typename Func> + static bool execute(CharData *ch, Func&& command_func) { + command_func(); + return true; + } + }; + +template<ClanPermission... Permissions> +struct RequiresPermissions { + static constexpr std::array<ClanPermission, sizeof...(Permissions)> perms = {Permissions...}; + + template<typename Func> + static bool execute(CharData *ch, Func&& command_func) { + // Gods bypass all permission checks + if (check_god_override(ch)) { + command_func(); + return true; + } + + // Check all required permissions + for (auto perm : perms) { + auto result = check_permission(ch, perm); + if (!result) { + char_printf(ch, "{}", result.error().reason); + return false; + } + } + + // Execute command + command_func(); + return true; + } }; -/*************************************************************************** - * Clan Constants - ***************************************************************************/ -const struct { - const char *abbr; - bool default_on; - const char *desc; -} clan_privileges[NUM_CLAN_PRIVS] = { - {"desc", false, "Change Description"}, - {"motd", false, "Change Message of the Day"}, - {"grant", false, "Grant Privilege"}, - {"ranks", false, "Change Ranks"}, - {"title", false, "Change Titles"}, - {"enroll", false, "Enroll"}, - {"expel", false, "Expel"}, - {"promote", false, "Promote"}, - {"demote", false, "Demote"}, - {"appfee", false, "Change Application Fee"}, - {"applev", false, "Change Application Level"}, - {"dues", false, "Change Dues"}, - {"withdraw", false, "Withdraw"}, - {"alts", true, "Alts"}, - {"chat", true, "Chat"}, +// Convenience aliases for common permission combinations +struct RequiresClanMembership { + template<typename Func> + static bool execute(CharData *ch, Func&& command_func) { + // Gods bypass all permission checks + if (check_god_override(ch)) { + command_func(); + return true; + } + + // Check clan membership + auto result = check_clan_member(ch); + if (!result) { + char_printf(ch, "{}", result.error().reason); + return false; + } + + // Execute command + command_func(); + return true; + } }; +using RequiresClanChat = RequiresPermissions<ClanPermission::CLAN_CHAT>; +using RequiresFinancialAccess = RequiresPermissions<ClanPermission::VIEW_FINANCES>; +using RequiresDepositFunds = RequiresPermissions<ClanPermission::DEPOSIT_FUNDS>; +using RequiresWithdrawFunds = RequiresPermissions<ClanPermission::WITHDRAW_FUNDS>; +using RequiresStorageAccess = RequiresPermissions<ClanPermission::VIEW_STORAGE>; +using RequiresStoreItems = RequiresPermissions<ClanPermission::STORE_ITEMS>; +using RequiresRetrieveItems = RequiresPermissions<ClanPermission::RETRIEVE_ITEMS>; +using RequiresMemberManagement = RequiresPermissions<ClanPermission::INVITE_MEMBERS, ClanPermission::KICK_MEMBERS>; +using RequiresRankManagement = RequiresPermissions<ClanPermission::MANAGE_RANKS>; +using RequiresLeaderOverride = RequiresPermissions<ClanPermission::LEADER_OVERRIDE>; + +// Macro for clean clan command declarations with permission requirements +#define CLAN_COMMAND(name, permission_class) \ + static void name##_impl(CharData *ch, Arguments argument); \ + static void(name)(CharData *ch, Arguments argument) { \ + permission_class::execute(ch, [ch, argument]() { name##_impl(ch, argument); }); \ + } \ + static void name##_impl(CharData *ch, Arguments argument) + +// Command wrapper functions (simpler than templates) +bool execute_with_clan_permission(CharData *ch, ClanPermission permission, + std::function<void()> command_func); +bool execute_with_clan_membership(CharData *ch, std::function<void()> command_func); + +} // namespace clan_permissions + +// Legacy conversion functions for migration from old ClanPrivilege system +namespace legacy_conversion { + // Convert old ClanPrivilege values to new ClanPermission values + [[nodiscard]] std::optional<ClanPermission> convert_legacy_privilege(int legacy_value); + + // Convert legacy bitset to new permission set + [[nodiscard]] PermissionSet convert_legacy_permissions(const std::bitset<64>& legacy_bits); + +} // namespace legacy_conversion -#define GET_CLAN_MEMBERSHIP(ch) ((ch)->player_specials->clan) -#define GET_CLAN(ch) (GET_CLAN_MEMBERSHIP(ch) ? GET_CLAN_MEMBERSHIP(ch)->clan : NULL) -#define GET_CLAN_RANK(ch) (GET_CLAN_MEMBERSHIP(ch) ? GET_CLAN_MEMBERSHIP(ch)->rank : RANK_NONE) -#define GET_CLAN_TITLE(ch) (IS_CLAN_MEMBER(ch) ? GET_CLAN(ch)->ranks[GET_CLAN_RANK(ch) - 1].title : NULL) -#define MEMBER_CAN(member, priv) \ - (IS_MEMBER_RANK(member->rank) && IS_FLAGGED(member->clan->ranks[member->rank - 1].privileges, (priv))) -#define HAS_CLAN_PRIV(ch, priv) (GET_CLAN_MEMBERSHIP(ch) ? MEMBER_CAN(GET_CLAN_MEMBERSHIP(ch), (priv)) : false) -#define CAN_DO_PRIV(ch, priv) (IS_CLAN_ADMIN(ch) || IS_CLAN_SUPERADMIN(ch) || HAS_CLAN_PRIV((ch), (priv))) -#define GET_CLAN_SNOOP(ch) ((ch)->player_specials->clan_snoop) - -void init_clans(void); -void save_clans(void); -Clan *find_clan(const char *id); -Clan *find_clan_by_abbr(const char *abbr); -Clan *find_clan_by_number(unsigned int number); -ClanMembership *find_clan_membership(const char *name); -ClanMembership *find_clan_membership_in_clan(const char *name, const Clan *clan); -bool revoke_clan_membership(ClanMembership *); -void add_clan_membership(Clan *, ClanMembership *); -void save_clan(const Clan *); -void update_clan(Clan *); -void free_clans(void); -void clan_notification(Clan *, CharData *skip, const char *str, ...) __attribute__((format(printf, 3, 4))); -void clan_set_title(CharData *ch); -Clan *alloc_clan(void); -void dealloc_clan(Clan *); -typedef Clan **clan_iter; -unsigned int days_until_reapply(const ClanMembership *member); -PRIV_FUNC(clan_admin_check); - -clan_iter clans_start(void); -clan_iter clans_end(void); -unsigned int clan_count(void); +// Complete security and validation utilities namespace +namespace clan_security { + + // Input validation constants + static constexpr size_t MAX_CLAN_NAME_LENGTH = 30; + static constexpr size_t MAX_CLAN_ABBR_LENGTH = 10; + static constexpr size_t MAX_CLAN_DESCRIPTION_LENGTH = 1000; + static constexpr size_t MAX_CLAN_MOTD_LENGTH = 2000; + static constexpr size_t MAX_CLAN_MEMBERS = 100; + static constexpr size_t MAX_CLAN_RANKS = 20; + static constexpr size_t MAX_CLAN_STORAGE_ITEMS = 1000; + + // Input validation result + struct ValidationError { + std::string reason; + std::string field; + std::string value; + + ValidationError(std::string r, std::string f = "", std::string v = "") + : reason(std::move(r)), field(std::move(f)), value(std::move(v)) {} + }; + + using ValidationResult = std::expected<void, ValidationError>; + + // Resource limit validation + [[nodiscard]] bool check_member_limit(const Clan& clan); + [[nodiscard]] bool check_rank_limit(const Clan& clan); + [[nodiscard]] bool check_storage_limit(const Clan& clan); + + // Financial operation safety + [[nodiscard]] std::expected<Money, std::string> safe_add_money(const Money& base, const Money& addition); + [[nodiscard]] std::expected<Money, std::string> safe_subtract_money(const Money& base, const Money& subtraction); + [[nodiscard]] std::expected<int, std::string> safe_add_quantity(int base, int addition); + + // Safe character lookup without const_cast + [[nodiscard]] CharData* find_player_safe(std::string_view name); + [[nodiscard]] CharData* find_clan_member_safe(std::string_view name); + + // Basic string safety check + [[nodiscard]] bool contains_unsafe_characters(std::string_view input); + +} // namespace clan_security diff --git a/src/clansys.cpp b/src/clansys.cpp deleted file mode 100644 index a8fbe363..00000000 --- a/src/clansys.cpp +++ /dev/null @@ -1,718 +0,0 @@ -/*************************************************************************** - * File: clansys.c Part of FieryMUD * - * Usage: Infrastructure for the clan system * - * * - * FieryMUD Copyright (C) 1998, 1999, 2000 by the Fiery Consortium * - * FieryMUD is based on HubisMUD Copyright (C) 1997, 1998. * - * HubisMUD is based on DikuMUD, Copyright (C) 1990, 1991. * - ***************************************************************************/ - -#include "clan.hpp" -#include "comm.hpp" -#include "conf.hpp" -#include "db.hpp" -#include "handler.hpp" -#include "interpreter.hpp" -#include "limits.hpp" -#include "logging.hpp" -#include "math.hpp" -#include "players.hpp" -#include "screen.hpp" -#include "structs.hpp" -#include "sysdep.hpp" -#include "utils.hpp" - -#include <sys/stat.h> - -/*************************************************************************** - * Clan Variables - ***************************************************************************/ -static unsigned int num_of_clans = 0; -static Clan **clans = nullptr; - -/*************************************************************************** - * Clan Infrastructure - ***************************************************************************/ - -static void sort_clan_people(Clan *clan) { - ClanMembership **array, *member; - size_t i, count; - bool swap; - - if (clan->people_count == 0) - return; - - /* copy the linked list to an array */ - CREATE(array, ClanMembership *, clan->people_count); - member = clan->people; - for (i = 0; i < clan->people_count; ++i) { - array[i] = member; - member = member->next; - } - - /* bubble sort by rank */ - count = clan->people_count; - do { - swap = false; - --count; - for (i = 0; i < count; ++i) - if (OUTRANKS(array[i + 1]->rank, array[i]->rank)) { - member = array[i]; - array[i] = array[i + 1]; - array[i + 1] = member; - swap = true; - } - } while (swap); - - /* redo the linked list */ - clan->people = array[0]; - for (i = 1; i < clan->people_count; ++i) - array[i - 1]->next = array[i]; - array[i - 1]->next = nullptr; - - free(array); -} - -static void load_clan_member(Clan *clan, const char *line) { - ClanMembership *member, *alt; - int num; - char name[MAX_NAME_LENGTH + 1]; - - line = fetch_word(line, name, sizeof(name)); - cap_by_color(name); - if ((num = get_ptable_by_name(name)) < 0) - return; - - CREATE(member, ClanMembership, 1); - member->name = strdup(name); - member->clan = clan; - member->relation.alts = nullptr; - member->player = nullptr; - - line = fetch_word(line, name, sizeof(name)); - member->rank = std::max(1, atoi(name)); - - line = fetch_word(line, name, sizeof(name)); - member->since = atoi(name); - - if (IS_MEMBER_RANK(member->rank)) { - clan->power += player_table[num].level; - clan->member_count++; - } else if (IS_ADMIN_RANK(member->rank)) - clan->admin_count++; - else if (IS_APPLICANT_RANK(member->rank)) - clan->applicant_count++; - else if (IS_REJECT_RANK(member->rank)) - clan->reject_count++; - - for (;;) { - line = fetch_word(line, name, sizeof(name)); - if (!*name) - break; - cap_by_color(name); - if (get_ptable_by_name(name) < 0) - continue; - CREATE(alt, ClanMembership, 1); - alt->name = strdup(name); - alt->rank = ALT_RANK_OFFSET + member->rank; - alt->clan = clan; - alt->relation.member = member; - alt->since = member->since; - alt->next = member->relation.alts; - member->relation.alts = alt; - } - - member->next = clan->people; - clan->people = member; - clan->people_count++; -} - -static void refresh_list_pointers(Clan *clan) { - ClanMembership *member; - -#define CHANGE_IF_NULL(a, b) ((a) = ((a) ? (a) : (b))) - - clan->admins = nullptr; - clan->members = nullptr; - clan->applicants = nullptr; - clan->rejects = nullptr; - - for (member = clan->people; member; member = member->next) - if (IS_ADMIN_RANK(member->rank)) - CHANGE_IF_NULL(clan->admins, member); - else if (IS_MEMBER_RANK(member->rank)) - CHANGE_IF_NULL(clan->members, member); - else if (IS_APPLICANT_RANK(member->rank)) - CHANGE_IF_NULL(clan->applicants, member); - else if (IS_REJECT_RANK(member->rank)) - CHANGE_IF_NULL(clan->rejects, member); - -#undef CHANGE_IF_NULL -} - -void update_clan(Clan *clan) { - sort_clan_people(clan); - refresh_list_pointers(clan); -} - -bool load_clan(const char *clan_num, Clan *clan) { - FILE *fl; - char filename[128], tag[128], *line = buf; - const char *string; - int num, i; - - /* Open clan file for reading */ - snprintf(filename, sizeof(filename), "%s/%s%s", CLAN_PREFIX, clan_num, CLAN_SUFFIX); - if (!(fl = fopen(filename, "r"))) { - log("Couldn't open clan file '{}'", filename); - return false; - } - - /* Initialize fields */ - clan->people_count = 0; - clan->member_count = 0; - clan->admin_count = 0; - clan->applicant_count = 0; - clan->reject_count = 0; - clan->rank_count = 0; - CREATE(clan->ranks, ClanRank, MAX_CLAN_RANKS); - clan->people = nullptr; - clan->power = 0; - - /* Tag-based ASCII file parser */ - while (get_line(fl, line)) { - tag_argument(line, tag); - num = atoi(line); - - switch (toupper(*tag)) { - case 'A': - if (TAG_IS("appfee")) - clan->app_fee = num; - else if (TAG_IS("applevel")) - clan->app_level = num; - else if (TAG_IS("abbr")) { - char *space = strchr(line, ' '); - if (space) - *space = '\0'; - clan->abbreviation = strdup(line); - } else - goto bad_tag; - break; - case 'C': - if (TAG_IS("copper")) - clan->treasure[COPPER] = num; - else - goto bad_tag; - break; - case 'D': - if (TAG_IS("dues")) - clan->dues = num; - else if (TAG_IS("description")) - clan->description = fread_string(fl, "load_clan"); - else - goto bad_tag; - break; - case 'G': - if (TAG_IS("gold")) - clan->treasure[GOLD] = num; - else - goto bad_tag; - break; - case 'M': - if (TAG_IS("member")) - load_clan_member(clan, line); - else if (TAG_IS("motd")) - clan->motd = fread_string(fl, "load_clan"); - else - goto bad_tag; - break; - case 'N': - if (TAG_IS("name")) - clan->name = strdup(line); - else if (TAG_IS("number")) - clan->number = num; - else - goto bad_tag; - break; - case 'P': - if (TAG_IS("platinum")) - clan->treasure[PLATINUM] = num; - else if (TAG_IS("privilege")) { - if (num <= 0 || num > MAX_CLAN_RANKS) - log("SYSERR: load_clan: attempt to set clan privilege for invalid rank {:d}", num); - else { - string = skip_chars(skip_over(line, S_DIGITS), ' '); - while (*string) { - string = fetch_word(string, buf1, sizeof(buf1)); - for (i = 0; i < NUM_CLAN_PRIVS; ++i) - if (!strcasecmp(clan_privileges[i].abbr, buf1)) { - clan->rank_count = std::max<size_t>(clan->rank_count, num); - SET_FLAG(clan->ranks[num - 1].privileges, i); - break; - } - if (i >= NUM_CLAN_PRIVS) - log("SYSERR: load_clan: attempt to assign invalid clan privilege {} to rank {:d}", buf1, - num); - } - } - } else - goto bad_tag; - break; - case 'S': - if (TAG_IS("silver")) - clan->treasure[SILVER] = num; - else - goto bad_tag; - break; - case 'T': - if (TAG_IS("title")) { - if (num <= 0 || num > MAX_CLAN_RANKS) - log("SYSERR: load_clan: attempt to set clan title for invalid rank {:d}", num); - else { - string = skip_chars(skip_over(line, S_DIGITS), ' '); - if (clan->ranks[num - 1].title) - log("SYSERR: load_clan: attempt to load duplicate clan title for rank {:d}", num); - else { - clan->rank_count = std::max<size_t>(clan->rank_count, num); - clan->ranks[num - 1].title = strdup(string); - } - } - } else - goto bad_tag; - break; - default: - bad_tag: - log("SYSERR: Unknown tag {} in clan file {}: {}", tag, clan_num, line); - break; - } - } - - RECREATE(clan->ranks, ClanRank, clan->rank_count); - - update_clan(clan); - - return true; -} - -void init_clans(void) { - FILE *fl; - char clan_id[17]; - struct load_list { - Clan *clan; - load_list *next; - } *temp, *load = nullptr; - unsigned int pos; - - /* - * Legacy-checking code. If we successfully open the file, that means - * it is not a directory, and is probably the binary format from the - * old clan system. In this case, back it up with a .old extension. - */ - if ((fl = fopen(CLAN_PREFIX, "r"))) { - struct stat statbuf; - fstat(fileno(fl), &statbuf); - fclose(fl); - if (!S_ISDIR(statbuf.st_mode)) { - rename(CLAN_PREFIX, CLAN_PREFIX_OLD); - /* - * This is a POSIX-only. Create the new clan directory. - */ - log("Backed-up old clans file and creating new clan directory."); - mkdir(CLAN_PREFIX, 0775); - } - } - - if (!(fl = fopen(CLAN_INDEX_FILE, "r"))) { - log("No clan index. Creating a new one."); - touch(CLAN_INDEX_FILE); - if (!(fl = fopen(CLAN_INDEX_FILE, "r"))) { - perror("fatal error opening clan index"); - exit(1); - } - } - - num_of_clans = 0; - while (fgets(clan_id, sizeof(clan_id), fl)) { - clan_id[strlen(clan_id) - 1] = '\0'; /* remove the \n */ - CREATE(temp, load_list, 1); - CREATE(temp->clan, Clan, 1); - if (load_clan(clan_id, temp->clan)) { - ++num_of_clans; - temp->next = load; - load = temp; - } else - free(temp); - } - - CREATE(clans, Clan *, num_of_clans); - for (pos = num_of_clans; pos > 0; --pos) { - clans[pos - 1] = load->clan; - temp = load->next; - free(load); - load = temp; - } - - log("{:3d} clan{} loaded.", num_of_clans, num_of_clans == 1 ? "" : "s"); - - fclose(fl); -} - -static void save_clan_index(void) { - FILE *fl; - unsigned int i; - - if (!(fl = fopen(CLAN_INDEX_FILE, "w"))) { - log("No clan index. Creating a new one."); - touch(CLAN_INDEX_FILE); - if (!(fl = fopen(CLAN_INDEX_FILE, "w"))) { - perror("fatal error opening clan index"); - exit(1); - } - } - - for (i = 0; i < num_of_clans; ++i) - fprintf(fl, "%u\n", clans[i]->number); - - fclose(fl); -} - -static void perform_save_clan(const Clan *clan) { - FILE *fl; - char temp_filename[128]; - char filename[128]; - ClanMembership *member, *alt; - unsigned int i, j; - - sprintf(filename, "%s/%d%s", CLAN_PREFIX, clan->number, CLAN_SUFFIX); - sprintf(temp_filename, "%s/%d.tmp", CLAN_PREFIX, clan->number); - if (!(fl = fopen(temp_filename, "w"))) { - log(LogSeverity::Error, LVL_GOD, "SYSERR: Couldn't open temp clan file {} for write", temp_filename); - return; - } - - fprintf(fl, "number: %d\n", clan->number); - fprintf(fl, "name: %s\n", clan->name); - fprintf(fl, "abbr: %s\n", clan->abbreviation); - if (clan->description) - fprintf(fl, "description:\n%s~\n", filter_chars(buf, clan->description, "\r~")); - if (clan->motd) - fprintf(fl, "motd:\n%s~\n", filter_chars(buf, clan->motd, "\r~")); - fprintf(fl, "dues: %u\n", clan->dues); - fprintf(fl, "appfee: %u\n", clan->app_fee); - fprintf(fl, "applevel: %u\n", clan->app_level); - - fprintf(fl, "copper: %d\n", clan->treasure[COPPER]); - fprintf(fl, "silver: %d\n", clan->treasure[SILVER]); - fprintf(fl, "gold: %d\n", clan->treasure[GOLD]); - fprintf(fl, "platinum: %d\n", clan->treasure[PLATINUM]); - - for (i = 0; i < clan->rank_count; ++i) { - fprintf(fl, "title: %u %s\n", i + 1, clan->ranks[i].title); - fprintf(fl, "privilege: %u", i + 1); - for (j = 0; j < NUM_CLAN_PRIVS; ++j) - if (IS_FLAGGED(clan->ranks[i].privileges, j)) - fprintf(fl, " %s", clan_privileges[j].abbr); - fprintf(fl, "\n"); - } - - for (member = clan->people; member; member = member->next) { - fprintf(fl, "member: %s %u %lu", member->name, member->rank, member->since); - for (alt = member->relation.alts; alt; alt = alt->next) - fprintf(fl, " %s", alt->name); - fprintf(fl, "\n"); - } - - fclose(fl); - - if (rename(temp_filename, filename)) - log("SYSERR: Error renaming temp clan file {} to {}: {}", temp_filename, filename, strerror(errno)); -} - -void save_clan(const Clan *clan) { - perform_save_clan(clan); - save_clan_index(); -} - -void save_clans(void) { - unsigned int i; - for (i = 0; i < num_of_clans; ++i) - perform_save_clan(clans[i]); - save_clan_index(); -} - -Clan *find_clan_by_number(unsigned int number) { - clan_iter iter; - - for (iter = clans_start(); iter != clans_end(); ++iter) - if ((*iter)->number == number) - return *iter; - - return nullptr; -} - -Clan *find_clan_by_abbr(const char *abbr) { - clan_iter iter; - - for (iter = clans_start(); iter != clans_end(); ++iter) - if (isname(abbr, (*iter)->abbreviation)) - return *iter; - - return nullptr; -} - -Clan *find_clan(const char *id) { - Clan *clan; - if (is_number(id)) - if ((clan = find_clan_by_number(atoi(id)))) - return clan; - if ((clan = find_clan_by_abbr(id))) - return clan; - { - clan_iter iter; - for (iter = clans_start(); iter != clans_end(); ++iter) - if (isname(id, (*iter)->name)) - return *iter; - } - return nullptr; -} - -ClanMembership *find_clan_membership_in_clan(const char *name, const Clan *clan) { - ClanMembership *member = clan->people; - - while (member) { - if (!strcasecmp(name, member->name)) - return member; - - if (IS_ALT_RANK(member->rank)) { - /* We're currently on an alt. If there is another alt for - * this member, go to that one. Otherwise, go to the next - * member in the clan. - */ - if (member->next) - member = member->next; - else - member = member->relation.member->next; - } else { - /* We're currently on a member. If this member has alts, - * go to the first alt in the list. Otherwise go to the - * next member in the clan. - */ - if (member->relation.alts) - member = member->relation.alts; - else - member = member->next; - } - } - - return nullptr; -} - -ClanMembership *find_clan_membership(const char *name) { - clan_iter iter; - ClanMembership *member; - - for (iter = clans_start(); iter != clans_end(); ++iter) - if ((member = find_clan_membership_in_clan(name, *iter))) - return member; - - return nullptr; -} - -void free_clan_membership(ClanMembership *member) { - free(member->name); - if (!IS_ALT_RANK(member->rank)) { - ClanMembership *alt; - for (alt = member->relation.alts; alt; alt = alt->next) - free_clan_membership(alt); - } - if (member->player) - member->player->player_specials->clan = nullptr; - free(member); -} - -/* Doesn't free the clan itself! */ -void free_clan(Clan *clan) { - ClanMembership *member; - free(clan->name); - free(clan->abbreviation); - free(clan->description); - free(clan->motd); - free(clan->ranks); - while (clan->people) { - member = clan->people->next; - free_clan_membership(clan->people); - clan->people = member; - } - free(clan); -} - -void free_clans(void) { - unsigned int i; - for (i = 0; i < num_of_clans; ++i) - free_clan(clans[i]); - free(clans); -} - -bool revoke_clan_membership(ClanMembership *member) { - ClanMembership *p; - Clan *clan; - int i; - - if (!member) - return false; - - clan = member->clan; - - /* Remove from clan lists */ - if (IS_ALT_RANK(member->rank)) { - p = member->relation.member; - if (p->relation.alts == member) - p->relation.alts = member->next; - else - for (p = p->relation.alts; p->next; p = p->next) - if (p->next == member) { - p->next = member->next; - break; - } - } else if (clan) { - if (clan->people == member) - clan->people = member->next; - else - for (p = clan->people; p->next; p = p->next) - if (p->next == member) { - p->next = member->next; - break; - } - if (IS_MEMBER_RANK(member->rank)) { - clan->member_count--; - if (member->player) - clan->power -= GET_LEVEL(member->player); - else if ((i = get_ptable_by_name(member->name)) >= 0) - clan->power -= player_table[i].level; - } else if (IS_ADMIN_RANK(member->rank)) - clan->admin_count--; - else if (IS_APPLICANT_RANK(member->rank)) - clan->applicant_count--; - else if (IS_REJECT_RANK(member->rank)) - clan->reject_count--; - } - - free_clan_membership(member); - - if (clan) - save_clan(clan); - - return true; -} - -void add_clan_membership(Clan *clan, ClanMembership *member) { - ClanMembership *m; - - if (!clan->people || OUTRANKS(member->rank, clan->people->rank)) { - member->next = clan->people; - clan->people = member; - } else { - member->next = nullptr; - for (m = clan->people; m->next; m = m->next) - if (OUTRANKS(member->rank, m->next->rank)) { - member->next = m->next; - break; - } - m->next = member; - } - - if (IS_ADMIN_RANK(member->rank)) - clan->admin_count++; - else if (IS_MEMBER_RANK(member->rank)) - clan->member_count++; - else if (IS_APPLICANT_RANK(member->rank)) - clan->applicant_count++; - else if (IS_REJECT_RANK(member->rank)) - clan->reject_count++; - - clan->people_count++; - - if (!IS_ALT_RANK(member->rank)) - refresh_list_pointers(clan); - - member->clan = clan; -} - -void clan_notification(Clan *clan, CharData *skip, const char *messg, ...) { - DescriptorData *d; - size_t found = false; - va_list args; - char comm_buf[MAX_STRING_LENGTH]; - - if (clan && messg && *messg) - for (d = descriptor_list; d; d = d->next) - if (IS_PLAYING(d) && d->character && (!skip || (d->character != skip && d->original != skip)) && - GET_CLAN(REAL_CHAR(d->character)) == clan && !PRF_FLAGGED(d->character, PRF_NOCLANCOMM) && - !OUTRANKS(RANK_APPLICANT, GET_CLAN_RANK(REAL_CHAR(d->character)))) { - if (!found) { - strcpy(comm_buf, AFMAG "[[ "); - found = strlen(comm_buf); - va_start(args, messg); - vsnprintf(comm_buf + found, sizeof(comm_buf) - found, messg, args); - va_end(args); - strcat(comm_buf, AFMAG " ]]\n" ANRM); - found = true; - } - string_to_output(d, comm_buf); - } -} - -clan_iter clans_start(void) { return clans; } - -clan_iter clans_end(void) { return clans + num_of_clans; } - -unsigned int clan_count(void) { return num_of_clans; } - -void clan_set_title(CharData *ch) { - if (IS_CLAN_MEMBER(ch)) { - sprintf(buf, "%s %s", GET_CLAN_TITLE(ch), GET_CLAN(ch)->abbreviation); - set_title(ch, buf); - } else - set_title(ch, nullptr); -} - -Clan *alloc_clan() { - clan_iter iter; - unsigned int number = 0; - - for (iter = clans_start(); iter != clans_end(); ++iter) - number = std::max(number, (*iter)->number); - - ++num_of_clans; - RECREATE(clans, Clan *, num_of_clans); - CREATE(clans[num_of_clans - 1], Clan, 1); - clans[num_of_clans - 1]->number = number + 1; - save_clan_index(); - return clans[num_of_clans - 1]; -} - -void dealloc_clan(Clan *clan) { - clan_iter dest, src; - - for (dest = src = clans_start(); src != clans_end(); ++src) - if (*src != clan) - *(dest++) = *src; - free_clan(clan); - --num_of_clans; - save_clan_index(); -} - -unsigned int days_until_reapply(const ClanMembership *member) { - if (IS_REJECT_RANK(member->rank)) { - double diff = difftime(time(0), member->since); - if (diff > 0) - return diff; - } - - return 0; -} - -PRIV_FUNC(clan_admin_check) { - if (PRV_FLAGGED(ch, PRV_CLAN_ADMIN) && GET_CLAN_MEMBERSHIP(ch)) - revoke_clan_membership(GET_CLAN_MEMBERSHIP(ch)); -} diff --git a/src/class.cpp b/src/class.cpp index f6211e94..6cb0c86e 100644 --- a/src/class.cpp +++ b/src/class.cpp @@ -1298,8 +1298,7 @@ int level_max_skill(CharData *ch, int level, int skill) { int return_max_skill(CharData *ch, int skill) { return level_max_skill(ch, GET_LEVEL(ch), skill); } -void init_char_class(CharData *ch) { /* Nothing much to do here. */ -} +void init_char_class(CharData *ch) { /* Nothing much to do here. */ } void update_char_class(CharData *ch) { if (!VALID_CLASS(ch)) { @@ -1479,14 +1478,6 @@ void advance_level(CharData *ch, enum level_action action) { SET_FLAG(PRF_FLAGS(ch), PRF_HOLYLIGHT); } - /* Modify clan power */ - if (GET_CLAN(ch) && IS_CLAN_MEMBER(ch)) { - if (action == LEVEL_GAIN) - ++GET_CLAN(ch)->power; - else - --GET_CLAN(ch)->power; - } - check_regen_rates(ch); /* start regening new points */ update_char(ch); /* update skills/spells/innates/etc. for new level */ save_player_char(ch); diff --git a/src/comm.cpp b/src/comm.cpp index aa78114e..df842af8 100644 --- a/src/comm.cpp +++ b/src/comm.cpp @@ -62,6 +62,7 @@ #else #include "telnet.h" #endif +#include "bitflags.hpp" #ifndef INVALID_SOCKET #define INVALID_SOCKET -1 @@ -160,204 +161,7 @@ void free_invalid_list(void); * main game loop and related stuff * ********************************************************************* */ -int main(int argc, char **argv) { - int pos = 1; - const char *dir, *env; - - port = DFLT_PORT; - dir = DFLT_DIR; - env = DFLT_ENV; - - while ((pos < argc) && (*(argv[pos]) == '-')) { - switch (*(argv[pos] + 1)) { - case 'd': - if (*(argv[pos] + 2)) - dir = argv[pos] + 2; - else if (++pos < argc) - dir = argv[pos]; - else { - log("Directory arg expected after option -d."); - exit(1); - } - break; - case 'e': - if (*(argv[pos] + 2)) - env = argv[pos] + 2; - else if (++pos < argc) - env = argv[pos]; - else { - log("Environment arg expected after option -e."); - exit(1); - } - break; - case 'H': /* -H<socket number> recover from hotboot, this is the control socket */ - num_hotboots = 1; - mother_desc = atoi(argv[pos] + 2); - break; - case 'c': - scheck = 1; - log("Syntax check mode enabled."); - break; - case 'q': - log("Quick boot mode."); - break; - case 'r': - should_restrict = 1; - restrict_reason = RESTRICT_ARGUMENT; - log("Restricting game -- no new players allowed."); - break; - case 's': - no_specials = 1; - log("Suppressing assignment of special routines."); - break; - default: - log("SYSERR: Unknown option -{:c} in argument string.", *(argv[pos] + 1)); - break; - } - pos++; - } - - if (pos < argc) { - if (!isdigit(*argv[pos])) { - fprintf(stderr, "Usage: %s [-c] [-m] [-q] [-r] [-s] [-d pathname] [port #]\n", argv[0]); - exit(1); - } else if ((port = atoi(argv[pos])) <= 1024) { - fprintf(stderr, "Illegal port number.\n"); - exit(1); - } - } - if (chdir(dir) < 0) { - perror("Fatal error changing to data directory"); - exit(1); - } - log("Using {} as data directory.", dir); - - if (strcasecmp(env, "test") == 0) { - environment = ENV_TEST; - log("Running in test mode."); - } else if (strcasecmp(env, "dev") == 0) { - environment = ENV_DEV; - log("Running in dev mode."); - } else if (strcasecmp(env, "prod") == 0) { - environment = ENV_PROD; - log("Running in production mode."); - } else { - log("Unknown environment '{}'; valid choices are 'test', 'dev', and 'prod'.", env); - exit(1); - } - - log("Initializing runtime game constants."); - init_flagvectors(); - // init_rules(); - init_races(); - init_classes(); - init_objtypes(); - init_exp_table(); - - if (scheck) { - boot_world(); - } else { - log("Running game on port {:d}.", port); - init_game(port); - } - - log("Clearing game world."); - destroy_db(); - - return 0; -} - -void hotboot_recover() { - DescriptorData *d; - FILE *fp; - char host[1024]; - int desc, player_i; - bool fOld; - char name[MAX_INPUT_LENGTH]; - int count; - char *p; - - extern time_t *boot_time; - - log("Hotboot recovery initiated."); - - fp = fopen(HOTBOOT_FILE, "r"); - /* There are some descriptors open which will hang forever then? */ - if (!fp) { - perror("hotboot_recover:fopen"); - log("Hotboot file not found. Exiting.\n"); - exit(1); - } - - /* In case something crashes - doesn't prevent reading */ - unlink(HOTBOOT_FILE); - - /* read boot_time - first line in file */ - if (boot_time) - free(boot_time); - fgets(p = buf, MAX_STRING_LENGTH, fp); - p = any_one_arg(p, name); - num_hotboots = atoi(name); /* actually the total number of boots */ - CREATE(boot_time, time_t, num_hotboots + 1); - for (count = 0; count < num_hotboots; ++count) { - p = any_one_arg(p, name); - boot_time[count] = atol(name); - } - boot_time[num_hotboots] = time(0); - - /* More than 1000 iterations means something is pretty wrong. */ - for (count = 0; count <= 1000; ++count) { - fOld = true; - fscanf(fp, "%d %s %s\n", &desc, name, host); - if (desc == -1) - break; - - /* Write something, and check if it goes error-free */ - if (write_to_descriptor(desc, "\nRestoring from hotboot...\n") < 0) { - close(desc); /* nope */ - continue; - } - - /* Create a new descriptor */ - CREATE(d, DescriptorData, 1); - memset((char *)d, 0, sizeof(DescriptorData)); - init_descriptor(d, desc); /* set up various stuff */ - - strcpy(d->host, host); - d->next = descriptor_list; - descriptor_list = d; - - d->connected = CON_CLOSE; - - CREATE(d->character, CharData, 1); - clear_char(d->character); - CREATE(d->character->player_specials, PlayerSpecialData, 1); - d->character->desc = d; - - if ((player_i = load_player(name, d->character)) >= 0) { - if (!PLR_FLAGGED(d->character, PLR_DELETED)) { - REMOVE_FLAG(PLR_FLAGS(d->character), PLR_WRITING); - REMOVE_FLAG(PLR_FLAGS(d->character), PLR_MAILING); - } else - fOld = false; - } else - fOld = false; - - if (!fOld) { - write_to_descriptor(desc, "\nSomehow, your character was lost in the hotboot. Sorry.\n"); - close_socket(d); - } else { - sprintf(buf, "\n%sHotboot recovery complete.%s\n", CLR(d->character, HGRN), CLR(d->character, ANRM)); - write_to_descriptor(desc, buf); - enter_player_game(d); - d->connected = CON_PLAYING; - look_at_room(d->character, false); - } - } - - fclose(fp); -} /* Init sockets, run game, and cleanup sockets */ void init_game(int port) { @@ -370,10 +174,8 @@ void init_game(int port) { log("Finding player limit."); max_players = get_max_players(); - if (num_hotboots == 0) { - log("Opening mother connection."); - mother_desc = init_socket(port); - } + log("Opening mother connection."); + mother_desc = init_socket(port); event_init(); @@ -386,8 +188,6 @@ void init_game(int port) { reboot_pulse = 3600 * PASSES_PER_SEC * (reboot_hours_base - reboot_hours_deviation) + random_number(0, 3600 * PASSES_PER_SEC * 2 * reboot_hours_deviation); - if (num_hotboots > 0) - hotboot_recover(); log("Entering game loop."); @@ -1209,7 +1009,7 @@ void echo_off(DescriptorData *d) { void send_gmcp_prompt(DescriptorData *d) { CharData *ch = d->character, *vict = FIGHTING(ch), *tank; - char position[MAX_STRING_LENGTH]; + std::string_view position; effect *eff; if (!d->gmcp_enabled) { @@ -1233,9 +1033,9 @@ void send_gmcp_prompt(DescriptorData *d) { /* Need to construct a json string. Would be nice to get a module to do it for us, but the code base is too old to rely on something like that. */ if (d->original) - sprinttype(GET_POS(d->original), position_types, position); + position = sprinttype(GET_POS(d->original), position_types); else - sprinttype(GET_POS(d->character), position_types, position); + position = sprinttype(GET_POS(d->character), position_types); json gmcp_data = { {"name", strip_ansi(GET_NAME(ch))}, @@ -1336,7 +1136,7 @@ void send_mssp(DescriptorData *d) { mssp_data = fmt::format("{:c}{:c}{:c}", IAC, SB, MSSP); mssp_data += fmt::format("{:c}{}{:c}{}", MSSP_VAR, "NAME", MSSP_VAL, "FieryMUD"); mssp_data += fmt::format("{:c}{}{:c}{}", MSSP_VAR, "PLAYERS", MSSP_VAL, sockets_playing); - mssp_data += fmt::format("{:c}{}{:c}{}", MSSP_VAR, "UPTIME", MSSP_VAL, boot_time[0]); + mssp_data += fmt::format("{:c}{}{:c}{}", MSSP_VAR, "UPTIME", MSSP_VAL, boot_time); mssp_data += fmt::format("{:c}{}{:c}{}", MSSP_VAR, "AREAS", MSSP_VAL, top_of_zone_table + 1); mssp_data += fmt::format("{:c}{}{:c}{}", MSSP_VAR, "MOBILES", MSSP_VAL, top_of_mobt + 1); mssp_data += fmt::format("{:c}{}{:c}{}", MSSP_VAR, "OBJECTS", MSSP_VAL, top_of_objt + 1); @@ -2151,7 +1951,7 @@ int process_input(DescriptorData *t) { command_space_left--; } else { /* If we are here, it's normal text input. */ - if (IS_NEWLINE(*ptr) || space_left <= 0) { /* End of command, process it */ + if (*ptr == '\n' || space_left <= 0) { /* End of command, process it */ *write_point = '\0'; if (t->snoop_by) desc_printf(t->snoop_by, "&6>>&b {} &0\n", tmp); @@ -2180,10 +1980,10 @@ int process_input(DescriptorData *t) { char buffer[MAX_INPUT_LENGTH + 64]; if (write_to_descriptor(t->descriptor, "Line too long. Truncated to:\n{}\n", tmp) < 0) return -1; - while (*ptr && !IS_NEWLINE(*ptr)) /* Find next newline */ + while (*ptr && *ptr != '\n') /* Find next newline */ ++ptr; } - while (*(ptr + 1) && IS_NEWLINE(*(ptr + 1))) /* Find start of next command. */ + while (*(ptr + 1) && *(ptr + 1) == '\n') /* Find start of next command. */ ++ptr; write_point = tmp; diff --git a/src/comm.hpp b/src/comm.hpp index a54998f0..84bd86b7 100644 --- a/src/comm.hpp +++ b/src/comm.hpp @@ -25,7 +25,6 @@ using json = nlohmann::json; #define NUM_RESERVED_DESCS 8 -#define HOTBOOT_FILE "hotboot.dat" // #define CBP_FUNC(name) int(name)(CharData *, int) using CBP_FUNC = std::function<int(CharData *, int)>; diff --git a/src/commands.cpp b/src/commands.cpp index c7281f55..565bf340 100644 --- a/src/commands.cpp +++ b/src/commands.cpp @@ -14,6 +14,7 @@ #include "commands.hpp" +#include "bitflags.hpp" #include "comm.hpp" #include "conf.hpp" #include "constants.hpp" @@ -43,7 +44,7 @@ CommandGroupInfo *grp_info; * Private interface */ #define VALID_GROUP_NUM(gg) ((gg) >= cmd_groups && (gg) < top_of_cmd_groups) -#define GROUP_NUM(gg) ((gg)-cmd_groups) +#define GROUP_NUM(gg) ((gg) - cmd_groups) static void gedit_setup_existing(DescriptorData *d, int group); static void gedit_setup_new(DescriptorData *d); static void gedit_save_internally(DescriptorData *d); @@ -204,7 +205,7 @@ void gedit_parse(DescriptorData *d, char *arg) { switch (OLC_MODE(d)) { case GEDIT_MAIN_MENU: - switch (LOWER(*arg)) { + switch (to_lower(*arg)) { case '1': string_to_output(d, "Enter new group alias:\n"); OLC_MODE(d) = GEDIT_ALIAS; @@ -346,7 +347,7 @@ void gedit_parse(DescriptorData *d, char *arg) { gedit_disp_menu(d); return; case GEDIT_CONFIRM_SAVE: - switch (LOWER(*arg)) { + switch (to_lower(*arg)) { case 'y': string_to_output(d, "Saving command group in memory.\n"); gedit_save_internally(d); @@ -619,6 +620,9 @@ void do_show_command(CharData *ch, char *argument) { const CommandInfo *command; int cmd, grp; + constexpr std::string_view command_flags[] = {"MEDITATE", "MAJOR PARA", "MINOR PARA", "HIDE", "BOUND", + "CAST", "OLC", "NOFIGHT", "\n"}; + skip_spaces(&argument); if (!*argument) { @@ -633,8 +637,6 @@ void do_show_command(CharData *ch, char *argument) { command = &cmd_info[cmd]; - sprintbit(command->flags, command_flags, buf1); - resp += fmt::format( "Command : @y{}@0 (@g{}@0)\n" "Minimum Position : @c{}@0\n" @@ -644,7 +646,8 @@ void do_show_command(CharData *ch, char *argument) { "Usage Flags : @c{}@0\n" "Groups : @c", command->command, cmd, position_types[(int)command->minimum_position], - stance_types[(int)command->minimum_stance], command->minimum_level, command->subcmd, buf1); + stance_types[(int)command->minimum_stance], command->minimum_level, command->subcmd, + sprintbit(command->flags, command_flags)); if (grp_info[cmd].groups) { for (grp = 0; grp_info[cmd].groups[grp] >= 0; ++grp) diff --git a/src/conf.cpp b/src/conf.cpp index b479c470..9074d5b9 100644 --- a/src/conf.cpp +++ b/src/conf.cpp @@ -122,8 +122,7 @@ int donation_room_3 = NOWHERE; /* unused - room for expansion */ /****************************************************************************/ /* GAME OPERATION OPTIONS */ -time_t *boot_time = nullptr; /* times of mud boots (size = 1 + num_hotboots) */ -int num_hotboots = 0; /* are we doing a hotboot? */ +time_t boot_time; int should_restrict = 0; /* level of game restriction */ int restrict_reason = RESTRICT_NONE; /* reason for should_restrict > 0 */ diff --git a/src/conf.hpp b/src/conf.hpp index 132c6884..de05e5f1 100644 --- a/src/conf.hpp +++ b/src/conf.hpp @@ -132,8 +132,7 @@ extern int r_mortal_start_room, r_immort_start_room, r_frozen_start_room; extern int donation_room_1; extern int donation_room_2; extern int donation_room_3; -extern time_t *boot_time; -extern int num_hotboots; +extern time_t boot_time; extern int should_restrict; extern int restrict_reason; extern int environment; diff --git a/src/constants.cpp b/src/constants.cpp index 4b6d3d5b..d3384a1d 100644 --- a/src/constants.cpp +++ b/src/constants.cpp @@ -22,63 +22,96 @@ // Globals // Utility -const char *number_words[] = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"}; +const std::string_view number_words[] = {"zero", "one", "two", "three", "four", "five", + "six", "seven", "eight", "nine", "ten"}; /* MINOR CREATION ITEMS */ -const char *minor_creation_items[] = {"backpack", /* 0 */ - "sack", "robe", "hood", "lantern", "torch", /* 5 */ - "waterskin", "barrel", "rations", "raft", "club", /* 10 */ - "mace", "dagger", "greatsword", "longsword", "staff", /* 15 */ - "shield", "shortsword", "jacket", "pants", "leggings", /* 20 */ - "gauntlets", "sleeves", "gloves", "helmet", "skullcap", /* 25 */ - "boots", "sandals", "cloak", "book", "quill", /* 30 */ - "belt", "ring", "bracelet", "bottle", "keg", /* 35 */ - "mask", "earring", "scarf", "bracer", "\n"}; +const std::string_view minor_creation_items[] = { + "backpack", /* 0 */ + "sack", "robe", "hood", "lantern", "torch", /* 5 */ + "waterskin", "barrel", "rations", "raft", "club", /* 10 */ + "mace", "dagger", "greatsword", "longsword", "staff", /* 15 */ + "shield", "shortsword", "jacket", "pants", "leggings", /* 20 */ + "gauntlets", "sleeves", "gloves", "helmet", "skullcap", /* 25 */ + "boots", "sandals", "cloak", "book", "quill", /* 30 */ + "belt", "ring", "bracelet", "bottle", "keg", /* 35 */ + "mask", "earring", "scarf", "bracer", "\n"}; /* EX_x */ -const char *exit_bits[] = {"DOOR", "CLOSED", "LOCKED", "PICKPROOF", "HIDDEN", "DESCRIPT", "\n"}; +const std::string_view exit_bits[] = {"DOOR", "CLOSED", "LOCKED", "PICKPROOF", "HIDDEN", "DESCRIPT", "\n"}; /* SEX_x */ -const char *genders[NUM_SEXES + 1] = {"Neuter", "Male", "Female", "Nonbinary", "\n"}; +const std::string_view genders[NUM_SEXES + 1] = {"Neuter", "Male", "Female", "Nonbinary", "\n"}; /* STANCE_x */ -const char *stance_types[NUM_STANCES + 1] = { +const std::string_view stance_types[NUM_STANCES + 1] = { "dead", "mortally wounded", "incapacitated", "stunned", "sleeping", "resting", "alert", "fighting", "\n"}; /* POS_x */ -const char *position_types[NUM_POSITIONS + 1] = {"prone", "sitting", "kneeling", "standing", "flying", "\n"}; +const std::string_view position_types[NUM_POSITIONS + 1] = {"prone", "sitting", "kneeling", "standing", "flying", "\n"}; /* PLR_x */ -const char *player_bits[NUM_PLR_FLAGS + 1] = {"KILLER", "THIEF", - "FROZEN", "DONTSET", - "WRITING", "MAILING", - "AUTOSAVE", "SITEOK", - "NOSHOUT", "NOTITLE", - "DELETED", "LOADRM", - "!WIZL", "!DEL", - "INVST", "CRYO", - "MEDITATING", "CASTING", - "BOUND", "SCRIBE", - "TEACHING", "NAMENEEDSAPPROVE", - "RENAME", "REMOVING", - "SAVING", "GOTSTARS", - "\n"}; +const std::string_view player_bits[NUM_PLR_FLAGS + 1] = {"KILLER", "THIEF", + "FROZEN", "DONTSET", + "WRITING", "MAILING", + "AUTOSAVE", "SITEOK", + "NOSHOUT", "NOTITLE", + "DELETED", "LOADRM", + "!WIZL", "!DEL", + "INVST", "CRYO", + "MEDITATING", "CASTING", + "BOUND", "SCRIBE", + "TEACHING", "NAMENEEDSAPPROVE", + "RENAME", "REMOVING", + "SAVING", "GOTSTARS", + "\n"}; /* MOB_x */ -const char *action_bits[NUM_MOB_FLAGS + 1] = {"SPEC", "SENTINEL", "SCAVENGER", "ISNPC", - "AWARE", "AGGR", "STAY_ZONE", "WIMPY", - "AGGR_EVIL", "AGGR_GOOD", "AGGR_NEUTRAL", "MEMORY", - "HELPER", "!CHARM", "!SUMMN", "!SLEEP", - "!BASH", "!BLIND", "MOUNTABLE", "NO_EQ_RESTRICT", - "FAST_TRACK", "SLOW_TRACK", "CASTINGDONTUSE", "SUMMONED_MOUNT", - "AQUATIC", "AGGR_EVIL_RACE", "AGGR_GOOD_RACE", "!SILENCE", - "NOVICIOUS", "TEACHER", "ANIMATED", "PEACEFUL", - "!POISON", "ILLUSORY", "PLAYER_PHANTASM", "!CLASS_AI", - "!SCRIPT", "PEACEKEEPER", "PROTECTOR", "PET", - "MEDITATEDONTUSE," "\n"}; +const std::string_view action_bits[NUM_MOB_FLAGS + 1] = {"SPEC", + "SENTINEL", + "SCAVENGER", + "ISNPC", + "AWARE", + "AGGR", + "STAY_ZONE", + "WIMPY", + "AGGR_EVIL", + "AGGR_GOOD", + "AGGR_NEUTRAL", + "MEMORY", + "HELPER", + "!CHARM", + "!SUMMN", + "!SLEEP", + "!BASH", + "!BLIND", + "MOUNTABLE", + "NO_EQ_RESTRICT", + "FAST_TRACK", + "SLOW_TRACK", + "CASTINGDONTUSE", + "SUMMONED_MOUNT", + "AQUATIC", + "AGGR_EVIL_RACE", + "AGGR_GOOD_RACE", + "!SILENCE", + "NOVICIOUS", + "TEACHER", + "ANIMATED", + "PEACEFUL", + "!POISON", + "ILLUSORY", + "PLAYER_PHANTASM", + "!CLASS_AI", + "!SCRIPT", + "PEACEKEEPER", + "PROTECTOR", + "PET", + "MEDITATEDONTUSE," + "\n"}; /* PRF_x */ -const char *preference_bits[NUM_PRF_FLAGS + 1] = { +const std::string_view preference_bits[NUM_PRF_FLAGS + 1] = { "BRIEF", "COMPACT", "DEAF", "!TELL", "OLCCOMM", "LINENUMS", "AUTOLOOT", "AUTOEXIT", "!HASSLE", "QUEST", "SUMMON", "!REPEAT", "LIGHT", "COLOR1", "COLOR2", "!WIZNET", "LOG1", "LOG2", "!AUCTION", "!GOSSIP", "!HINTS", "ROOMFLAG", "!PETITION", "AUTOSPLIT", @@ -86,10 +119,10 @@ const char *preference_bits[NUM_PRF_FLAGS + 1] = { "AUTOTREAS", "STK_OBJ", "STK_MOB", "SACRIFICIAL", "\n"}; /* PRV_x */ -const char *privilege_bits[NUM_PRV_FLAGS + 1] = {"CLAN_ADMIN", "TITLE", "ANON_TOGGLE", "AUTO_GAIN", "\n"}; +const std::string_view privilege_bits[NUM_PRV_FLAGS + 1] = {"CLAN_ADMIN", "TITLE", "ANON_TOGGLE", "AUTO_GAIN", "\n"}; /* CON_x */ -const char *connected_types[NUM_CON_MODES + 1] = { +const std::string_view connected_types[NUM_CON_MODES + 1] = { "Playing", "Disconnecting", "Get name", "Confirm name", "Get password", "Get new PW", "Confirm new PW", "Select gender", "Select class", "Reading MOTD", "Main Menu", "Get descript.", "Changing PW 1", "Changing PW 2", "Changing PW 3", "Self-Delete 1", "Self-Delete 2", "Select race", @@ -100,7 +133,7 @@ const char *connected_types[NUM_CON_MODES + 1] = { "Select dex", "Select con", "select int", "Select wis", "Select cha", "\n"}; /* WEAR_x - for eq list */ -const char *where[NUM_WEARS] = { +const std::string_view where[NUM_WEARS] = { "<used as light> ", "<worn on finger> ", "<worn on finger> ", "<worn around neck> ", "<worn around neck> ", "<worn on body> ", "<worn on head> ", "<worn on legs> ", "<worn on feet> ", "<worn on hands> ", "<worn on arms> ", "<worn as shield> ", @@ -110,38 +143,38 @@ const char *where[NUM_WEARS] = { "<worn in right ear> ", "<worn as badge> ", "<attached to belt> ", "<hovering> "}; /* WEAR_x - for stat */ -const char *equipment_types[NUM_WEARS + 1] = {"Used as light", - "Worn on right finger", - "Worn on left finger", - "First worn around Neck", - "Second worn around Neck", - "Worn on body", - "Worn on head", - "Worn on legs", - "Worn on feet", - "Worn on hands", - "Worn on arms", - "Worn as shield", - "Worn about body", - "Worn around waist", - "Worn around right wrist", - "Worn around left wrist", - "Wielded", - "Wielded secondary", - "Held", - "Held", - "Wielded two-handed", - "Worn on eyes", - "Worn on face", - "Worn in left ear", - "Worn in right ear", - "Worn as badge", - "Attached to belt", - "Hovering", - "\n"}; +const std::string_view equipment_types[NUM_WEARS + 1] = {"Used as light", + "Worn on right finger", + "Worn on left finger", + "First worn around Neck", + "Second worn around Neck", + "Worn on body", + "Worn on head", + "Worn on legs", + "Worn on feet", + "Worn on hands", + "Worn on arms", + "Worn as shield", + "Worn about body", + "Worn around waist", + "Worn around right wrist", + "Worn around left wrist", + "Wielded", + "Wielded secondary", + "Held", + "Held", + "Wielded two-handed", + "Worn on eyes", + "Worn on face", + "Worn in left ear", + "Worn in right ear", + "Worn as badge", + "Attached to belt", + "Hovering", + "\n"}; /* WEAR_x - for scripts */ -const char *wear_positions[NUM_WEARS + 1] = { +const std::string_view wear_positions[NUM_WEARS + 1] = { "light", "rfinger", "lfinger", "neck1", "neck2", "body", "head", "legs", "feet", "hands", "arms", "shield", "aboutbody", "waist", "rwrist", "lwrist", "wield", "wield2", "held", "held2", "2hwield", "eyes", "face", "lear", "rear", "badge", "belt", "hover", "\n"}; @@ -184,12 +217,12 @@ const int wear_flags[NUM_WEARS] = { }; /* ITEM_WEAR_ (wear bitvector) */ -const char *wear_bits[NUM_ITEM_WEAR_FLAGS + 1] = { +const std::string_view wear_bits[NUM_ITEM_WEAR_FLAGS + 1] = { "TAKE", "FINGER", "NECK", "BODY", "HEAD", "LEGS", "FEET", "HANDS", "ARMS", "SHIELD", "ABOUT", "WAIST", "WRIST", "WIELD", "HOLD", "2HWIELD", "EYES", "FACE", "EAR", "BADGE", "OBELT", "HOVER", "\n"}; /* ITEM_x (extra bits) */ -const char *extra_bits[NUM_ITEM_FLAGS + 1] = { +const std::string_view extra_bits[NUM_ITEM_FLAGS + 1] = { "GLOW", "HUM", "!RENT", "!BERSERKER", "!INVIS", "INVISIBLE", "MAGIC", "!DROP", "PERMANENT", "!GOOD", "!EVIL", "!NEUTRAL", "!SORCERER", "!CLERIC", "!ROGUE", "!WARRIOR", "!SELL", "!PALADIN", @@ -201,85 +234,60 @@ const char *extra_bits[NUM_ITEM_FLAGS + 1] = { "!GARGANTUAN", "!COLOSSAL", "!TITANIC", "!MOUNTAINOUS", "!ARBOREAN", "\n"}; /* APPLY_x */ -const char *apply_types[NUM_APPLY_TYPES + 1] = { +const std::string_view apply_types[NUM_APPLY_TYPES + 1] = { "NONE", "STR", "DEX", "INT", "WIS", "CON", "CHA", "CLASS", "LEVEL", "AGE", "CHAR_WEIGHT", "CHAR_HEIGHT", "MAXMANA", "HITPOINTS", "MAXMOVE", "GOLD", "EXP", "ARMOR", "HITROLL", "DAMROLL", "SAVING_PARA", "SAVING_ROD", "SAVING_PETRI", "SAVING_BREATH", "SAVING_SPELL", "SIZE", "HIT_REGEN", "FOCUS", "PERCEPTION", "HIDDENNESS", "COMPOSITION", "\n"}; /* APPLY_x */ -const char *apply_abbrevs[NUM_APPLY_TYPES + 1] = {"none", "str", "dex", "int", "wis", "con", "cha", "cls", - "lvl", "age", "lbs", "in.", "mp", "hp", "mv", "gld", - "exp", "ac", "hr", "dr", "spa", "sr", "spe", "sb", - "ss", "size", "regen", "mregen", "perc", "hide", "\n"}; +const std::string_view apply_abbrevs[NUM_APPLY_TYPES + 1] = { + "none", "str", "dex", "int", "wis", "con", "cha", "cls", "lvl", "age", "lbs", + "in.", "mp", "hp", "mv", "gld", "exp", "ac", "hr", "dr", "spa", "sr", + "spe", "sb", "ss", "size", "regen", "mregen", "perc", "hide", "\n"}; /* CONT_x */ -const char *container_bits[] = { +const std::string_view container_bits[] = { "CLOSEABLE", "PICKPROOF", "CLOSED", "LOCKED", "\n", }; -const char *carry_desc[] = {"Weightless", "Featherweight", "Paltry", "Very Light", "Light", "Moderate", - "Burdensome", "Very Heavy", "Instant hernia", "Immobilizing", "Intolerable"}; - -const char *weekdays[DAYS_PER_WEEK] = {"the Day of the Run", "the Day of the Fight", "the Day of Remembrance", - "the Day of Storm", "the Day of Fire", "the Day of Conquest", - "the Day of Rest"}; - -const char *rolls_abils_result[] = {"Fantastic ", "Very Good ", "Average ", "Mediocre ", "Bad "}; - -const char *month_name[MONTHS_PER_YEAR] = {"Month of Winter", /* 0 */ - "Month of the Dark Destiny", - "Month of the Arcane Power", - "Month of the Moonless Night", - "Month of the Spring", - "Month of the Diabolical Awakening", - "Month of Resistance", - "Month of the Shal Du Stauk", - "Month of the Firestorm", - "Month of the Long Journey", - "Month of Discovery", - "Month of the Stranger", - "Month of Heroes and Valor", - "Month of the Great Deceit", - "Month of the Rift War", - "Month of the Wicked Deception"}; +const std::string_view carry_desc[] = {"Weightless", "Featherweight", "Paltry", "Very Light", + "Light", "Moderate", "Burdensome", "Very Heavy", + "Instant hernia", "Immobilizing", "Intolerable"}; + +const std::string_view weekdays[DAYS_PER_WEEK] = { + "the Day of the Run", "the Day of the Fight", "the Day of Remembrance", "the Day of Storm", + "the Day of Fire", "the Day of Conquest", "the Day of Rest"}; + +const std::string_view rolls_abils_result[] = {"Fantastic ", "Very Good ", "Average ", "Mediocre ", + "Bad "}; + +const std::string_view month_name[MONTHS_PER_YEAR] = {"Month of Winter", /* 0 */ + "Month of the Dark Destiny", + "Month of the Arcane Power", + "Month of the Moonless Night", + "Month of the Spring", + "Month of the Diabolical Awakening", + "Month of Resistance", + "Month of the Shal Du Stauk", + "Month of the Firestorm", + "Month of the Long Journey", + "Month of Discovery", + "Month of the Stranger", + "Month of Heroes and Valor", + "Month of the Great Deceit", + "Month of the Rift War", + "Month of the Wicked Deception"}; const int sharp[] = {0, 0, 0, 1, /* Slashing */ 0, 0, 0, 0, /* Bludgeon */ 0, 0, 0, 0}; /* Pierce */ -const char *default_prompts[][2] = {{"Basic", "&0%hhp %vmv>&0 "}, - {"Colorized Basic", "&1&b%h&0&1hp &2&b%v&0&2mv&0> "}, - {"Basic Percentages", "&1&b%ph&0&1hp &2&b%pv&0&2mv&0> "}, - {"Full-Featured", - "&6Opponent&0: &4&b%o &7&b/ &0&6Tank&0: &4&b%t%_&0&1%h&0(&1&b%H&0)" - "hitp &2%v&0(&2&b%V&0)&7move&0> "}, - {"Standard", - "&2&b<&0&2%hh&0(&2&b%HH&0) &2%vv&0(&2&b%VV&0)&2&b>%_" - "&0<%t&0>:<&0%o&0> "}, - {"Complete w/ Spells", - "&2&b<&0&2%hh&0(&2&b%HH&0) &2%vv&0(&2&b%VV&0)&2&b> " - "<&0&2%aA&2&b> <&0%l&2&b>%_&0<%t&0>:<&0%o&0> "}, - {"Complete w/ Exp", - "&2&b<&0&2%hh&0(&2&b%HH&0) &2%vv&0(&2&b%VV&0)&2&b> " - "<&0&2%aA&2&b> <&0%e&2&b>%_&0<%t&0>:<&0%o&0> "}, - {"Complete w/ Hide Pts", - "&2&b<&0&2%hh&0(&2&b%HH&0) &2%vv&0(&2&b%VV&0)&2&b> " - "<&0&2%aA&2&b> <&0&2%ih&2&b>%_&0<%t&0>:<&0%o&0> "}, - {"Complete w/ Rage", - "&2&b<&0&2%hh&0(&2&b%HH&0) &2%vv&0(&2&b%VV&0)&2&b> " - "<&0&2%aA&2&b> <&0&2%rr&2&b>%_&0<%t&0>:<&0%o&0> "}, - {"Complete w/ 1st Aid", - "&2&b<&0&2%hh&0(&2&b%HH&0) &2%vv&0(&2&b%VV&0)&2&b> " - "<&0&2%aA&2&b> <&0&2%df&2&b>%_&0<%t&0>:<&0%o&0> "}, - {nullptr, nullptr}}; - /* The following functions replace the hard coded bonus */ /* tables for attributes. This was done for the conversion */ /* to the 100 base system. These functions closely approximate */ /* the older 18 base hard coded arrays. -gurlaek 6/24/1999 */ - void load_stat_bonus(void) { int x; diff --git a/src/constants.hpp b/src/constants.hpp index a1e7ba08..a5a36bbb 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -24,30 +24,56 @@ constexpr char discord_app_id[] = "998826809686765569"; constexpr char fierymud_icon[] = "https://www.fierymud.org/images/fiery64.png"; constexpr char fierymud_url[] = "https://www.fierymud.org"; -extern const char *number_words[]; -extern const char *minor_creation_items[]; -extern const char *exit_bits[]; -extern const char *genders[NUM_SEXES + 1]; -extern const char *stance_types[NUM_STANCES + 1]; -extern const char *position_types[NUM_POSITIONS + 1]; -extern const char *player_bits[NUM_PLR_FLAGS + 1]; -extern const char *action_bits[NUM_MOB_FLAGS + 1]; -extern const char *preference_bits[NUM_PRF_FLAGS + 1]; -extern const char *privilege_bits[NUM_PRV_FLAGS + 1]; -extern const char *connected_types[NUM_CON_MODES + 1]; -extern const char *where[NUM_WEARS]; -extern const char *equipment_types[NUM_WEARS + 1]; -extern const char *wear_positions[NUM_WEARS + 1]; +extern const std::string_view number_words[]; +extern const std::string_view minor_creation_items[]; +extern const std::string_view exit_bits[]; +extern const std::string_view genders[NUM_SEXES + 1]; +extern const std::string_view stance_types[NUM_STANCES + 1]; +extern const std::string_view position_types[NUM_POSITIONS + 1]; +extern const std::string_view player_bits[NUM_PLR_FLAGS + 1]; +extern const std::string_view action_bits[NUM_MOB_FLAGS + 1]; +extern const std::string_view preference_bits[NUM_PRF_FLAGS + 1]; +extern const std::string_view privilege_bits[NUM_PRV_FLAGS + 1]; +extern const std::string_view connected_types[NUM_CON_MODES + 1]; +extern const std::string_view where[NUM_WEARS]; +extern const std::string_view equipment_types[NUM_WEARS + 1]; +extern const std::string_view wear_positions[NUM_WEARS + 1]; extern int wear_order_index[NUM_WEARS]; extern const int wear_flags[NUM_WEARS]; -extern const char *wear_bits[NUM_ITEM_WEAR_FLAGS + 1]; -extern const char *extra_bits[NUM_ITEM_FLAGS + 1]; -extern const char *apply_types[NUM_APPLY_TYPES + 1]; -extern const char *apply_abbrevs[NUM_APPLY_TYPES + 1]; -extern const char *container_bits[]; -extern const char *carry_desc[]; -extern const char *weekdays[DAYS_PER_WEEK]; -extern const char *rolls_abils_result[]; -extern const char *month_name[MONTHS_PER_YEAR]; +extern const std::string_view wear_bits[NUM_ITEM_WEAR_FLAGS + 1]; +extern const std::string_view extra_bits[NUM_ITEM_FLAGS + 1]; +extern const std::string_view apply_types[NUM_APPLY_TYPES + 1]; +extern const std::string_view apply_abbrevs[NUM_APPLY_TYPES + 1]; +extern const std::string_view container_bits[]; +extern const std::string_view carry_desc[]; +extern const std::string_view weekdays[DAYS_PER_WEEK]; +extern const std::string_view rolls_abils_result[]; +extern const std::string_view month_name[MONTHS_PER_YEAR]; extern const int sharp[]; -extern const char *default_prompts[][2]; \ No newline at end of file + +constexpr std::array<std::array<std::string_view, 2>, 11> default_prompts = {{ + {"Basic", "&0%hhp %vmv>&0 "}, + {"Colorized Basic", "&1&b%h&0&1hp &2&b%v&0&2mv&0> "}, + {"Basic Percentages", "&1&b%ph&0&1hp &2&b%pv&0&2mv&0> "}, + {"Full-Featured", + "&6Opponent&0: &4&b%o &7&b/ &0&6Tank&0: &4&b%t%_&0&1%h&0(&1&b%H&0)" + "hitp &2%v&0(&2&b%V&0)&7move&0> "}, + {"Standard", + "&2&b<&0&2%hh&0(&2&b%HH&0) &2%vv&0(&2&b%VV&0)&2&b>%_" + "&0<%t&0>:<&0%o&0> "}, + {"Complete w/ Spells", + "&2&b<&0&2%hh&0(&2&b%HH&0) &2%vv&0(&2&b%VV&0)&2&b> " + "<&0&2%aA&2&b> <&0%l&2&b>%_&0<%t&0>:<&0%o&0> "}, + {"Complete w/ Exp", + "&2&b<&0&2%hh&0(&2&b%HH&0) &2%vv&0(&2&b%VV&0)&2&b> " + "<&0&2%aA&2&b> <&0%e&2&b>%_&0<%t&0>:<&0%o&0> "}, + {"Complete w/ Hide Pts", + "&2&b<&0&2%hh&0(&2&b%HH&0) &2%vv&0(&2&b%VV&0)&2&b> " + "<&0&2%aA&2&b> <&0&2%ih&2&b>%_&0<%t&0>:<&0%o&0> "}, + {"Complete w/ Rage", + "&2&b<&0&2%hh&0(&2&b%HH&0) &2%vv&0(&2&b%VV&0)&2&b> " + "<&0&2%aA&2&b> <&0&2%rr&2&b>%_&0<%t&0>:<&0%o&0> "}, + {"Complete w/ 1st Aid", + "&2&b<&0&2%hh&0(&2&b%HH&0) &2%vv&0(&2&b%VV&0)&2&b> " + "<&0&2%aA&2&b> <&0&2%df&2&b>%_&0<%t&0>:<&0%o&0> "}, +}}; diff --git a/src/corpse_save.cpp b/src/corpse_save.cpp index 74d37b9a..c143b7b1 100644 --- a/src/corpse_save.cpp +++ b/src/corpse_save.cpp @@ -361,29 +361,32 @@ void destroy_corpse(ObjData *corpse) { void show_corpses(CharData *ch, char *argument) { corpse_data *entry; + std::string location; if (corpse_control.count) { char_printf(ch, "Id Corpse Level Decomp Location\n" "-------------------------------------------------------------------\n"); for (entry = SENTINEL->next; entry != SENTINEL; entry = entry->next) { - if (!strncasecmp(entry->corpse->short_description, "the corpse of ", 14)) - strcpy(buf1, entry->corpse->short_description + 14); - else - strcpy(buf1, entry->corpse->name); + std::string name = fmt::format("{:-4d}{:<20}{:5d} {:6d} ", entry->id, + matches_start(entry->corpse->short_description, "the corpse of ") + ? entry->corpse->short_description + strlen("the corpse of ") + : entry->corpse->short_description, + GET_OBJ_LEVEL(entry->corpse), GET_OBJ_DECOMP(entry->corpse)); + location.clear(); if (entry->corpse->carried_by) - sprintf(buf2, "carried by %s", GET_NAME(entry->corpse->carried_by)); + location += fmt::format("carried by {}", GET_NAME(entry->corpse->carried_by)); else if (entry->corpse->in_room != NOWHERE) - sprintf(buf2, "%s @L[&0%d@L]&0", world[entry->corpse->in_room].name, - world[entry->corpse->in_room].vnum); + location += fmt::format("{} @L[&0{}@L]&0", world[entry->corpse->in_room].name, + world[entry->corpse->in_room].vnum); else if (entry->corpse->in_obj) - sprintf(buf2, "in %s", entry->corpse->in_obj->short_description); + location += fmt::format("in {}", entry->corpse->in_obj->short_description); else if (entry->corpse->worn_by) - sprintf(buf2, "worn by %s", GET_NAME(entry->corpse->worn_by)); + location += fmt::format("worn by {}", GET_NAME(entry->corpse->worn_by)); else - strcpy(buf2, "an unknown location"); - char_printf(ch, "{:-4d}{:<20}{:5d} {:6d} {:<25s}\n", entry->id, buf1, GET_OBJ_LEVEL(entry->corpse), - GET_OBJ_DECOMP(entry->corpse), buf2); + location += "an unknown location"; + char_printf(ch, "{:-4d}{:<20}{:5d} {:6d} {:<25s}\n", entry->id, name, GET_OBJ_LEVEL(entry->corpse), + GET_OBJ_DECOMP(entry->corpse), location); } } else char_printf(ch, "There are no player corpses in the game.\n"); diff --git a/src/db.cpp b/src/db.cpp index fa20f6a7..d475d6a3 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -54,7 +54,6 @@ #include <math.h> #include <sys/stat.h> -void init_clans(void); char err_buf[MAX_STRING_LENGTH]; PlayerSpecialData dummy_mob; @@ -416,7 +415,7 @@ int get_copper(int i) return 0; /*copper calculations */ - copper = (random_number(1, 150)) * mob_proto[i].player.level; + copper = (random_number(1, 150))*mob_proto[i].player.level; sfactor = (int)((sfactor + cfactor + zfactor) / 3); copper = (int)((float)((sfactor / 100.0) * copper)); @@ -540,7 +539,7 @@ void boot_world(void) { /* Must happen after loading the player index */ log("Booting Clans."); - init_clans(); + clan_repository.init_clans(); log("Booting boards."); board_init(); @@ -557,7 +556,7 @@ void boot_db(void) { log(" Skills."); init_skills(); - + log("Assigning skills and spells to classes."); assign_class_skills(); @@ -625,8 +624,7 @@ void boot_db(void) { log("Booting quests."); boot_quests(); - CREATE(boot_time, time_t, 1); - *boot_time = time(0); + boot_time = time(0); log("Boot db -- DONE."); } @@ -689,10 +687,6 @@ void destroy_db(void) { /* Rooms */ for (cnt = 0; cnt <= top_of_world; cnt++) { - if (world[cnt].name) - free(world[cnt].name); - if (world[cnt].description) - free(world[cnt].description); free_extra_descriptions(world[cnt].ex_description); /* free any assigned scripts */ @@ -940,7 +934,10 @@ void index_boot(int mode) { } /* first, count the number of records in the file so we can malloc */ - fscanf(index, "%s\n", buf1); + if (fscanf(index, "%s\n", buf1) != 1) { + log("Error reading from index file."); + exit(1); + } while (*buf1 != '$') { sprintf(buf2, "%s/%s", prefix, buf1); if (!(db_file = fopen(buf2, "r"))) { @@ -955,7 +952,10 @@ void index_boot(int mode) { } fclose(db_file); - fscanf(index, "%s\n", buf1); + if (fscanf(index, "%s\n", buf1) != 1) { + log("Error reading from index file."); + exit(1); + } } /* Exit if 0 records, unless this is shops */ @@ -992,7 +992,10 @@ void index_boot(int mode) { } rewind(index); - fscanf(index, "%s\n", buf1); + if (fscanf(index, "%s\n", buf1) != 1) { + log("Error reading from index file."); + exit(1); + } while (*buf1 != '$') { sprintf(buf2, "%s/%s", prefix, buf1); if (!(db_file = fopen(buf2, "r"))) { @@ -1018,7 +1021,10 @@ void index_boot(int mode) { } fclose(db_file); - fscanf(index, "%s\n", buf1); + if (fscanf(index, "%s\n", buf1) != 1) { + log("Error reading from index file."); + exit(1); + } } /* sort the help index */ @@ -1134,8 +1140,8 @@ void parse_room(FILE *fl, int virtual_nr) { exit(1); } /* t[0] is the zone number; ignored with the zone-file system */ - /* room_flags is a flagvector array */ - world[room_nr].room_flags[0] = asciiflag_conv(flags); + /* flags is a flagvector array */ + world[room_nr].flags[0] = asciiflag_conv(flags); world[room_nr].sector_type = t[2]; world[room_nr].func = nullptr; @@ -1396,30 +1402,30 @@ void parse_simple_mob(FILE *mob_f, int i, int nr) { mob_proto[i].player.height = 198; - mob_proto[i].points.coins[PLATINUM] = 0; - mob_proto[i].points.coins[GOLD] = 0; - mob_proto[i].points.coins[SILVER] = 0; - mob_proto[i].points.coins[COPPER] = 0; + mob_proto[i].points.money[PLATINUM] = 0; + mob_proto[i].points.money[GOLD] = 0; + mob_proto[i].points.money[SILVER] = 0; + mob_proto[i].points.money[COPPER] = 0; /*Money adder */ k = j = 0; k = get_copper(i); j = (60 * k) / 100; - mob_proto[i].points.coins[PLATINUM] = j / PLATINUM_SCALE; + mob_proto[i].points.money[PLATINUM] = j / PLATINUM_SCALE; j = ((20 * k) / 100) + (j % PLATINUM_SCALE); - mob_proto[i].points.coins[GOLD] = j / GOLD_SCALE; + mob_proto[i].points.money[GOLD] = j / GOLD_SCALE; j = ((18 * k) / 100) + (j % GOLD_SCALE); - mob_proto[i].points.coins[SILVER] = j / SILVER_SCALE; + mob_proto[i].points.money[SILVER] = j / SILVER_SCALE; if (mob_proto[i].player.level > 20) j = (k / 200) + (j % SILVER_SCALE); j = ((2 * k) / 100) + (j % SILVER_SCALE); - mob_proto[i].points.coins[COPPER] = j / COPPER_SCALE; + mob_proto[i].points.money[COPPER] = j / COPPER_SCALE; - mob_proto[i].points.coins[PLATINUM] = - std::max(0, mob_proto[i].points.coins[PLATINUM] + GET_EX_PLATINUM(mob_proto + i)); - mob_proto[i].points.coins[GOLD] = std::max(0, mob_proto[i].points.coins[GOLD] + GET_EX_GOLD(mob_proto + i)); - mob_proto[i].points.coins[COPPER] = std::max(0, mob_proto[i].points.coins[COPPER] + GET_EX_COPPER(mob_proto + i)); - mob_proto[i].points.coins[SILVER] = std::max(0, mob_proto[i].points.coins[SILVER] + GET_EX_SILVER(mob_proto + i)); + mob_proto[i].points.money[PLATINUM] = + std::max(0, mob_proto[i].points.money[PLATINUM] + GET_EX_PLATINUM(mob_proto + i)); + mob_proto[i].points.money[GOLD] = std::max(0, mob_proto[i].points.money[GOLD] + GET_EX_GOLD(mob_proto + i)); + mob_proto[i].points.money[COPPER] = std::max(0, mob_proto[i].points.money[COPPER] + GET_EX_COPPER(mob_proto + i)); + mob_proto[i].points.money[SILVER] = std::max(0, mob_proto[i].points.money[SILVER] + GET_EX_SILVER(mob_proto + i)); if ((mob_proto[i].mob_specials.ex_armor != 100)) mob_proto[i].points.armor = mob_proto[i].mob_specials.ex_armor + @@ -1603,7 +1609,7 @@ void parse_mobile(FILE *mob_f, int nr) { tmpptr = mob_proto[i].player.short_descr = fread_string(mob_f, buf2); if (tmpptr && *tmpptr) if (!strcasecmp(fname(tmpptr), "a") || !strcasecmp(fname(tmpptr), "an") || !strcasecmp(fname(tmpptr), "the")) - *tmpptr = LOWER(*tmpptr); + *tmpptr = to_lower(*tmpptr); mob_proto[i].player.long_descr = fread_string(mob_f, buf2); mob_proto[i].player.description = fread_string(mob_f, buf2); mob_proto[i].player.title = nullptr; @@ -1766,11 +1772,11 @@ char *parse_object(FILE *obj_f, int nr) { tmpptr = obj_proto[i].short_description = fread_string(obj_f, buf2); if (*tmpptr) if (!strcasecmp(fname(tmpptr), "a") || !strcasecmp(fname(tmpptr), "an") || !strcasecmp(fname(tmpptr), "the")) - *tmpptr = LOWER(*tmpptr); + *tmpptr = to_lower(*tmpptr); tmpptr = obj_proto[i].description = fread_string(obj_f, buf2); if (tmpptr && *tmpptr) - *tmpptr = UPPER(*tmpptr); + *tmpptr = to_upper(*tmpptr); obj_proto[i].action_description = fread_string(obj_f, buf2); /* *** numeric data *** */ @@ -2632,10 +2638,6 @@ void free_char(CharData *ch) { free_trophy(ch); free_aliases(GET_ALIASES(ch)); - /* Remove runtime link to clan */ - if (GET_CLAN_MEMBERSHIP(ch)) - GET_CLAN_MEMBERSHIP(ch)->player = nullptr; - if (GET_WIZ_TITLE(ch)) free(GET_WIZ_TITLE(ch)); if (GET_PERM_TITLES(ch)) { @@ -3063,7 +3065,7 @@ bool _parse_name(char *arg, char *name) { test[0] = 0; for (i = 0; (*name = *arg); arg++, i++, name++) { - *(test + i) = LOWER(*arg); + *(test + i) = to_lower(*arg); if ((*arg < 0) || !isalpha(*arg) || (i > 15) || (i && (*(test + i) != *arg))) return (true); } diff --git a/src/defines.hpp b/src/defines.hpp index a5569fb5..89729641 100644 --- a/src/defines.hpp +++ b/src/defines.hpp @@ -1156,7 +1156,7 @@ #define ITEM_ANTI_COLOSSAL 49 #define ITEM_ANTI_TITANIC 50 #define ITEM_ANTI_MOUNTAINOUS 51 -#define ITEM_ANTI_ARBOREAN 52 /* Not usable by Arboreans */ +#define ITEM_ANTI_ARBOREAN 52 /* Not usable by Arboreans */ #define NUM_ITEM_FLAGS 53 /* Modifier constants used with obj effects ('A' fields) */ diff --git a/src/dg_comm.cpp b/src/dg_comm.cpp index ce773fae..b14c804a 100644 --- a/src/dg_comm.cpp +++ b/src/dg_comm.cpp @@ -13,12 +13,11 @@ #include "db.hpp" #include "dg_scripts.hpp" #include "handler.hpp" +#include "logging.hpp" #include "screen.hpp" #include "structs.hpp" #include "sysdep.hpp" #include "utils.hpp" -#include "logging.hpp" - /* same as any_one_arg except that it stops at punctuation */ char *any_one_name(char *argument, char *first_arg) { @@ -32,7 +31,7 @@ char *any_one_name(char *argument, char *first_arg) { for (arg = first_arg; *argument && !isspace(*argument) && (!ispunct(*argument) || *argument == '#' || *argument == '-'); arg++, argument++) - *arg = LOWER(*argument); + *arg = to_lower(*argument); *arg = '\0'; return argument; diff --git a/src/dg_mobcmd.cpp b/src/dg_mobcmd.cpp index 0a4a7d2f..cdc2a3e7 100644 --- a/src/dg_mobcmd.cpp +++ b/src/dg_mobcmd.cpp @@ -1143,7 +1143,7 @@ ACMD(do_mroomflag) { if (target == NOWHERE) mob_log(ch, "target is an invalid room"); - flag = search_block(arg2, room_bits, false); + flag = search_block(std::string_view{arg2}, room_bits, false); if (flag < 0) { sprintf(buf, "mroomflag called with unknown flag '%s'", arg2); diff --git a/src/dg_olc.cpp b/src/dg_olc.cpp index 6e93ef79..1dca5c95 100644 --- a/src/dg_olc.cpp +++ b/src/dg_olc.cpp @@ -12,9 +12,11 @@ #include "dg_olc.hpp" +#include "bitflags.hpp" #include "comm.hpp" #include "conf.hpp" #include "db.hpp" +#include "dg_scripts.hpp" #include "events.hpp" #include "interpreter.hpp" #include "logging.hpp" @@ -115,50 +117,49 @@ void trigedit_setup_existing(DescriptorData *d, int rtrg_num) { void trigedit_disp_menu(DescriptorData *d) { TrigData *trig = OLC_TRIG(d); const char *attach_type; - char trgtypes[256]; + std::string_view trgtypes; get_char_cols(d->character); if (trig->attach_type == OBJ_TRIGGER) { attach_type = "Objects"; - sprintbit(GET_TRIG_TYPE(trig), otrig_types, trgtypes); + trgtypes = sprintbit(GET_TRIG_TYPE(trig), otrig_types); } else if (trig->attach_type == WLD_TRIGGER) { attach_type = "Rooms"; - sprintbit(GET_TRIG_TYPE(trig), wtrig_types, trgtypes); + trgtypes = sprintbit(GET_TRIG_TYPE(trig), wtrig_types); } else { attach_type = "Mobiles"; - sprintbit(GET_TRIG_TYPE(trig), trig_types, trgtypes); + trgtypes = sprintbit(GET_TRIG_TYPE(trig), trig_types); } - sprintf(buf, + char_printf(d->character, #if defined(CLEAR_SCREEN) - "" + "" #endif - "Trigger Editor [%s%d%s]\n\n" - "%s1)%s Name : %s%s\n" - "%s2)%s Intended for : %s%s\n" - "%s3)%s Trigger types: %s%s\n" - "%s4)%s Numeric Arg : %s%d\n" - "%s5)%s Arguments : %s%s\n" - "%s6)%s Commands:\n%s%s\n" - "%sQ)%s Quit\n" - "Enter Choice:\n", - grn, OLC_NUM(d), nrm, /* vnum on the title line */ - grn, nrm, yel, GET_TRIG_NAME(trig), /* name */ - grn, nrm, yel, attach_type, /* attach type */ - grn, nrm, yel, trgtypes, /* greet/drop/etc */ - grn, nrm, yel, trig->narg, /* numeric arg */ - grn, nrm, yel, trig->arglist, /* strict arg */ - grn, nrm, cyn, OLC_STORAGE(d), /* the command list */ - grn, nrm); /* quit colors */ + "Trigger Editor [{}{}{}]\n\n" + "{}1){} Name : {}{}\n" + "{}2){} Intended for : {}{}\n" + "{}3){} Trigger types: {}{}\n" + "{}4){} Numeric Arg : {}{}\n" + "{}5){} Arguments : {}{}\n" + "{}6){} Commands:\n{}{}\n" + "{}Q){} Quit\n" + "Enter Choice:\n", + grn, OLC_NUM(d), nrm, /* vnum on the title line */ + grn, nrm, yel, GET_TRIG_NAME(trig), /* name */ + grn, nrm, yel, attach_type, /* attach type */ + grn, nrm, yel, trgtypes, /* greet/drop/etc */ + grn, nrm, yel, trig->narg, /* numeric arg */ + grn, nrm, yel, trig->arglist, /* strict arg */ + grn, nrm, cyn, OLC_STORAGE(d), /* the command list */ + grn, nrm); /* quit colors */ - char_printf(d->character, buf); OLC_MODE(d) = TRIGEDIT_MAIN_MENU; } void trigedit_disp_types(DescriptorData *d) { - int i, columns = 0; - const char **types; + int columns = 0; + const std::string_view *types; switch (OLC_TRIG(d)->attach_type) { case WLD_TRIGGER: @@ -177,13 +178,11 @@ void trigedit_disp_types(DescriptorData *d) { #if defined(CLEAR_SCREEN) char_printf(d->character, ""); #endif - for (i = 0; i < NUM_TRIG_TYPE_FLAGS; i++) { - sprintf(buf, "%s%2d%s) %-20.20s %s", grn, i + 1, nrm, types[i], !(++columns % 2) ? "\n" : ""); - char_printf(d->character, buf); - } - sprintbit(GET_TRIG_TYPE(OLC_TRIG(d)), types, buf1); - sprintf(buf, "\nCurrent types : %s%s%s\nEnter type (0 to quit):\n", cyn, buf1, nrm); - char_printf(d->character, buf); + for (int i = 0; i < NUM_TRIG_TYPE_FLAGS; i++) + char_printf(d->character, + fmt::format("{}{:2d}{}) {:20.20s} {}", grn, i + 1, nrm, types[i], !(++columns % 2) ? "\n" : "")); + char_printf(d->character, fmt::format("\nCurrent types : {}{}{}\nEnter type (0 to quit):\n", cyn, + sprintbit(GET_TRIG_TYPE(OLC_TRIG(d)), types), nrm)); } void trigedit_parse(DescriptorData *d, char *arg) { diff --git a/src/dg_scripts.cpp b/src/dg_scripts.cpp index cbb47b0f..6b1ff918 100644 --- a/src/dg_scripts.cpp +++ b/src/dg_scripts.cpp @@ -2,6 +2,7 @@ #include "dg_scripts.hpp" #include "ai.hpp" +#include "bitflags.hpp" #include "casting.hpp" #include "charsize.hpp" #include "clan.hpp" @@ -30,21 +31,6 @@ #define PULSES_PER_MUD_HOUR (SECS_PER_MUD_HOUR * PASSES_PER_SEC) -/* mob trigger types */ -const char *trig_types[] = {"Global", "Random", "Command", "Speech", "Act", "Death", "Greet", - "Greet-All", "Entry", "Receive", "Fight", "HitPrcnt", "Bribe", "SpeechTo*", - "Load", "Cast", "Leave", "Door", "Look", "Time", "\n"}; - -/* obj trigger types */ -const char *otrig_types[] = {"Global", "Random", "Command", "Attack", "Defense", "Timer", "Get", - "Drop", "Give", "Wear", "Death", "Remove", "Look", "Use", - "Load", "Cast", "Leave", "UNUSED", "Consume", "Time", "\n"}; - -/* wld trigger types */ -const char *wtrig_types[] = {"Global", "Random", "Command", "Speech", "UNUSED", "Reset", "Preentry", - "Drop", "Postentry", "UNUSED", "UNUSED", "UNUSED", "UNUSED", "UNUSED", - "UNUSED", "Cast", "Leave", "Door", "UNUSED", "Time", "\n"}; - TrigData *trigger_list = nullptr; /* all attached triggers */ /* external functions */ @@ -277,18 +263,19 @@ void do_stat_trigger(CharData *ch, TrigData *trig) { char_printf(ch, "Trigger Name: '{}{}{}', VNum: [{}{:5d}{}], RNum: [{:5d}]\n", yel, GET_TRIG_NAME(trig), nrm, grn, GET_TRIG_VNUM(trig), nrm, GET_TRIG_RNUM(trig)); + std::string_view types; if (trig->attach_type == OBJ_TRIGGER) { char_printf(ch, "Trigger Intended Assignment: Objects\n"); - sprintbit(GET_TRIG_TYPE(trig), otrig_types, buf); + types = sprintbit(GET_TRIG_TYPE(trig), otrig_types); } else if (trig->attach_type == WLD_TRIGGER) { char_printf(ch, "Trigger Intended Assignment: Rooms\n"); - sprintbit(GET_TRIG_TYPE(trig), wtrig_types, buf); + types = sprintbit(GET_TRIG_TYPE(trig), wtrig_types); } else { char_printf(ch, "Trigger Intended Assignment: Mobiles\n"); - sprintbit(GET_TRIG_TYPE(trig), trig_types, buf); + types = sprintbit(GET_TRIG_TYPE(trig), trig_types); } - sprintf(sb, "Trigger Type: %s, Numeric Arg: %d, Arg list: %s\n", buf, GET_TRIG_NARG(trig), + sprintf(sb, "Trigger Type: %s, Numeric Arg: %d, Arg list: %s\n", types.data(), GET_TRIG_NARG(trig), ((GET_TRIG_ARG(trig) && *GET_TRIG_ARG(trig)) ? GET_TRIG_ARG(trig) : "None")); strcat(sb, "Commands:\n\n"); @@ -1053,7 +1040,7 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * } } else if (!strcasecmp(field, "opposite_dir")) { if ((num = search_block(value, dirs, false)) >= 0) - strcpy(str, dirs[rev_dir[num]]); + strcpy(str, dirs[rev_dir[num]].data()); else { /* * If they didn't give a valid direction, then reverse @@ -1086,13 +1073,13 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * else if (!strcasecmp(field, "tolower")) { do { - *(str++) = LOWER(*(value++)); + *(str++) = to_lower(*(value++)); } while (*value); } else if (!strcasecmp(field, "toupper")) { do { - *(str++) = UPPER(*(value++)); + *(str++) = to_upper(*(value++)); } while (*value); } @@ -1146,7 +1133,7 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * /* Attributes */ else if (!strcasecmp(field, "sex") || !strcasecmp(field, "gender")) - strcpy(str, genders[(int)GET_SEX(c)]); + strcpy(str, genders[(int)GET_SEX(c)].data()); else if (!strcasecmp(field, "class")) { strcpy(str, CLASS_PLAINNAME(c)); cap_by_color(str); @@ -1228,12 +1215,12 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * else if (!strcasecmp(field, "flags")) { *str = '\0'; if (IS_NPC(c)) /* ACT flags */ - sprintflag(str, MOB_FLAGS(c), NUM_MOB_FLAGS, action_bits); + strcpy(str, sprintflag(MOB_FLAGS(c), NUM_MOB_FLAGS, action_bits).c_str()); else { /* concatenation of PLR and PRF flags */ if (HAS_FLAGS(PLR_FLAGS(c), NUM_PLR_FLAGS) || !HAS_FLAGS(PRF_FLAGS(c), NUM_PRF_FLAGS)) - sprintflag(str, PLR_FLAGS(c), NUM_PLR_FLAGS, player_bits); + strcpy(str, sprintflag(PLR_FLAGS(c), NUM_PLR_FLAGS, player_bits).c_str()); if (HAS_FLAGS(PRF_FLAGS(c), NUM_PRF_FLAGS)) - sprintflag(str + strlen(str), PRF_FLAGS(c), NUM_PRF_FLAGS, preference_bits); + strcpy(str + strlen(str), sprintflag(PRF_FLAGS(c), NUM_PRF_FLAGS, preference_bits).c_str()); } } else if (!strcasecmp(field, "flagged")) { if (IS_NPC(c)) { @@ -1256,7 +1243,7 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * } } } else if (!strcasecmp(field, "aff_flags") || !strcasecmp(field, "eff_flags")) - sprintflag(str, EFF_FLAGS(c), NUM_EFF_FLAGS, effect_flags); + strcpy(str, sprintflag(EFF_FLAGS(c), NUM_EFF_FLAGS, effect_flags).c_str()); else if (!strcasecmp(field, "aff_flagged") || !strcasecmp(field, "eff_flagged")) { if ((num = search_block(value, effect_flags, false)) >= 0) @@ -1427,9 +1414,9 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * } else if (!strcasecmp(field, "position")) { - strcpy(str, position_types[(int)GET_POS(c)]); + strcpy(str, position_types[(int)GET_POS(c)].data()); } else if (!strcasecmp(field, "stance")) - strcpy(str, stance_types[(int)GET_STANCE(c)]); + strcpy(str, stance_types[(int)GET_STANCE(c)].data()); else if (!strcasecmp(field, "room")) { if (IN_ROOM(c) >= 0 && IN_ROOM(c) <= top_of_world) @@ -1447,14 +1434,159 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * } else if (!strcasecmp(field, "clan")) { - if (!IS_NPC(c) && GET_CLAN(c)) - strcpy(str, GET_CLAN(c)->name); + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) + strcpy(str, clan.value()->name().data()); + else + *str = '\0'; + } else if (!strcasecmp(field, "clan_rank")) { + auto rank = get_clan_rank(c); + if (!IS_NPC(c) && rank.has_value()) + strcpy(str, rank.value().title().data()); + else + *str = '\0'; + + } else if (!strcasecmp(field, "clan_id")) { + if (!IS_NPC(c)) { + sprintf(str, "%u", get_clan_id(c)); + } else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_abbr")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) + strcpy(str, clan.value()->abbreviation().data()); + else + *str = '\0'; + + } else if (!strcasecmp(field, "clan_description")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) + strcpy(str, clan.value()->description().data()); + else + *str = '\0'; + + } else if (!strcasecmp(field, "clan_motd")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) + strcpy(str, clan.value()->motd().data()); else *str = '\0'; - } else if (!strcasecmp(field, "clan_rank")) - sprintf(str, "%d", IS_NPC(c) ? 0 : GET_CLAN_RANK(c)); - else if (!strcasecmp(field, "can_be_seen")) + } else if (!strcasecmp(field, "clan_dues")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) + sprintf(str, "%u", clan.value()->dues()); + else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_app_fee")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) + sprintf(str, "%u", clan.value()->app_fee()); + else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_min_level")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) + sprintf(str, "%u", clan.value()->min_application_level()); + else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_member_count")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) + sprintf(str, "%zu", clan.value()->member_count()); + else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_treasure_total")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) { + auto treasure = clan.value()->treasure(); + sprintf(str, "%d", treasure.value()); + } else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_treasure_platinum")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) { + auto treasure = clan.value()->treasure(); + sprintf(str, "%d", treasure.platinum()); + } else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_treasure_gold")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) { + auto treasure = clan.value()->treasure(); + sprintf(str, "%d", treasure.gold()); + } else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_treasure_silver")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) { + auto treasure = clan.value()->treasure(); + sprintf(str, "%d", treasure.silver()); + } else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_treasure_copper")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) { + auto treasure = clan.value()->treasure(); + sprintf(str, "%d", treasure.copper()); + } else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_bank_room")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) { + sprintf(str, "%d", clan.value()->bank_room()); + } else + strcpy(str, "-1"); + + } else if (!strcasecmp(field, "clan_chest_room")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) { + sprintf(str, "%d", clan.value()->chest_room()); + } else + strcpy(str, "-1"); + + } else if (!strcasecmp(field, "clan_hall_room")) { + auto clan = get_clan_membership(c); + if (!IS_NPC(c) && clan.has_value()) { + sprintf(str, "%d", clan.value()->hall_room()); + } else + strcpy(str, "-1"); + + } else if (!strcasecmp(field, "clan_can_deposit")) { + if (!IS_NPC(c) && has_clan_permission(c, ClanPermission::DEPOSIT_FUNDS)) + strcpy(str, "1"); + else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_can_withdraw")) { + if (!IS_NPC(c) && has_clan_permission(c, ClanPermission::WITHDRAW_FUNDS)) + strcpy(str, "1"); + else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_can_store")) { + if (!IS_NPC(c) && has_clan_permission(c, ClanPermission::STORE_ITEMS)) + strcpy(str, "1"); + else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "clan_can_retrieve")) { + if (!IS_NPC(c) && has_clan_permission(c, ClanPermission::RETRIEVE_ITEMS)) + strcpy(str, "1"); + else + strcpy(str, "0"); + + } else if (!strcasecmp(field, "can_be_seen")) strcpy(str, type == MOB_TRIGGER && !CAN_SEE(ch, c) ? "0" : "1"); else if (!strcasecmp(field, "trophy")) { @@ -1533,13 +1665,13 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * if (!is_positive_integer(value) || (num = atoi(value)) > 5) *str = '\0'; else - sprintf(str, "%+d %s", o->applies[num].modifier, apply_types[(int)o->applies[num].location]); + sprintf(str, "%+d %s", o->applies[num].modifier, apply_types[(int)o->applies[num].location].data()); } else if (!strcasecmp(field, "affect_value") || !strcasecmp(field, "effect_value")) sprintf(str, "%d", is_positive_integer(value) && (num = atoi(value) <= 5) ? o->applies[num].modifier : 0); /* Flags */ else if (!strcasecmp(field, "flags")) - sprintflag(str, GET_OBJ_FLAGS(o), NUM_ITEM_FLAGS, extra_bits); + strcpy(str, sprintflag(GET_OBJ_FLAGS(o), NUM_ITEM_FLAGS, extra_bits).c_str()); else if (!strcasecmp(field, "flagged")) { if ((num = search_block(value, extra_bits, false)) >= 0) strcpy(str, OBJ_FLAGGED(o, num) ? "1" : "0"); @@ -1549,7 +1681,7 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * script_log(trig, buf2); } } else if (!strcasecmp(field, "spells")) - sprintflag(str, GET_OBJ_EFF_FLAGS(o), NUM_EFF_FLAGS, effect_flags); + strcpy(str, sprintflag(GET_OBJ_EFF_FLAGS(o), NUM_EFF_FLAGS, effect_flags).c_str()); else if (!strcasecmp(field, "has_spell")) { if ((num = search_block(value, effect_flags, false)) >= 0) @@ -1574,7 +1706,7 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * UID_VAR(str, o->worn_by); else if (!strcasecmp(field, "worn_on")) { if (o->worn_by) - sprinttype(o->worn_on, wear_positions, str); + strcpy(str, sprinttype(o->worn_on, wear_positions).c_str()); else *str = '\0'; } else if (!strcasecmp(field, "contents")) { @@ -1609,7 +1741,7 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * */ else if (r) { if (!strcasecmp(field, "name")) - strcpy(str, r->name); + strcpy(str, r->name.c_str()); else if (!strcasecmp(field, "vnum")) sprintf(str, "%d", r->vnum); @@ -1619,20 +1751,20 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * strcpy(str, r->light > 0 ? "1" : "0"); else if (!strcasecmp(field, "flags")) - sprintflag(str, r->room_flags, NUM_ROOM_FLAGS, room_bits); + strcpy(str, sprintflag(r->flags, room_bits).data()); else if (!strcasecmp(field, "flagged")) { if ((num = search_block(value, room_bits, false)) >= 0) - strcpy(str, IS_FLAGGED(r->room_flags, num) ? "1" : "0"); + strcpy(str, IS_FLAGGED(r->flags, num) ? "1" : "0"); else { strcpy(str, "0"); sprintf(buf2, "unrecognized room flag '%s' to %%%s.flagged%%", value, var); script_log(trig, buf2); } } else if (!strcasecmp(field, "effects") || !strcasecmp(field, "affects")) - sprintflag(str, r->room_effects, NUM_ROOM_EFF_FLAGS, room_effects); + strcpy(str, sprintflag(r->effects, room_effects).data()); else if (!strcasecmp(field, "has_effect") || !strcasecmp(field, "has_affect")) { if ((num = search_block(value, room_effects, false)) >= 0) - strcpy(str, IS_FLAGGED(r->room_effects, num) ? "1" : "0"); + strcpy(str, IS_FLAGGED(r->effects, num) ? "1" : "0"); else { strcpy(str, "0"); sprintf(buf2, "unrecognized room effect flag '%s' to %%%s.has_effect%%", value, var); @@ -1696,7 +1828,7 @@ void find_replacement(void *go, ScriptData *sc, TrigData *trig, int type, char * } else if (!strcasecmp(value, "key")) /* %room.DIR[key]% */ sprintf(str, "%d", r->exits[num]->key); else if (!strcasecmp(value, "bits")) /* %room.DIR[bits]% */ - sprintbit(r->exits[num]->exit_info, exit_bits, str); + strcpy(str, sprintbit(r->exits[num]->exit_info, exit_bits).c_str()); else *str = '\0'; } diff --git a/src/dg_scripts.hpp b/src/dg_scripts.hpp index 49c91954..a934d0c0 100644 --- a/src/dg_scripts.hpp +++ b/src/dg_scripts.hpp @@ -137,9 +137,6 @@ struct TrigData { TrigData *next; TrigData *next_in_world; /* next in the global trigger list */ }; -extern const char *trig_types[]; -extern const char *otrig_types[]; -extern const char *wtrig_types[]; extern TrigData *trigger_list; /* a complete script (composed of several triggers) */ @@ -173,7 +170,7 @@ int drop_wtrigger(ObjData *obj, CharData *actor); int give_otrigger(ObjData *obj, CharData *actor, CharData *victim); int remove_otrigger(ObjData *obj, CharData *actor); int receive_mtrigger(CharData *ch, CharData *actor, ObjData *obj); -void bribe_mtrigger(CharData *ch, CharData *actor, int *cPtr); +void bribe_mtrigger(CharData *ch, CharData *actor, Money cPtr); int wear_otrigger(ObjData *obj, CharData *actor, int where); int command_mtrigger(CharData *actor, char *cmd, char *argument); int command_otrigger(CharData *actor, char *cmd, char *argument); @@ -278,3 +275,18 @@ int remove_var(TriggerVariableData **var_list, const char *name); // typedef char_data CharData; int script_driver(void *go_address, TrigData *trig, int type, int mode); + +/* mob trigger types */ +constexpr std::string_view trig_types[] = {"Global", "Random", "Command", "Speech", "Act", "Death", "Greet", + "Greet-All", "Entry", "Receive", "Fight", "HitPrcnt", "Bribe", "SpeechTo*", + "Load", "Cast", "Leave", "Door", "Look", "Time", "\n"}; + +/* obj trigger types */ +constexpr std::string_view otrig_types[] = {"Global", "Random", "Command", "Attack", "Defense", "Timer", "Get", + "Drop", "Give", "Wear", "Death", "Remove", "Look", "Use", + "Load", "Cast", "Leave", "UNUSED", "Consume", "Time", "\n"}; + +/* wld trigger types */ +constexpr std::string_view wtrig_types[] = {"Global", "Random", "Command", "Speech", "UNUSED", "Reset", "Preentry", + "Drop", "Postentry", "UNUSED", "UNUSED", "UNUSED", "UNUSED", "UNUSED", + "UNUSED", "Cast", "Leave", "Door", "UNUSED", "Time", "\n"}; diff --git a/src/dg_triggers.cpp b/src/dg_triggers.cpp index 7b77df89..ce7f8158 100644 --- a/src/dg_triggers.cpp +++ b/src/dg_triggers.cpp @@ -114,7 +114,7 @@ void random_mtrigger(CharData *ch) { * array for platinum, gold, silver, and copper. Only one bribe trigger * will be executed. */ -void bribe_mtrigger(CharData *ch, CharData *actor, int coins[]) { +void bribe_mtrigger(CharData *ch, CharData *actor, Money coins) { TrigData *t; char buf[MAX_INPUT_LENGTH]; int raw_value; @@ -170,7 +170,7 @@ int greet_mtrigger(CharData *actor, int dir) { if (((IS_SET(GET_TRIG_TYPE(t), MTRIG_GREET) && CAN_SEE(ch, actor)) || IS_SET(GET_TRIG_TYPE(t), MTRIG_GREET_ALL)) && !GET_TRIG_DEPTH(t) && (random_number(1, 100) <= GET_TRIG_NARG(t))) { - add_var(&GET_TRIG_VARS(t), "direction", dirs[rev_dir[dir]]); + add_var(&GET_TRIG_VARS(t), "direction", dirs[rev_dir[dir]].data()); ADD_UID_VAR(buf, t, actor, "actor"); if (!script_driver(&ch, t, MOB_TRIGGER, TRIG_NEW)) ret_val = false; @@ -412,7 +412,6 @@ int receive_mtrigger(CharData *ch, CharData *actor, ObjData *obj) { sprintf(vnum, "%d", GET_OBJ_VNUM(obj)); - if (!MOB_PERFORMS_SCRIPTS(ch) || !SCRIPT_CHECK(ch, MTRIG_RECEIVE) || !char_susceptible_to_triggers(actor) || !char_susceptible_to_triggers(ch)) return ret_val; @@ -420,15 +419,15 @@ int receive_mtrigger(CharData *ch, CharData *actor, ObjData *obj) { for (t = TRIGGERS(SCRIPT(ch)); t; t = t->next) { if (IS_SET(GET_TRIG_TYPE(t), MTRIG_RECEIVE)) { - if ((GET_TRIG_ARG(t) && word_check(vnum, GET_TRIG_ARG(t)) && GET_TRIG_NARG(t)) || - ((GET_TRIG_ARG(t) && !word_check(vnum, GET_TRIG_ARG(t)) && !GET_TRIG_NARG(t))) || - (!GET_TRIG_ARG(t) || !*GET_TRIG_ARG(t))) { + if ((GET_TRIG_ARG(t) && word_check(vnum, GET_TRIG_ARG(t)) && GET_TRIG_NARG(t)) || + ((GET_TRIG_ARG(t) && !word_check(vnum, GET_TRIG_ARG(t)) && !GET_TRIG_NARG(t))) || + (!GET_TRIG_ARG(t) || !*GET_TRIG_ARG(t))) { - ADD_UID_VAR(buf, t, actor, "actor"); - ADD_UID_VAR(buf, t, obj, "object"); - ret_val = script_driver(&ch, t, MOB_TRIGGER, TRIG_NEW); - if (!ret_val) - return ret_val; + ADD_UID_VAR(buf, t, actor, "actor"); + ADD_UID_VAR(buf, t, obj, "object"); + ret_val = script_driver(&ch, t, MOB_TRIGGER, TRIG_NEW); + if (!ret_val) + return ret_val; } } } @@ -508,7 +507,7 @@ int leave_mtrigger(CharData *actor, int dir) { for (t = TRIGGERS(SCRIPT(ch)); t; t = t->next) { if (TRIGGER_CHECK(t, MTRIG_LEAVE) && CAN_SEE(ch, actor) && random_number(1, 100) <= GET_TRIG_NARG(t)) { if (dir >= 0 && dir < NUM_OF_DIRS) - add_var(&GET_TRIG_VARS(t), "direction", dirs[dir]); + add_var(&GET_TRIG_VARS(t), "direction", dirs[dir].data()); else add_var(&GET_TRIG_VARS(t), "direction", "none"); ADD_UID_VAR(buf, t, actor, "actor"); @@ -535,7 +534,7 @@ int door_mtrigger(CharData *actor, int subcmd, int dir) { for (t = TRIGGERS(SCRIPT(ch)); t; t = t->next) { if (TRIGGER_CHECK(t, MTRIG_DOOR) && random_number(1, 100) <= GET_TRIG_NARG(t)) { add_var(&GET_TRIG_VARS(t), "cmd", cmd_door[subcmd]); - add_var(&GET_TRIG_VARS(t), "direction", dirs[dir]); + add_var(&GET_TRIG_VARS(t), "direction", dirs[dir].data()); ADD_UID_VAR(buf, t, actor, "actor"); return script_driver(&ch, t, MOB_TRIGGER, TRIG_NEW); } @@ -568,7 +567,8 @@ int look_mtrigger(CharData *ch, CharData *actor, const char *str) { char buf[MAX_INPUT_LENGTH]; int ret_val = 1; - if (!MOB_PERFORMS_SCRIPTS(ch) || !char_susceptible_to_triggers(actor) || !SCRIPT_CHECK(ch, MTRIG_LOOK) || !char_susceptible_to_triggers(ch)) + if (!MOB_PERFORMS_SCRIPTS(ch) || !char_susceptible_to_triggers(actor) || !SCRIPT_CHECK(ch, MTRIG_LOOK) || + !char_susceptible_to_triggers(ch)) return ret_val; for (t = TRIGGERS(SCRIPT(ch)); t; t = t->next) { @@ -586,7 +586,6 @@ int look_mtrigger(CharData *ch, CharData *actor, const char *str) { return ret_val; } - /* * object triggers */ @@ -882,7 +881,7 @@ int leave_otrigger(RoomData *room, CharData *actor, int dir) { for (t = TRIGGERS(SCRIPT(obj)); t; t = t->next) { if (TRIGGER_CHECK(t, OTRIG_LEAVE) && (random_number(1, 100) <= GET_TRIG_NARG(t))) { if (dir >= 0 && dir < NUM_OF_DIRS) - add_var(&GET_TRIG_VARS(t), "direction", dirs[dir]); + add_var(&GET_TRIG_VARS(t), "direction", dirs[dir].data()); else add_var(&GET_TRIG_VARS(t), "direction", "none"); ADD_UID_VAR(buf, t, actor, "actor"); @@ -959,7 +958,6 @@ int look_otrigger(ObjData *obj, CharData *actor, char *name, const char *additio if (!char_susceptible_to_triggers(actor) || !SCRIPT_CHECK(obj, OTRIG_LOOK)) return ret_val; - if ((pos = strchr(name, '.'))) { num = atoi(name); name = ++pos; @@ -968,8 +966,8 @@ int look_otrigger(ObjData *obj, CharData *actor, char *name, const char *additio str = name; for (t = TRIGGERS(SCRIPT(obj)); t; t = t->next) { - if (GET_TRIG_ARG(t) && word_check(str, GET_TRIG_ARG(t)) || - (!GET_TRIG_ARG(t) || !*GET_TRIG_ARG(t)) && isname(str, obj->name)) { + if (GET_TRIG_ARG(t) && word_check(str, GET_TRIG_ARG(t)) || + (!GET_TRIG_ARG(t) || !*GET_TRIG_ARG(t)) && isname(str, obj->name)) { if (TRIGGER_CHECK(t, OTRIG_LOOK) && (random_number(1, 100) <= GET_TRIG_NARG(t))) { if (actor) ADD_UID_VAR(buf, t, actor, "actor"); @@ -978,9 +976,8 @@ int look_otrigger(ObjData *obj, CharData *actor, char *name, const char *additio /* Don't allow a look to take place, if the object is purged. */ if (!script_driver(&obj, t, OBJ_TRIGGER, TRIG_NEW) && obj) - ret_val = 0; - - } + ret_val = 0; + } } else continue; } @@ -993,7 +990,6 @@ int use_otrigger(ObjData *obj, ObjData *tobj, CharData *actor, CharData *victim) char buf[MAX_INPUT_LENGTH]; int ret_val = 1; - if (!char_susceptible_to_triggers(actor) || !SCRIPT_CHECK(obj, OTRIG_USE)) return ret_val; @@ -1046,7 +1042,7 @@ int preentry_wtrigger(RoomData *room, CharData *actor, int dir) { for (t = TRIGGERS(SCRIPT(room)); t; t = t->next) { if (TRIGGER_CHECK(t, WTRIG_PREENTRY) && (random_number(1, 100) <= GET_TRIG_NARG(t))) { - add_var(&GET_TRIG_VARS(t), "direction", dirs[rev_dir[dir]]); + add_var(&GET_TRIG_VARS(t), "direction", dirs[rev_dir[dir]].data()); ADD_UID_VAR(buf, t, actor, "actor"); return script_driver(&room, t, WLD_TRIGGER, TRIG_NEW); } @@ -1066,7 +1062,7 @@ int postentry_wtrigger(CharData *actor, int dir) { for (t = TRIGGERS(SCRIPT(room)); t; t = t->next) { if (TRIGGER_CHECK(t, WTRIG_POSTENTRY) && (random_number(1, 100) <= GET_TRIG_NARG(t))) { - add_var(&GET_TRIG_VARS(t), "direction", dirs[rev_dir[dir]]); + add_var(&GET_TRIG_VARS(t), "direction", dirs[rev_dir[dir]].data()); ADD_UID_VAR(buf, t, actor, "actor"); return script_driver(&room, t, WLD_TRIGGER, TRIG_NEW); } @@ -1209,7 +1205,7 @@ int leave_wtrigger(RoomData *room, CharData *actor, int dir) { for (t = TRIGGERS(SCRIPT(room)); t; t = t->next) { if (TRIGGER_CHECK(t, WTRIG_LEAVE) && (random_number(1, 100) <= GET_TRIG_NARG(t))) { - add_var(&GET_TRIG_VARS(t), "direction", dirs[dir]); + add_var(&GET_TRIG_VARS(t), "direction", dirs[dir].data()); ADD_UID_VAR(buf, t, actor, "actor"); return script_driver(&room, t, WLD_TRIGGER, TRIG_NEW); } @@ -1230,7 +1226,7 @@ int door_wtrigger(CharData *actor, int subcmd, int dir) { for (t = TRIGGERS(SCRIPT(room)); t; t = t->next) { if (TRIGGER_CHECK(t, WTRIG_DOOR) && (random_number(1, 100) <= GET_TRIG_NARG(t))) { add_var(&GET_TRIG_VARS(t), "cmd", cmd_door[subcmd]); - add_var(&GET_TRIG_VARS(t), "direction", (char *)dirs[dir]); + add_var(&GET_TRIG_VARS(t), "direction", dirs[dir].data()); ADD_UID_VAR(buf, t, actor, "actor"); return script_driver(&room, t, WLD_TRIGGER, TRIG_NEW); } diff --git a/src/dg_wldcmd.cpp b/src/dg_wldcmd.cpp index 30bd6f4e..bccf5803 100644 --- a/src/dg_wldcmd.cpp +++ b/src/dg_wldcmd.cpp @@ -190,7 +190,7 @@ WCMD(do_wdoor) { return; } - if ((fd = searchblock(field, door_field, false)) == -1) { + if ((fd = search_block(field, door_field, false)) == -1) { wld_log(room, t, "wdoor: invalid field"); return; } diff --git a/src/directions.cpp b/src/directions.cpp index 6d1d2428..aa5f5f6c 100644 --- a/src/directions.cpp +++ b/src/directions.cpp @@ -14,12 +14,3 @@ #include "conf.hpp" #include "sysdep.hpp" - -const char *dirs[NUM_OF_DIRS + 1] = {"north", "east", "south", "west", "up", "down", "\n"}; - -const char *capdirs[NUM_OF_DIRS + 1] = {"N", "E", "S", "W", "U", "D", "\n"}; - -const char *dirpreposition[NUM_OF_DIRS + 1] = { - "to the north", "to the east", "to the south", "to the west", "in the ceiling", "in the floor", "\n"}; - -const int rev_dir[NUM_OF_DIRS] = {SOUTH, WEST, NORTH, EAST, DOWN, UP}; diff --git a/src/directions.hpp b/src/directions.hpp index 721a46da..6b57689e 100644 --- a/src/directions.hpp +++ b/src/directions.hpp @@ -12,10 +12,23 @@ #pragma once +#include "interpreter.hpp" #include "structs.hpp" #include "sysdep.hpp" -extern const char *dirs[NUM_OF_DIRS + 1]; -extern const char *capdirs[NUM_OF_DIRS + 1]; -extern const char *dirpreposition[NUM_OF_DIRS + 1]; -extern const int rev_dir[NUM_OF_DIRS]; +#include <array> + +constexpr std::string_view dirs[NUM_OF_DIRS + 1] = {"north", "east", "south", "west", "up", "down", "\n"}; +constexpr std::string_view capdirs[NUM_OF_DIRS + 1] = {"N", "E", "S", "W", "U", "D", "\n"}; +constexpr std::string_view dirpreposition[NUM_OF_DIRS + 1] = { + "to the north", "to the east", "to the south", "to the west", "in the ceiling", "in the floor", "\n"}; +constexpr int rev_dir[NUM_OF_DIRS] = {SOUTH, WEST, NORTH, EAST, DOWN, UP}; + +inline int parse_direction(std::string_view dir) { + for (int i = 0; i < NUM_OF_DIRS; i++) { + if (matches_start(dir, dirs[i])) { + return i; + } + } + return -1; +} diff --git a/src/diskio.cpp b/src/diskio.cpp index 7adb010b..af28d7f8 100644 --- a/src/diskio.cpp +++ b/src/diskio.cpp @@ -159,8 +159,15 @@ FBFILE *fbopen_for_read(char *fname) { } fbfl->ptr = fbfl->buf; fbfl->flags = FB_READ; - strcpy(fbfl->name, fname); - fread(fbfl->buf, sizeof(char), fbfl->size, fl); + strncpy(fbfl->name, fname, strlen(fname)); + fbfl->name[strlen(fname)] = '\0'; + if (fread(fbfl->buf, sizeof(char), fbfl->size, fl) != fbfl->size) { + free(fbfl->buf); + free(fbfl->name); + free(fbfl); + fclose(fl); + return nullptr; + } fclose(fl); return fbfl; @@ -181,7 +188,8 @@ FBFILE *fbopen_for_write(char *fname, int mode) { free(fbfl); return nullptr; } - strcpy(fbfl->name, fname); + strncpy(fbfl->name, fname, strlen(fname)); + fbfl->name[strlen(fname)] = '\0'; fbfl->ptr = fbfl->buf; fbfl->size = FB_STARTSIZE; fbfl->flags = mode; @@ -233,7 +241,7 @@ int fbclose_for_write(FBFILE *fbfl) { len = strlen(fbfl->buf); if (!len) return 0; - sprintf(tname, "%s.tmp", fbfl->name); + snprintf(tname, strlen(fbfl->name) + 5, "%s.tmp", fbfl->name); if (!(fl = fopen(tname, arg))) { free(tname); @@ -280,7 +288,9 @@ int fbwrite(FBFILE *fbfl, const char *string) { fbfl->size += FB_STARTSIZE; } - strcpy(fbfl->ptr, string); + size_t remaining_space = fbfl->size - (fbfl->ptr - fbfl->buf) - 1; + strncpy(fbfl->ptr, string, remaining_space); + fbfl->ptr[remaining_space] = '\0'; bytes_written = strlen(string); fbfl->ptr += bytes_written; @@ -302,7 +312,8 @@ int fbprintf(FBFILE *fbfl, const char *format, ...) { } va_start(args, format); - bytes_written = vsprintf(fbfl->ptr, format, args); + size_t remaining_space = fbfl->size - (fbfl->ptr - fbfl->buf) - 1; + bytes_written = vsnprintf(fbfl->ptr, remaining_space, format, args); va_end(args); fbfl->ptr += bytes_written; diff --git a/src/effects.cpp b/src/effects.cpp index e3a8b9c6..ee6fd37f 100644 --- a/src/effects.cpp +++ b/src/effects.cpp @@ -12,94 +12,94 @@ #include "effects.hpp" -const char *effect_flags[NUM_EFF_FLAGS + 1] = {"BLIND", /* 0 */ - "INVIS", - "DET_ALIGN", - "DET_INVIS", - "DET_MAGIC", - "SENSE_LIFE", /* 5 */ - "WATWALK", - "SANCT", - "CONFUSION", - "CURSE", - "INFRA", /* 10 */ - "POISON", - "PROT_EVIL", - "PROT_GOOD", - "SLEEP", - "!TRACK", /* 15 */ - "TAMED", - "BERSERK", - "SNEAK", - "STEALTH", - "FLY", /* 20 */ - "CHARM", - "STONE_SKIN", - "FARSEE", - "HASTE", - "BLUR", /* 25 */ - "VITALITY", - "GLORY", - "MAJOR_PARALYSIS", - "FAMILIARITY", - "MESMERIZED", /* 30 */ - "IMMOBILIZED", - "LIGHT", - "MAJOR_GROUP", - "MINOR_PARALYSIS", - "HURT_THROAT", /* 35 */ - "FEATHER_FALL", - "WATERBREATH", - "SOULSHIELD", - "SILENCE", - "PROT_FIRE", /* 40 */ - "PROT_COLD", - "PROT_AIR", - "PROT_EARTH", - "FIRESHIELD", - "COLDSHIELD", /* 45 */ - "MINOR_GLOBE", - "MAJOR_GLOBE", - "HARNESS", - "ON_FIRE", - "FEAR", /* 50 */ - "TONGUES", - "DISEASE", - "INSANITY", - "ULTRAVISION", - "!HEAT", /* 55 */ - "!COLD", - "!AIR", - "!EARTH", - "REMOTE_AGGR", - "FIREHANDS", /* 60 */ - "ICEHANDS", - "LIGHTNING_HANDS", - "ACIDHANDS", - "AWARE", - "REDUCE", /* 65 */ - "ENLARGE", - "VAMP", - "ENFEEB", - "ANIMATE", - "EXPOSED", /* 70 */ - "SHADOW", - "CAMOUFLAGE", - "SPIRIT_WOLF", - "SPIRIT_BEAR", - "WRATH", /* 75 */ - "MISDIRECTION", - "MISDIRECTING", - "BLESS", - "HEX", - "DETECT_POISON", /* 80 */ - "SONG_OF_REST", - "DISPLACEMENT", - "GREATER_DISPLACEMENT", - "FIRE_WEAPON", - "ICE_WEAPON", /* 85 */ - "POISON_WEAPON", - "ACID_WEAPON", - "SHOCK_WEAPON", - "RADIANT_WEAPON", - "\n"}; +const std::string_view effect_flags[NUM_EFF_FLAGS + 1] = {"BLIND", /* 0 */ + "INVIS", + "DET_ALIGN", + "DET_INVIS", + "DET_MAGIC", + "SENSE_LIFE", /* 5 */ + "WATWALK", + "SANCT", + "CONFUSION", + "CURSE", + "INFRA", /* 10 */ + "POISON", + "PROT_EVIL", + "PROT_GOOD", + "SLEEP", + "!TRACK", /* 15 */ + "TAMED", + "BERSERK", + "SNEAK", + "STEALTH", + "FLY", /* 20 */ + "CHARM", + "STONE_SKIN", + "FARSEE", + "HASTE", + "BLUR", /* 25 */ + "VITALITY", + "GLORY", + "MAJOR_PARALYSIS", + "FAMILIARITY", + "MESMERIZED", /* 30 */ + "IMMOBILIZED", + "LIGHT", + "MAJOR_GROUP", + "MINOR_PARALYSIS", + "HURT_THROAT", /* 35 */ + "FEATHER_FALL", + "WATERBREATH", + "SOULSHIELD", + "SILENCE", + "PROT_FIRE", /* 40 */ + "PROT_COLD", + "PROT_AIR", + "PROT_EARTH", + "FIRESHIELD", + "COLDSHIELD", /* 45 */ + "MINOR_GLOBE", + "MAJOR_GLOBE", + "HARNESS", + "ON_FIRE", + "FEAR", /* 50 */ + "TONGUES", + "DISEASE", + "INSANITY", + "ULTRAVISION", + "!HEAT", /* 55 */ + "!COLD", + "!AIR", + "!EARTH", + "REMOTE_AGGR", + "FIREHANDS", /* 60 */ + "ICEHANDS", + "LIGHTNING_HANDS", + "ACIDHANDS", + "AWARE", + "REDUCE", /* 65 */ + "ENLARGE", + "VAMP", + "ENFEEB", + "ANIMATE", + "EXPOSED", /* 70 */ + "SHADOW", + "CAMOUFLAGE", + "SPIRIT_WOLF", + "SPIRIT_BEAR", + "WRATH", /* 75 */ + "MISDIRECTION", + "MISDIRECTING", + "BLESS", + "HEX", + "DETECT_POISON", /* 80 */ + "SONG_OF_REST", + "DISPLACEMENT", + "GREATER_DISPLACEMENT", + "FIRE_WEAPON", + "ICE_WEAPON", /* 85 */ + "POISON_WEAPON", + "ACID_WEAPON", + "SHOCK_WEAPON", + "RADIANT_WEAPON", + "\n"}; diff --git a/src/effects.hpp b/src/effects.hpp index b4ec797a..bd133052 100644 --- a/src/effects.hpp +++ b/src/effects.hpp @@ -13,4 +13,7 @@ #pragma once #include "defines.hpp" -extern const char *effect_flags[NUM_EFF_FLAGS + 1]; \ No newline at end of file + +#include <string_view> + +extern const std::string_view effect_flags[NUM_EFF_FLAGS + 1]; \ No newline at end of file diff --git a/src/fight.cpp b/src/fight.cpp index 0b5e43b1..59e26358 100644 --- a/src/fight.cpp +++ b/src/fight.cpp @@ -454,12 +454,22 @@ void load_messages(void) { fight_messages[i].msg = 0; } - fgets(chk, 256, fl); - while (!feof(fl) && (*chk == '\n' || *chk == '*')) - fgets(chk, 256, fl); + if (!fgets(chk, 256, fl)) { + log("Error reading file: unexpected EOF or read error."); + return; + } + while (!feof(fl) && (*chk == '\n' || *chk == '*')) { + if (!fgets(chk, 256, fl)) { + log("Error reading file: unexpected EOF or read error."); + return; + } + } while (*chk == 'M') { - fgets(chk, 256, fl); + if (!fgets(chk, 256, fl)) { + log("Error reading file: unexpected EOF or read error."); + return; + } sscanf(chk, " %d\n", &type); for (i = 0; (i < MAX_MESSAGES) && (fight_messages[i].a_type != type) && (fight_messages[i].a_type); i++) ; @@ -489,9 +499,16 @@ void load_messages(void) { messages->heal_msg.attacker_msg = fread_message(fl, i); messages->heal_msg.victim_msg = fread_message(fl, i); messages->heal_msg.room_msg = fread_message(fl, i); - fgets(chk, 256, fl); - while (!feof(fl) && (*chk == '\n' || *chk == '*')) - fgets(chk, 256, fl); + if (!fgets(chk, 256, fl)) { + log("Error reading file: unexpected EOF or read error."); + return; + } + while (!feof(fl) && (*chk == '\n' || *chk == '*')) { + if (!fgets(chk, 256, fl)) { + log("Error reading file: unexpected EOF or read error."); + return; + } + } } fclose(fl); @@ -935,10 +952,13 @@ void die(CharData *ch, CharData *killer) { sprintf(buf2, "%s was killed by %s", GET_NAME(ch), GET_NAME(killer)); else sprintf(buf2, "%s died", GET_NAME(ch)); - clan_notification(GET_CLAN(ch), ch, "%s.", buf2); + auto clan = get_clan_membership(ch); + if (clan) { + auto char_shared = std::shared_ptr<CharData>(ch, [](CharData*){}); + clan.value()->notify(char_shared, buf2); + } log(LogSeverity::Stat, LVL_IMMORT, "{} in {} [{:d}]", buf2, world[ch->in_room].name, world[ch->in_room].vnum); } - /* Stop the fighting */ if (FIGHTING(ch)) stop_fighting(ch); @@ -2147,12 +2167,12 @@ void hit(CharData *ch, CharData *victim, int type) { if (type == SKILL_KICK) { act(EVASIONCLR "Your foot passes harmlessly through $N" EVASIONCLR "!&0", false, ch, 0, victim, TO_CHAR); act(EVASIONCLR "$n&7&b sends $s foot whistling right through $N" EVASIONCLR ".&0", false, ch, 0, victim, - TO_NOTVICT); + TO_NOTVICT); act(EVASIONCLR "$n" EVASIONCLR " tries to kick you, but $s foot passes through you harmlessly.&0", false, - ch, 0, victim, TO_VICT); + ch, 0, victim, TO_VICT); } else damage_evasion_message(ch, victim, weapon, dtype); - + set_fighting(victim, ch, true); /* Process Triggers - added here so they still process even if the attack is evaded */ @@ -2179,9 +2199,9 @@ void hit(CharData *ch, CharData *victim, int type) { * Some skills don't get a chance for riposte, parry, and dodge, * so short-circuit those function calls here. */ - else if (type == SKILL_BACKSTAB || type == SKILL_2BACK || type == SKILL_BAREHAND || type == SKILL_KICK || no_defense_check || - EFF_FLAGGED(ch, EFF_FIREHANDS) || EFF_FLAGGED(ch, EFF_ICEHANDS) || EFF_FLAGGED(ch, EFF_LIGHTNINGHANDS) || - EFF_FLAGGED(ch, EFF_ACIDHANDS) || + else if (type == SKILL_BACKSTAB || type == SKILL_2BACK || type == SKILL_BAREHAND || type == SKILL_KICK || + no_defense_check || EFF_FLAGGED(ch, EFF_FIREHANDS) || EFF_FLAGGED(ch, EFF_ICEHANDS) || + EFF_FLAGGED(ch, EFF_LIGHTNINGHANDS) || EFF_FLAGGED(ch, EFF_ACIDHANDS) || (!riposte(ch, victim) && !parry(ch, victim) && !dodge(ch, victim) && (!weapon || !weapon_special(weapon, ch)))) { /* @@ -2230,7 +2250,7 @@ void hit(CharData *ch, CharData *victim, int type) { } else if (type == SKILL_KICK) { dam += (GET_SKILL(ch, SKILL_KICK) / 2); dam += stat_bonus[GET_DEX(ch)].todam; - + } else if (type == SKILL_BAREHAND || EFF_FLAGGED(ch, EFF_FIREHANDS) || EFF_FLAGGED(ch, EFF_ICEHANDS) || EFF_FLAGGED(ch, EFF_LIGHTNINGHANDS) || EFF_FLAGGED(ch, EFF_ACIDHANDS)) dam += GET_SKILL(ch, SKILL_BAREHAND) / 4 + random_number(1, GET_LEVEL(ch) / 3) + (GET_LEVEL(ch) / 2); diff --git a/src/function_registration.hpp b/src/function_registration.hpp new file mode 100644 index 00000000..47ca8bf0 --- /dev/null +++ b/src/function_registration.hpp @@ -0,0 +1,746 @@ +#include "arguments.hpp" +#include "logging.hpp" +#include "structs.hpp" +#include "utils.hpp" + +#include <algorithm> +#include <format> +#include <functional> +#include <iostream> +#include <memory> +#include <string_view> +#include <unordered_map> +#include <vector> + +// All of our functions should include these headers +// Enhanced command categorization for intelligent disambiguation +enum class CommandCategory : uint8_t { + MOVEMENT = 0, // north, south, east, west, up, down + COMBAT = 1, // kill, flee, bash, kick, backstab + COMMUNICATION = 2, // say, tell, gossip, clan + OBJECT_MANIPULATION = 3, // get, drop, put, give, wear, remove + INFORMATION = 4, // look, examine, score, inventory, who + SOCIAL = 5, // smile, nod, nap, sleep, wake + ADMINISTRATIVE = 6, // shutdown, advance, set, restore + SYSTEM = 7, // save, quit, time, weather + CLAN = 8, // clan-specific commands + MAGIC = 9, // cast, memorize, pray + SKILLS = 10, // practice, train, use + UNKNOWN = 255 // Default/unspecified category +}; + +// Context flags for situational command priority +using ContextFlags = uint32_t; +constexpr ContextFlags CONTEXT_COMBAT = 1 << 0; // Higher priority in combat +constexpr ContextFlags CONTEXT_PEACEFUL = 1 << 1; // Higher priority when peaceful +constexpr ContextFlags CONTEXT_INDOOR = 1 << 2; // Indoor environments +constexpr ContextFlags CONTEXT_OUTDOOR = 1 << 3; // Outdoor environments +constexpr ContextFlags CONTEXT_WATER = 1 << 4; // Water rooms +constexpr ContextFlags CONTEXT_CLAN_ROOM = 1 << 5; // Clan-specific rooms +constexpr ContextFlags CONTEXT_ALWAYS = 0; // No context restrictions + +// User preference system for command disambiguation +struct UserCommandPreferences { + std::unordered_map<std::string, std::string> abbreviation_overrides; // "n" -> "north" + std::unordered_map<CommandCategory, int> category_priorities; // Custom category weights + bool prefer_combat_commands = false; // Prefer combat in ambiguous cases + bool prefer_movement_commands = true; // Prefer movement (default true) +}; + +using UniformFunction = void (*)(CharData *, Arguments); +using PermissionFlags = uint32_t; + +// Lambda handler - wrapper for runtime functions with any capture +class LambdaHandler { + public: + virtual ~LambdaHandler() = default; + virtual void invoke(CharData *data, Arguments args) = 0; +}; + +// Template implementation that wraps any lambda/functor +template <typename F> class LambdaHandlerImpl : public LambdaHandler { + private: + F func_; + + public: + LambdaHandlerImpl(F &&func) : func_(std::forward<F>(func)) {} + + void invoke(CharData *data, Arguments args) override { func_(data, args); } +}; + +// Function registry with runtime registration support +class FunctionRegistry { + private: + // Enhanced function info structure with category-based disambiguation + struct FunctionInfo { + std::string name; + UniformFunction func; + size_t priority; + PermissionFlags permissions; + std::string description; + CommandCategory category; + ContextFlags context_flags; + + // Enhanced comparison for sophisticated disambiguation + bool operator<(const FunctionInfo &other) const { + // 1. Category-based priority (movement commands beat social commands) + if (category != other.category) { + return get_category_priority(category) > get_category_priority(other.category); + } + + // 2. Explicit priority within category + if (priority != other.priority) { + return priority > other.priority; // Higher priority first + } + + // 3. Context-specific priority (combat vs peaceful) + auto context_priority = get_context_priority(context_flags); + auto other_context_priority = get_context_priority(other.context_flags); + if (context_priority != other_context_priority) { + return context_priority > other_context_priority; + } + + // 4. Alphabetical for final tie-breaking + return name < other.name; + } + + // Helper functions for priority calculation + static constexpr size_t get_category_priority(CommandCategory cat) { + using enum CommandCategory; + switch (cat) { + case MOVEMENT: + return 100; // Highest: n = north + case COMBAT: + return 90; // High: k = kill + case COMMUNICATION: + return 80; // Medium-high: s = say + case OBJECT_MANIPULATION: + return 70; // Medium: g = get + case INFORMATION: + return 60; // Medium-low: l = look + case SOCIAL: + return 50; // Low: n = nap + case ADMINISTRATIVE: + return 40; // Lower + case SYSTEM: + return 30; // Lowest + default: + return 0; + } + } + + static constexpr size_t get_context_priority(ContextFlags flags) { + size_t priority = 0; + if (flags & CONTEXT_COMBAT) + priority += 20; + if (flags & CONTEXT_PEACEFUL) + priority += 10; + if (flags & CONTEXT_INDOOR) + priority += 5; + if (flags & CONTEXT_OUTDOOR) + priority += 3; + return priority; + } + }; + + // Primary storage: map for O(1) direct lookups by name + inline static std::unordered_map<std::string, UniformFunction> function_map_; + + // Secondary storage: sorted vector for prefix matching and priority ordering + inline static std::vector<FunctionInfo> sorted_functions_; + + // Storage for lambda handlers (owns the lambdas) + inline static std::vector<std::unique_ptr<LambdaHandler>> lambda_handlers_; + + // Abbreviation cache + inline static std::unordered_map<std::string, std::string> abbreviation_cache_; + inline static bool abbreviation_cache_valid_ = false; + + // Update the abbreviation cache based on the sorted functions list + static void update_abbreviation_cache() { + abbreviation_cache_.clear(); + + // For each prefix length, starting with shortest + for (size_t prefix_len = 1; prefix_len <= 10; prefix_len++) { + // Group functions by their prefix of this length + std::unordered_map<std::string, std::vector<FunctionInfo *>> prefix_groups; + + for (auto &func_info : sorted_functions_) { + if (func_info.name.length() >= prefix_len) { + std::string prefix = func_info.name.substr(0, prefix_len); + prefix_groups[prefix].push_back(&func_info); + } + } + + // For each prefix group, if there's only one function, it's unique + // Otherwise, select by priority and then alphabetically + for (auto &[prefix, funcs] : prefix_groups) { + if (funcs.size() == 1) { + // Unique prefix + abbreviation_cache_[prefix] = funcs[0]->name; + } else { + // Multiple functions with this prefix - sorted_functions_ is already + // sorted by priority and then name, so the first match is correct + abbreviation_cache_[prefix] = funcs[0]->name; + } + } + } + + // Also add full function names for completeness + for (const auto &func_info : sorted_functions_) { + abbreviation_cache_[func_info.name] = func_info.name; + } + + abbreviation_cache_valid_ = true; + } + + public: + // Enhanced registration with category and context support + static void register_function(std::string_view name, UniformFunction func, size_t priority, + CommandCategory category = CommandCategory::UNKNOWN, + ContextFlags context = CONTEXT_ALWAYS, + std::optional<PermissionFlags> required_permissions = std::nullopt, + std::string description = "") { + std::string name_str(name); + + // Add to the map + function_map_[name_str] = func; + + // Add to the sorted vector with enhanced metadata + sorted_functions_.push_back( + {name_str, func, priority, required_permissions.value_or(0), std::move(description), category, context}); + + // Resort the vector to maintain the priority ordering + std::sort(sorted_functions_.begin(), sorted_functions_.end()); + + // Invalidate the abbreviation cache + abbreviation_cache_valid_ = false; + } + + // Runtime function registration with proper lambda support + template <typename F> static void register_runtime_function(std::string_view name, F &&func, size_t priority) { + // Create a handler for the lambda (preserves captures and state) + auto handler = std::make_unique<LambdaHandlerImpl<F>>(std::forward<F>(func)); + + // Add the handler to our storage (we'll own it now) + lambda_handlers_.push_back(std::move(handler)); + + // Create a trampoline function that calls the handler + auto trampoline = [](CharData *data, Arguments args) { + // Find the last handler (most recently added) + auto &last_handler = lambda_handlers_.back(); + last_handler->invoke(data, args); + }; + + // Register the trampoline function + register_function(name, trampoline, priority); + } + + // Call a function by name - O(1) lookup + static bool call(std::string_view name, CharData *character, Arguments args) { + auto it = function_map_.find(std::string(name)); + if (it != function_map_.end()) { + log("Executing function: {}", name); + it->second(character, args); + return true; + } + + log("Function '{}' not found\n", name); + return false; + } + + // Check if a function can be called with given permissions + static bool can_call_function(std::string_view name, PermissionFlags user_permissions, CharData *character = nullptr) { + auto it = function_map_.find(std::string(name)); + if (it == function_map_.end()) { + return false; + } + + // Immortals bypass all permission checks + if (character && GET_LEVEL(character) >= LVL_IMMORT) { + return true; + } + + // Find the function info to check permissions + for (const auto &func_info : sorted_functions_) { + if (func_info.name == name) { + return (func_info.permissions & user_permissions) == func_info.permissions; + } + } + return false; + } + + // Call a function by abbreviation - O(1) lookup with cache and fuzzy matching fallback + static bool call_by_abbrev(std::string_view abbrev, CharData *character, Arguments args) { + // Ensure abbreviation cache is valid + if (!abbreviation_cache_valid_) { + update_abbreviation_cache(); + } + + // Try direct function lookup first + auto direct_it = function_map_.find(std::string(abbrev)); + if (direct_it != function_map_.end()) { + log("Executing function: {}", abbrev); + direct_it->second(character, args); + return true; + } + + // Try the abbreviation cache + auto abbrev_it = abbreviation_cache_.find(std::string(abbrev)); + if (abbrev_it != abbreviation_cache_.end()) { + const std::string &full_name = abbrev_it->second; + log("Executing function '{}' using abbreviation '{}'", full_name, abbrev); + function_map_[full_name](character, args); + return true; + } + + // Try fuzzy matching as fallback for longer abbreviations + if (abbrev.length() >= 4) { + auto fuzzy_matches = find_fuzzy_matches(abbrev, 2); + if (!fuzzy_matches.empty()) { + // Only use fuzzy match if there's a clear best candidate + if (fuzzy_matches.size() == 1 || + (fuzzy_matches.size() <= 3 && fuzzy_matches[0].second < fuzzy_matches[1].second)) { + const auto &best_match = fuzzy_matches[0].first; + log("Executing function '{}' using fuzzy match for '{}' (distance: {})", best_match->name, abbrev, + fuzzy_matches[0].second); + best_match->func(character, args); + return true; + } + } + } + + // If we get here, no matching function was found + log("No function matches abbreviation '{}'\n", abbrev); + return false; + } + + // Call a function by abbreviation with permission checking + static bool call_by_abbrev_with_permissions(std::string_view abbrev, CharData *character, Arguments args, + PermissionFlags user_permissions) { + // Ensure abbreviation cache is valid + if (!abbreviation_cache_valid_) { + update_abbreviation_cache(); + } + + std::string resolved_name; + + // Try direct function lookup first + auto direct_it = function_map_.find(std::string(abbrev)); + if (direct_it != function_map_.end()) { + resolved_name = std::string(abbrev); + } else { + // Try the abbreviation cache + auto abbrev_it = abbreviation_cache_.find(std::string(abbrev)); + if (abbrev_it != abbreviation_cache_.end()) { + resolved_name = abbrev_it->second; + } + } + + if (resolved_name.empty()) { + // Try fuzzy matching as fallback for longer abbreviations + if (abbrev.length() >= 4) { + auto fuzzy_matches = find_fuzzy_matches(abbrev, 2); + if (!fuzzy_matches.empty()) { + // Only use fuzzy match if there's a clear best candidate + if (fuzzy_matches.size() == 1 || + (fuzzy_matches.size() <= 3 && fuzzy_matches[0].second < fuzzy_matches[1].second)) { + + const auto &best_match = fuzzy_matches[0].first; + + // Check permissions for the fuzzy match + if (can_call_function(best_match->name, user_permissions, character)) { + log("Executing function '{}' using fuzzy match for '{}' (distance: {})", + best_match->name, abbrev, fuzzy_matches[0].second); + best_match->func(character, args); + return true; + } else { + // Found a fuzzy match but no permission + log("Permission denied for fuzzy match function '{}'", best_match->name); + return false; + } + } + } + } + + log("No function matches abbreviation '{}'", abbrev); + return false; + } + + // Check permissions + if (!can_call_function(resolved_name, user_permissions, character)) { + // Find the function info to get the required permissions for logging + PermissionFlags required_permissions = 0; + for (const auto &func_info : sorted_functions_) { + if (func_info.name == resolved_name) { + required_permissions = func_info.permissions; + break; + } + } + log("Permission denied for function '{}' Required permissions: {}, Current permissions: {}", resolved_name, + required_permissions, user_permissions); + return false; + } + + // Call the function + log("Executing function '{}' with permission check", resolved_name); + function_map_[resolved_name](character, args); + return true; + } + + // Calculate simple edit distance for fuzzy matching + static int edit_distance(std::string_view s1, std::string_view s2, int max_distance = 3) { + if (s1.empty()) + return static_cast<int>(s2.length()); + if (s2.empty()) + return static_cast<int>(s1.length()); + + // Early exit if length difference exceeds max_distance + int len_diff = std::abs(static_cast<int>(s1.length()) - static_cast<int>(s2.length())); + if (len_diff > max_distance) + return max_distance + 1; + + std::vector<int> prev(s2.length() + 1); + std::vector<int> curr(s2.length() + 1); + + // Initialize first row + for (size_t j = 0; j <= s2.length(); ++j) { + prev[j] = static_cast<int>(j); + } + + for (size_t i = 1; i <= s1.length(); ++i) { + curr[0] = static_cast<int>(i); + + int min_in_row = curr[0]; + for (size_t j = 1; j <= s2.length(); ++j) { + int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; + curr[j] = std::min({ + curr[j - 1] + 1, // insertion + prev[j] + 1, // deletion + prev[j - 1] + cost // substitution + }); + min_in_row = std::min(min_in_row, curr[j]); + } + + // Early exit if minimum distance in this row exceeds threshold + if (min_in_row > max_distance) { + return max_distance + 1; + } + + prev = curr; + } + + return curr[s2.length()]; + } + + // Find fuzzy matches when exact/prefix matching fails + static std::vector<std::pair<const FunctionInfo *, int>> find_fuzzy_matches(std::string_view abbrev, + int max_distance = 2) { + std::vector<std::pair<const FunctionInfo *, int>> fuzzy_candidates; + + for (const auto &func : sorted_functions_) { + int distance = edit_distance(abbrev, func.name, max_distance); + if (distance <= max_distance) { + fuzzy_candidates.emplace_back(&func, distance); + } + } + + // Sort by distance (closest first), then by priority + std::sort(fuzzy_candidates.begin(), fuzzy_candidates.end(), [](const auto &a, const auto &b) { + if (a.second != b.second) + return a.second < b.second; + return *a.first < *b.first; + }); + + return fuzzy_candidates; + } + + // Enhanced abbreviation resolution with context awareness and fuzzy matching + static bool call_by_abbrev_contextual(std::string_view abbrev, CharData *character, Arguments args, + PermissionFlags user_permissions = 0, + const UserCommandPreferences *user_prefs = nullptr) { + if (!abbreviation_cache_valid_) { + update_abbreviation_cache(); + } + + // Collect all matching functions (exact and prefix matches) + std::vector<const FunctionInfo *> candidates; + + // Check for exact match first + auto exact_it = function_map_.find(std::string(abbrev)); + if (exact_it != function_map_.end()) { + for (const auto &func : sorted_functions_) { + if (func.name == abbrev) { + candidates.push_back(&func); + break; + } + } + } + + // Find prefix matches if no exact match + if (candidates.empty()) { + for (const auto &func : sorted_functions_) { + if (func.name.starts_with(abbrev)) { + candidates.push_back(&func); + } + } + } + + // Try fuzzy matching if no prefix matches found + if (candidates.empty() && abbrev.length() >= 4) { // Only for longer abbreviations + auto fuzzy_matches = find_fuzzy_matches(abbrev, 2); + if (!fuzzy_matches.empty()) { + // Only use fuzzy match if there's a clear best candidate or very few options + if (fuzzy_matches.size() == 1 || + (fuzzy_matches.size() <= 3 && fuzzy_matches[0].second < fuzzy_matches[1].second)) { + candidates.push_back(fuzzy_matches[0].first); + log("Using fuzzy match '{}' for abbreviation '{}' (distance: {})", fuzzy_matches[0].first->name, + abbrev, fuzzy_matches[0].second); + } + } + } + + if (candidates.empty()) { + log("No function matches abbreviation '{}'", abbrev); + return false; + } + + // Apply contextual filtering and user preferences + auto best_match = select_best_command(candidates, character, user_prefs); + if (!best_match) { + log("No suitable function found for abbreviation '{}'", abbrev); + return false; + } + + // Check permissions if specified + if (user_permissions != 0) { + if ((best_match->permissions & user_permissions) != best_match->permissions) { + log("Permission denied for function '{}'", best_match->name); + return false; + } + } + + // Execute the selected command + log("Executing contextual function '{}' using abbreviation '{}'", best_match->name, abbrev); + best_match->func(character, args); + return true; + } + + private: + // Select the best command from candidates based on context and user preferences + static const FunctionInfo *select_best_command(const std::vector<const FunctionInfo *> &candidates, + CharData *character, const UserCommandPreferences *user_prefs) { + if (candidates.empty()) + return nullptr; + if (candidates.size() == 1) + return candidates[0]; + + // Apply user preference overrides first + if (user_prefs && !user_prefs->abbreviation_overrides.empty()) { + // Implementation would check user overrides here + } + + // Apply contextual scoring + std::vector<std::pair<const FunctionInfo *, int>> scored_candidates; + for (const auto *candidate : candidates) { + int score = calculate_contextual_score(*candidate, character, user_prefs); + scored_candidates.emplace_back(candidate, score); + } + + // Sort by score (highest first), then by the function's natural ordering + std::sort(scored_candidates.begin(), scored_candidates.end(), [](const auto &a, const auto &b) { + if (a.second != b.second) { + return a.second > b.second; // Higher score first + } + return *a.first < *b.first; // Natural function ordering + }); + + return scored_candidates[0].first; + } + + // Calculate contextual score for command selection + static int calculate_contextual_score(const FunctionInfo &func, CharData *character, + const UserCommandPreferences *user_prefs) { + int score = 0; + + // Base category priority (from the comparison function) + score += FunctionInfo::get_category_priority(func.category); + + // Context-specific bonuses + if (character) { + // Check if character is in combat + bool in_combat = false; // You'd implement this check + if (in_combat && (func.context_flags & CONTEXT_COMBAT)) { + score += 25; + } else if (!in_combat && (func.context_flags & CONTEXT_PEACEFUL)) { + score += 15; + } + + // Room-specific context bonuses + // You'd add room type checks here based on character's location + } + + // User preference bonuses + if (user_prefs) { + if (user_prefs->prefer_combat_commands && func.category == CommandCategory::COMBAT) { + score += 10; + } + if (user_prefs->prefer_movement_commands && func.category == CommandCategory::MOVEMENT) { + score += 10; + } + + // Custom category priorities + auto cat_prio = user_prefs->category_priorities.find(func.category); + if (cat_prio != user_prefs->category_priorities.end()) { + score += cat_prio->second; + } + } + + // Explicit priority bonus + score += static_cast<int>(func.priority); + + return score; + } + + public: + // Execute all functions in priority order + static void call_all(CharData *character, Arguments args) { + log("Calling all functions in priority order:\n"); + for (const auto &func_info : sorted_functions_) { + log(" {} (priority: {})\n", func_info.name, func_info.priority); + func_info.func(character, args); + } + } + + static std::string print_available(PermissionFlags permissions) { + if (!abbreviation_cache_valid_) { + update_abbreviation_cache(); + } + std::string result; + result += "Available functions:\n"; + for (const auto &func_info : sorted_functions_) { + if ((func_info.permissions & permissions) == permissions) { + result += fmt::format(" {} (priority: {}) - {}\n", func_info.name, func_info.priority, + func_info.description); + } + } + return result; + } + + // Print available functions with a specific prefix, formatting them nicely for user commands + static std::string + print_available_with_prefix(std::string_view prefix, PermissionFlags permissions, + std::function<std::string(std::string_view)> name_formatter = nullptr) { + if (!abbreviation_cache_valid_) { + update_abbreviation_cache(); + } + std::string result; + + for (const auto &func_info : sorted_functions_) { + if ((func_info.permissions & permissions) == func_info.permissions && func_info.name.starts_with(prefix)) { + + std::string display_name = func_info.name; + if (name_formatter) { + display_name = name_formatter(func_info.name); + } + + result += fmt::format(" {} - {}\n", display_name, func_info.description); + } + } + return result; + } + + // Print information about registered functions + static void print_info() { + log("Registry contains {} functions:\n", sorted_functions_.size()); + + for (const auto &func_info : sorted_functions_) { + log(" {} (priority: {})\n", func_info.name, func_info.priority); + } + } + + // Print available abbreviations + static void print_abbreviations() { + if (!abbreviation_cache_valid_) { + update_abbreviation_cache(); + } + + log("Available abbreviations:\n"); + + // Group by full function name for cleaner output + std::unordered_map<std::string, std::vector<std::string>> grouped_abbrevs; + + for (const auto &[abbrev, full_name] : abbreviation_cache_) { + if (abbrev != full_name) { // Don't show full names as abbreviations + grouped_abbrevs[full_name].push_back(abbrev); + } + } + + // Print sorted by function name + std::vector<std::string> func_names; + for (const auto &[name, _] : grouped_abbrevs) { + func_names.push_back(name); + } + std::sort(func_names.begin(), func_names.end()); + + for (const auto &name : func_names) { + auto &abbrevs = grouped_abbrevs[name]; + std::sort(abbrevs.begin(), abbrevs.end(), [](const auto &a, const auto &b) { + return a.length() < b.length(); // Sort by length (shortest first) + }); + + std::string abbrev_list; + for (const auto &abbrev : abbrevs) { + if (!abbrev_list.empty()) + abbrev_list += ", "; + abbrev_list += abbrev; + } + + log(" {} → {}\n", abbrev_list, name); + } + } + + // Remove a function by name + static bool unregister_function(std::string_view name) { + std::string name_str(name); + + // Remove from the map + auto map_it = function_map_.find(name_str); + if (map_it == function_map_.end()) { + log("Function '{}' not found for removal\n", name); + return false; + } + + function_map_.erase(map_it); + + // Remove from the sorted vector + auto vec_it = std::find_if(sorted_functions_.begin(), sorted_functions_.end(), + [&name_str](const auto &info) { return info.name == name_str; }); + + if (vec_it != sorted_functions_.end()) { + sorted_functions_.erase(vec_it); + } + + // Invalidate abbreviation cache + abbreviation_cache_valid_ = false; + + log("Function '{}' removed from registry\n", name); + return true; + } +}; + +// Simple registration macro - only required parameters +#define REGISTER_FUNCTION(func, name, required_permissions, description) \ + inline static const auto func##_reg = []() { \ + FunctionRegistry::register_function(name, func, 0, CommandCategory::UNKNOWN, CONTEXT_ALWAYS, \ + required_permissions, description); \ + return true; \ + }(); + +// Registration with category for enhanced disambiguation +#define REGISTER_FUNCTION_WITH_CATEGORY(func, name, category, required_permissions, description) \ + inline static const auto func##_reg = []() { \ + FunctionRegistry::register_function(name, func, 0, category, CONTEXT_ALWAYS, required_permissions, \ + description); \ + return true; \ + }(); diff --git a/src/graph.cpp b/src/graph.cpp index 157fa150..53473780 100644 --- a/src/graph.cpp +++ b/src/graph.cpp @@ -690,7 +690,7 @@ bool cause_single_track(TrackInfo track, CharData *ch, CharData *victim, int tra if (EXIT_IS_CLOSED(CH_EXIT(ch, direction)) && GET_LEVEL(ch) < LVL_GOD) { strcpy(doorname, exit_name(CH_EXIT(ch, direction))); char_printf(ch, "You try to open the {}.\n", doorname); - sprintf(doorname, "%s %s", doorname, dirs[direction]); + sprintf(doorname, "%s %s", doorname, dirs[direction].data()); cmd = find_command("cmd"); do_gen_door(ch, doorname, cmd, 0); if (EXIT_IS_CLOSED(CH_EXIT(ch, direction))) { diff --git a/src/handler.cpp b/src/handler.cpp index 93b1c098..a78b5246 100644 --- a/src/handler.cpp +++ b/src/handler.cpp @@ -87,7 +87,7 @@ int isname(const char *str, const char *namelist) { if (!*curstr || *curname == ' ') break; - if (LOWER(*curstr) != LOWER(*curname)) + if (to_lower(*curstr) != to_lower(*curname)) break; } @@ -1246,6 +1246,9 @@ void extract_char(CharData *ch) { if (ch->guarded_by) stop_guarding(ch->guarded_by); + // Clean up clan snooping + remove_all_clan_snoops(ch); + if (ch->cornering) { if (ch->cornering->cornered_by == ch) ch->cornering->cornered_by = nullptr; @@ -1284,10 +1287,6 @@ void extract_char(CharData *ch) { obj_to_room(obj, ch->in_room); } - /* Remove runtime link to clan */ - if (GET_CLAN_MEMBERSHIP(ch)) - GET_CLAN_MEMBERSHIP(ch)->player = nullptr; - /* transfer equipment to room */ for (i = 0; i < NUM_WEARS; i++) if (GET_EQ(ch, i)) diff --git a/src/house.cpp b/src/house.cpp index 4cd48c8c..6f9fb8e2 100644 --- a/src/house.cpp +++ b/src/house.cpp @@ -182,7 +182,10 @@ void House_boot(void) { return; } while (!feof(fl) && num_of_houses < MAX_HOUSES) { - fread(&temp_house, sizeof(HouseControlRec), 1, fl); + if (fread(&temp_house, sizeof(HouseControlRec), 1, fl) != 1) { + log("SYSERR: Error reading house control record."); + break; + } if (feof(fl)) break; @@ -304,7 +307,7 @@ void hcontrol_build_house(CharData *ch, char *arg) { char_printf(ch, HCONTROL_FORMAT); return; } - if ((exit_num = searchblock(arg1, dirs, false)) < 0) { + if ((exit_num = search_block(arg1, dirs, false)) < 0) { char_printf(ch, "'{}' is not a valid direction.\n", arg1); return; } diff --git a/src/interpreter.cpp b/src/interpreter.cpp index d1869487..3f3f0f02 100644 --- a/src/interpreter.cpp +++ b/src/interpreter.cpp @@ -110,6 +110,7 @@ ACMD(do_coredump); ACMD(do_corner); ACMD(do_credits); ACMD(do_ctell); +ACMD(do_csnoop); ACMD(do_date); ACMD(do_dc); ACMD(do_desc); @@ -153,7 +154,6 @@ ACMD(do_hcontrol); ACMD(do_hide); ACMD(do_hit); ACMD(do_hitall); -ACMD(do_hotboot); ACMD(do_house); ACMD(do_hunt); ACMD(do_iedit); @@ -454,6 +454,7 @@ const CommandInfo cmd_info[] = { {"clist", POS_PRONE, STANCE_DEAD, do_csearch, LVL_ATTENDANT, SCMD_VLIST, CMD_ANY}, {"csearch", POS_PRONE, STANCE_DEAD, do_csearch, LVL_ATTENDANT, SCMD_VSEARCH, CMD_ANY}, {"ctell", POS_PRONE, STANCE_SLEEPING, do_ctell, 0, 0, CMD_ANY}, + {"csnoop", POS_PRONE, STANCE_SLEEPING, do_csnoop, LVL_IMMORT, 0, CMD_ANY}, {"cuddle", POS_PRONE, STANCE_RESTING, do_action, 0, 0, 0}, {"curse", POS_PRONE, STANCE_RESTING, do_action, 0, 0, 0}, {"curtsey", POS_STANDING, STANCE_ALERT, do_action, 0, 0, CMD_NOFIGHT}, @@ -574,12 +575,12 @@ const CommandInfo cmd_info[] = { {"hitall", POS_STANDING, STANCE_ALERT, do_hitall, 0, SCMD_HITALL, 0}, {"hold", POS_PRONE, STANCE_RESTING, do_grab, 1, 0, 0}, {"hop", POS_STANDING, STANCE_ALERT, do_action, 0, 0, CMD_NOFIGHT}, - {"hotboot", POS_PRONE, STANCE_DEAD, do_hotboot, LVL_REBOOT_MASTER, 0, 0}, {"house", POS_PRONE, STANCE_RESTING, do_house, -1, 0, 0}, {"howl", POS_STANDING, STANCE_ALERT, do_roar, 0, SCMD_HOWL, 0}, {"hunt", POS_STANDING, STANCE_ALERT, do_hunt, -1, 0, CMD_NOFIGHT}, {"hug", POS_PRONE, STANCE_RESTING, do_action, 0, 0, 0}, {"hunger", POS_PRONE, STANCE_RESTING, do_action, 0, 0, 0}, + {"hour", POS_PRONE, STANCE_DEAD, do_inctime, LVL_HEAD_C, 0, CMD_ANY}, {"inventory", POS_PRONE, STANCE_SLEEPING, do_inventory, 0, 0, CMD_ANY}, {"identify", POS_PRONE, STANCE_RESTING, do_identify, 0, 0, CMD_HIDE | CMD_OLC}, @@ -593,7 +594,7 @@ const CommandInfo cmd_info[] = { {"infodump", POS_PRONE, STANCE_DEAD, do_infodump, LVL_HEAD_C, 0, CMD_ANY}, {"ignore", POS_PRONE, STANCE_DEAD, do_ignore, 0, 0, CMD_ANY}, {"inctime", POS_PRONE, STANCE_DEAD, do_inctime, LVL_HEAD_C, 0, CMD_ANY}, - {"hour", POS_PRONE, STANCE_DEAD, do_inctime, LVL_HEAD_C, 0, CMD_ANY}, + {"items", POS_STANDING, STANCE_ALERT, do_not_here, 1, 0, CMD_NOFIGHT}, {"info", POS_PRONE, STANCE_DEAD, do_textview, 0, SCMD_INFO, CMD_ANY}, {"insult", POS_PRONE, STANCE_RESTING, do_insult, 0, 0, 0}, {"invis", POS_PRONE, STANCE_DEAD, do_invis, LVL_IMMORT, 0, CMD_ANY}, @@ -739,6 +740,7 @@ const CommandInfo cmd_info[] = { {"pain", POS_PRONE, STANCE_DEAD, do_pain, LVL_RESTORE, 0, CMD_OLC}, {"rpain", POS_PRONE, STANCE_DEAD, do_rpain, LVL_RESTORE, 0, CMD_OLC}, {"retreat", POS_STANDING, STANCE_ALERT, do_retreat, 0, 0, 0}, + {"retrieve", POS_STANDING, STANCE_ALERT, do_not_here, 1, 0, CMD_NOFIGHT}, {"return", POS_PRONE, STANCE_DEAD, do_return, -1, 0, CMD_MINOR_PARA | CMD_MAJOR_PARA | CMD_BOUND}, {"redit", POS_PRONE, STANCE_DEAD, do_olc, LVL_BUILDER, SCMD_OLC_REDIT, 0}, {"rcopy", POS_PRONE, STANCE_DEAD, do_olc, LVL_BUILDER, SCMD_OLC_RCOPY, 0}, @@ -827,6 +829,7 @@ const CommandInfo cmd_info[] = { {"stay", POS_PRONE, STANCE_RESTING, do_move, 0, SCMD_STAY, CMD_HIDE | CMD_OLC}, {"steal", POS_STANDING, STANCE_ALERT, do_steal, 1, 0, CMD_HIDE | CMD_NOFIGHT}, {"steam", POS_PRONE, STANCE_RESTING, do_action, 0, 0, 0}, + {"store", POS_STANDING, STANCE_ALERT, do_not_here, 1, 0, CMD_NOFIGHT}, {"stow", POS_PRONE, STANCE_RESTING, do_stow, 0, 0, CMD_HIDE}, {"stomp", POS_STANDING, STANCE_ALERT, do_stomp, 0, 0, 0}, {"stroke", POS_PRONE, STANCE_RESTING, do_action, 0, 0, 0}, @@ -995,11 +998,9 @@ const CommandInfo cmd_info[] = { {"\n", 0, 0, 0, 0, 0, CMD_HIDE}}; /* this must be last */ -const char *command_flags[] = {"MEDITATE", "MAJOR PARA", "MINOR PARA", "HIDE", "BOUND", "CAST", "OLC", "NOFIGHT", "\n"}; - -const char *fill[] = {"in", "from", "with", "the", "on", "at", "to", "\n"}; +constexpr std::string_view fill[] = {"in", "from", "with", "the", "on", "at", "to", "\n"}; -const char *reserved[] = {"self", "me", "all", "room", "someone", "something", "\n"}; +constexpr std::string_view reserved[] = {"self", "me", "all", "room", "someone", "something", "\n"}; void list_similar_commands(CharData *ch, char *arg) { int found = false, cmd; @@ -1352,63 +1353,6 @@ int perform_alias(DescriptorData *d, char *orig) { * Various other parsing utilities * **************************************************************************/ -/* - * searches an array of strings for a target string. "exact" can be - * 0 or non-0, depending on whether or not the match must be exact for - * it to be returned. Returns -1 if not found; 0..n otherwise. Array - * must be terminated with a '\n' so it knows to stop searching. - * - * searchblock follows a similar naming convention to strcasecmp: - * searchblock is case-sensitive, search_block is case-insensitive. - * Often, which one you use only depends on the case of items in your - * list, because any_one_arg and one_argument always return lower case - * arguments. - */ -int searchblock(char *arg, const char **list, bool exact) { - int i, l; - - /* Make into lower case, and get length of string */ - for (l = 0; *(arg + l); l++) - *(arg + l) = LOWER(*(arg + l)); - - if (exact) { - for (i = 0; **(list + i) != '\n'; i++) - if (!strcasecmp(arg, *(list + i))) - return (i); - } else { - if (!l) - l = 1; /* Avoid "" to match the first available - * string */ - for (i = 0; **(list + i) != '\n'; i++) - if (!strncasecmp(arg, *(list + i), l)) - return (i); - } - - return -1; -} - -int search_block(const char *arg, const char **list, bool exact) { - int i, len; - - if (!arg) - return -1; - - if (exact) { - for (i = 0; **(list + i) != '\n'; i++) - if (!strcasecmp(arg, *(list + i))) - return (i); - } else { - len = strlen(arg); - if (!len) - len = 1; /* Avoid "" to match the first available string */ - for (i = 0; **(list + i) != '\n'; i++) - if (!strncasecmp(arg, *(list + i), (unsigned)len)) - return (i); - } - - return (-1); -} - /* \s*\d+ */ bool is_number(const char *str) { if (!str || !*str) @@ -1518,7 +1462,7 @@ char *one_argument(char *argument, char *first_arg) { first_arg = begin; while (*argument && !isspace(*argument)) { - *(first_arg++) = LOWER(*argument); + *(first_arg++) = to_lower(*argument); argument++; } @@ -1534,13 +1478,13 @@ char *delimited_arg(char *argument, char *first_arg, char delimiter) { if (*argument == delimiter) { argument++; while (*argument && *argument != delimiter) { - *(first_arg++) = LOWER(*argument); + *(first_arg++) = to_lower(*argument); argument++; } argument++; } else { while (*argument && !isspace(*argument)) { - *(first_arg++) = LOWER(*argument); + *(first_arg++) = to_lower(*argument); argument++; } } @@ -1579,13 +1523,13 @@ char *delimited_arg_all(char *argument, char *first_arg, char delimiter) { if (*argument == delimiter) { argument++; while (*argument && *argument != delimiter) { - *(first_arg++) = LOWER(*argument); + *(first_arg++) = to_lower(*argument); argument++; } argument++; } else { while (*argument) { - *(first_arg++) = LOWER(*argument); + *(first_arg++) = to_lower(*argument); argument++; } } @@ -1606,7 +1550,7 @@ char *any_one_arg(char *argument, char *first_arg) { skip_spaces(&argument); while (*argument && !isspace(*argument)) { - *(first_arg++) = LOWER(*argument); + *(first_arg++) = to_lower(*argument); argument++; } @@ -1636,7 +1580,7 @@ int is_abbrev(const char *arg1, const char *arg2) { return 0; for (; *arg1 && *arg2; arg1++, arg2++) - if (LOWER(*arg1) != LOWER(*arg2)) + if (to_lower(*arg1) != to_lower(*arg2)) return 0; if (!*arg1) @@ -2012,8 +1956,7 @@ int enter_player_game(DescriptorData *d) { GET_QUIT_REASON(d->character) = QUIT_AUTOSAVE; // A couple of hacks to reconnect things if the player logged out to the menu and then re-entered the game - if (GET_CLAN_MEMBERSHIP(d->character)) - GET_CLAN_MEMBERSHIP(d->character)->player = d->character; + clan_repository.load_clan_membership(std::make_shared<CharData>(*d->character)); // restart cooldowns for (int i = 0; i < NUM_COOLDOWNS; ++i) @@ -2555,8 +2498,10 @@ void nanny(DescriptorData *d, char *arg) { string_to_output(d, NEWSUPDATED2); } - if (GET_CLAN(d->character) && GET_CLAN(d->character)->motd) - desc_printf(d, "\n{}{} news:\n{}", GET_CLAN(d->character)->name, ANRM, GET_CLAN(d->character)->motd); + auto clan = get_clan(d->character); + if (clan) { + desc_printf(d, "\n{}{} news:\n{}", clan.value()->name(), ANRM, clan.value()->motd()); + } // Convert timestamp to string auto time = std::chrono::system_clock::from_time_t(d->character->player.time.logon); @@ -2619,7 +2564,7 @@ void nanny(DescriptorData *d, char *arg) { break; - case CON_QSEX: /* query sex of new user */ + case CON_QSEX: /* query gender of new user */ switch (*arg) { case 'm': case 'M': @@ -3268,8 +3213,11 @@ void nanny(DescriptorData *d, char *arg) { STATE(d) = CON_CLOSE; return; } - if (GET_CLAN_MEMBERSHIP(d->character)) - revoke_clan_membership(GET_CLAN_MEMBERSHIP(d->character)); + + auto clan = get_clan(d->character); + if (clan) { + clan.value()->remove_member_by_name(d->character->player.short_descr); + } if ((player_i = get_ptable_by_name(GET_NAME(d->character))) >= 0) { SET_BIT(player_table[player_i].flags, PINDEX_DELETED); @@ -3324,7 +3272,7 @@ long max_exp_gain(CharData *ch) { int bonus_stat(CharData *ch, char arg) { int b; int a; - arg = LOWER(arg); + arg = to_lower(arg); switch (arg) { case 'w': b = random_number(2, 6); diff --git a/src/interpreter.hpp b/src/interpreter.hpp index 75af83ef..6b26bec0 100644 --- a/src/interpreter.hpp +++ b/src/interpreter.hpp @@ -12,9 +12,12 @@ #pragma once +#include "string_utils.hpp" #include "structs.hpp" #include "sysdep.hpp" +#include <span> + #define ACMD(name) \ void(name)(CharData * ch, [[maybe_unused]] char *argument, [[maybe_unused]] int cmd, [[maybe_unused]] int subcmd) @@ -22,11 +25,82 @@ #define CMD_IS(cmd_name) (!strcasecmp(cmd_name, cmd_info[cmd].command)) #define IS_MOVE(cmdnum) (cmdnum >= 1 && cmdnum <= 6) +/* + * searches an array of strings for a target string. "exact" can be + * true or false, depending on whether or not the match must be exact for + * it to be returned. Returns -1 if not found; 0..n otherwise. + * + * If the passed in haystack is an old c-array, it must be terminated with + * a '\n' so it knows to stop searching. + * + * If the passed in haystack is a std::array, it will search until the + * end of the array. + * + * search_block follows a similar naming convention to strcasecmp: + * search_block is case-sensitive, search_block is case-insensitive. + * Often, which one you use only depends on the case of items in your + * list, because any_one_arg and one_argument always return lower case + * arguments. + */ + +// Primary implementation for std::array +template <std::size_t N> +int search_block(const std::string_view needle, const std::array<std::string_view, N> &haystack, bool exact) { + for (std::size_t i = 0; i < haystack.size(); ++i) + if (exact ? matches(needle, haystack[i]) : matches_start(needle, haystack[i])) + return static_cast<int>(i); + return -1; +} + +// Implementation for std::span (for C-style arrays with known size) +inline int search_block(const std::string_view needle, std::span<const std::string_view> haystack, bool exact) { + for (std::size_t i = 0; i < haystack.size(); ++i) + if (exact ? matches(needle, haystack[i]) : matches_start(needle, haystack[i])) + return static_cast<int>(i); + return -1; +} + +// Implementation for null-terminated arrays +inline int search_block(const std::string_view needle, const std::string_view haystack[], bool exact) { + for (std::size_t i = 0; haystack[i].front() != '\n'; ++i) + if (exact ? matches(needle, haystack[i]) : matches_start(needle, haystack[i])) + return static_cast<int>(i); + return -1; +} + +// Implementation for C-style null-terminated string arrays +inline int search_block(const std::string_view needle, const char *const haystack[], bool exact) { + for (std::size_t i = 0; *haystack[i] != '\n'; ++i) + if (exact ? matches(needle, haystack[i]) : matches_start(needle, haystack[i])) + return static_cast<int>(i); + return -1; +} + +// Forwarding overloads for different needle types +template <std::size_t N> +inline int search_block(const char *needle, const std::array<std::string_view, N> &haystack, bool exact) { + return search_block(std::string_view{needle}, haystack, exact); +} + +template <std::size_t N> +inline int search_block(char *needle, const std::array<std::string_view, N> &haystack, bool exact) { + return search_block(std::string_view{needle}, haystack, exact); +} + +inline int search_block(const char *needle, const char *const haystack[], bool exact) { + return search_block(std::string_view{needle}, haystack, exact); +} + +inline int search_block(char *needle, const char *const haystack[], bool exact) { + return search_block(std::string_view{needle}, haystack, exact); +} + +inline int search_block(char *needle, std::string_view haystack[], bool exact) { + return search_block(std::string_view{needle}, haystack, exact); +} + void command_interpreter(CharData *ch, char *argument); void list_similar_commands(CharData *ch, char *arg); -int searchblock(char *arg, const char **list, bool exact); -int search_block(const char *arg, const char **list, bool exact); -#define parse_direction(arg) (search_block(arg, dirs, false)) char lower(char c); char *one_argument(char *argument, char *first_arg); char *one_word(char *argument, char *first_arg); @@ -80,7 +154,7 @@ struct SortStruct { /* necessary for CMD_IS macro */ extern const CommandInfo cmd_info[]; -extern const char *command_flags[]; + extern int num_of_cmds; extern SortStruct *cmd_sort_info; diff --git a/src/ispell.cpp b/src/ispell.cpp index 8b646a11..ddd0e8ac 100644 --- a/src/ispell.cpp +++ b/src/ispell.cpp @@ -44,8 +44,14 @@ void ispell_init(void) { } #endif - pipe(fiery_to_ispell); - pipe(ispell_to_fiery); + if (pipe(fiery_to_ispell) == -1) { + log("Error creating pipe: fiery_to_ispell"); + return; + } + if (pipe(ispell_to_fiery) == -1) { + log("Error creating pipe: ispell_to_fiery"); + return; + } ispell_pid = fork(); @@ -80,10 +86,6 @@ void ispell_init(void) { ispell_in = fdopen(ispell_to_fiery[0], "r"); setbuf(ispell_in, nullptr); - -#if !defined(sun) /* that ispell on sun gives no (c) msg */ - fgets(ignore_buf, 1024, ispell_in); -#endif } } @@ -109,9 +111,15 @@ const char *get_ispell_line(const char *word) { fflush(ispell_out); } - fgets(buf, ISPELL_BUF_SIZE, ispell_in); - if (*buf && *buf != '\n') - fgets(throwaway, ISPELL_BUF_SIZE, ispell_in); + if (fgets(buf, ISPELL_BUF_SIZE, ispell_in) == nullptr) { + log("Error reading from ispell_in"); + return nullptr; + } + if (*buf && *buf != '\n') { + if (fgets(throwaway, ISPELL_BUF_SIZE, ispell_in) == nullptr) { + log("Error reading from ispell_in"); + } + } return buf; } diff --git a/src/magic.cpp b/src/magic.cpp index 881b3a3f..baa765df 100644 --- a/src/magic.cpp +++ b/src/magic.cpp @@ -317,7 +317,7 @@ void effect_update(void) { world[(int)reff->room].light++; if (ROOM_EFF_FLAGGED(reff->room, ROOM_EFF_ILLUMINATION)) world[(int)reff->room].light--; - REMOVE_FLAG(world[(int)reff->room].room_effects, reff->effect); + REMOVE_FLAG(world[(int)reff->room].effects, reff->effect); REMOVE_FROM_LIST(reff, room_effect_list, next); free(reff); } diff --git a/src/mail.cpp b/src/mail.cpp index a2087e07..974841be 100644 --- a/src/mail.cpp +++ b/src/mail.cpp @@ -112,7 +112,11 @@ void read_from_file(void *buf, int size, long filepos) { return; } fseek(mail_file, filepos, SEEK_SET); - fread(buf, size, 1, mail_file); + if (fread(buf, size, 1, mail_file) != 1) { + log("SYSERR: Failed to read from mail file."); + no_mail = 1; + return; + } fclose(mail_file); return; } diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 00000000..ace244b1 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,167 @@ +/*************************************************************************** + * File: main.cpp Part of FieryMUD * + * Usage: Main entry point for FieryMUD * + * * + * All rights reserved. See license.doc for complete information. * + * * + * FieryMUD Copyright (C) 1998, 1999, 2000 by the Fiery Consortium * + * FieryMUD is based on CircleMUD Copyright (C) 1993, 94 by the Trustees * + * of the Johns Hopkins University * + * CircleMUD is based on DikuMUD, Copyright (C) 1990, 1991. * + ***************************************************************************/ + +#include "comm.hpp" +#include "conf.hpp" +#include "constants.hpp" +#include "db.hpp" +#include "defines.hpp" +#include "logging.hpp" +#include "version.hpp" + +#include <cxxopts.hpp> +#include <fmt/format.h> +#include <cstdlib> +#include <cstring> +#include <unistd.h> + +// External variables from comm.cpp +extern ush_int port; +extern int scheck; +extern int should_restrict; +extern int restrict_reason; +extern int no_specials; +extern int environment; + +// Default values from conf.hpp +extern const char *DFLT_DIR; +extern const char *DFLT_ENV; +extern int DFLT_PORT; + +// Function declarations from other modules +void init_flagvectors(); +void init_races(); +void init_classes(); +void init_objtypes(); +void init_exp_table(); +void boot_world(); +void init_game(int port); +void destroy_db(); + +int main(int argc, char **argv) { + try { + cxxopts::Options options("fierymud", "FieryMUD - A Multi-User Dungeon Server"); + + options.add_options() + ("d,directory", "Data directory", cxxopts::value<std::string>()->default_value(DFLT_DIR)) + ("e,environment", "Environment (test, dev, prod)", cxxopts::value<std::string>()->default_value(DFLT_ENV)) + ("p,port", "Port number", cxxopts::value<int>()->default_value(std::to_string(DFLT_PORT))) + ("c,check", "Syntax check mode") + ("q,quick", "Quick boot mode") + ("r,restrict", "Restrict game - no new players") + ("s,suppress", "Suppress special routines") + ("h,help", "Show help message") + ("v,version", "Show version information"); + + // Parse positional argument for port + options.parse_positional({"port"}); + options.positional_help("[port]"); + + auto result = options.parse(argc, argv); + + if (result.count("help")) { + fmt::print("{}\n", options.help()); + return 0; + } + + if (result.count("version")) { + fmt::print("FieryMUD Git Hash: {}\n", get_git_hash()); + return 0; + } + + // Extract values + std::string dir = result["directory"].as<std::string>(); + std::string env = result["environment"].as<std::string>(); + port = result["port"].as<int>(); + + + // Handle flags + scheck = result.count("check") ? 1 : 0; + should_restrict = result.count("restrict") ? 1 : 0; + no_specials = result.count("suppress") ? 1 : 0; + + if (should_restrict) { + restrict_reason = 1; // RESTRICT_ARGUMENT + } + + // Validate port + if (port <= 1024) { + fprintf(stderr, "Error: Port number must be greater than 1024.\n"); + return 1; + } + + // Change to data directory + if (chdir(dir.c_str()) < 0) { + perror("Fatal error changing to data directory"); + return 1; + } + log("Using {} as data directory.", dir); + + // Set environment + if (env == "test") { + environment = ENV_TEST; + log("Running in test mode."); + } else if (env == "dev") { + environment = ENV_DEV; + log("Running in dev mode."); + } else if (env == "prod") { + environment = ENV_PROD; + log("Running in production mode."); + } else { + log("Unknown environment '{}'; valid choices are 'test', 'dev', and 'prod'.", env); + return 1; + } + + // Log mode information + if (scheck) { + log("Syntax check mode enabled."); + } + if (result.count("quick")) { + log("Quick boot mode."); + } + if (should_restrict) { + log("Restricting game -- no new players allowed."); + } + if (no_specials) { + log("Suppressing assignment of special routines."); + } + + // Initialize game constants + log("Initializing runtime game constants."); + init_flagvectors(); + init_races(); + init_classes(); + init_objtypes(); + init_exp_table(); + + // Start the game or check syntax + if (scheck) { + boot_world(); + } else { + log("Running game on port {}.", port); + init_game(port); + } + + // Cleanup + log("Clearing game world."); + destroy_db(); + + return 0; + + } catch (const cxxopts::exceptions::exception& e) { + fmt::print(stderr, "Error parsing options: {}\n", e.what()); + return 1; + } catch (const std::exception& e) { + fmt::print(stderr, "Error: {}\n", e.what()); + return 1; + } +} \ No newline at end of file diff --git a/src/medit.cpp b/src/medit.cpp index c675fe57..89092fd4 100644 --- a/src/medit.cpp +++ b/src/medit.cpp @@ -12,6 +12,7 @@ * CircleMUD is based on DikuMUD, Copyright (C) 1990, 1991. * ***************************************************************************/ +#include "bitflags.hpp" #include "casting.hpp" #include "chars.hpp" #include "charsize.hpp" @@ -620,8 +621,8 @@ void medit_disp_stances(DescriptorData *d) { #if defined(CLEAR_SCREEN) char_printf(d->character, ""); #endif - for (i = 0; *stance_types[i] != '\n'; i++) { - sprintf(buf, "%s%2d%s) %s\n", grn, i, nrm, stance_types[i]); + for (i = 0; stance_types[i].front() != '\n'; i++) { + sprintf(buf, "%s%2d%s) %s\n", grn, i, nrm, stance_types[i].data()); char_printf(d->character, buf); } char_printf(d->character, "Enter stance number:\n"); @@ -637,8 +638,8 @@ void medit_disp_positions(DescriptorData *d) { #if defined(CLEAR_SCREEN) char_printf(d->character, ""); #endif - for (i = 0; *position_types[i] != '\n'; i++) { - sprintf(buf, "%s%2d%s) %s\n", grn, i, nrm, position_types[i]); + for (i = 0; position_types[i].front() != '\n'; i++) { + sprintf(buf, "%s%2d%s) %s\n", grn, i, nrm, position_types[i].data()); char_printf(d->character, buf); } char_printf(d->character, "Enter position number:\n"); @@ -659,8 +660,7 @@ void medit_disp_sex(DescriptorData *d) { char_printf(d->character, ""); #endif for (i = 0; i < NUM_SEXES; i++) { - sprintf(buf, "%s%2d%s) %s\n", grn, i, nrm, genders[i]); - char_printf(d->character, buf); + char_printf(d->character, "{}{:2d}{}) {}\n", grn, i, nrm, genders[i]); } char_printf(d->character, "Enter gender number:\n"); } @@ -679,8 +679,7 @@ void medit_size(DescriptorData *d) { char_printf(d->character, ""); #endif for (i = 0; i < NUM_SIZES; i++) { - sprintf(buf, "%s%2d%s) %c%s\n", grn, i, nrm, (sizes[i].name)[0], sizes[i].name + 1); - char_printf(d->character, buf); + char_printf(d->character, "{}{:2d}{}) {}{}\n", grn, i, nrm, (sizes[i].name)[0], sizes[i].name + 1); } char_printf(d->character, "Enter size number:\n"); } @@ -696,8 +695,7 @@ void medit_disp_attack_types(DescriptorData *d) { char_printf(d->character, ""); #endif for (i = 0; i < NUM_ATTACK_TYPES; i++) { - sprintf(buf, "%s%2d%s) %s\n", grn, i, nrm, attack_hit_text[i].singular); - char_printf(d->character, buf); + char_printf(d->character, "{}{:2d}{}) {}\n", grn, i, nrm, attack_hit_text[i].singular); } char_printf(d->character, "Enter attack type:\n"); } @@ -717,18 +715,18 @@ void medit_disp_mob_flags(DescriptorData *d) { /* Outer loop goes through rows, inner loop goes through columns. */ for (i = 0; i <= NUM_MOB_FLAGS / columns; ++i) { - *buf = '\0'; + std::string output; for (j = 0; j < columns; ++j) { if (FLAG_INDEX >= NUM_MOB_FLAGS) break; - sprintf(buf, "%s%s%2d%s) %-20.20s", buf, grn, FLAG_INDEX + 1, nrm, action_bits[FLAG_INDEX]); + output += fmt::format("{}{:2d}{}) {:<20.20}", grn, FLAG_INDEX + 1, nrm, action_bits[FLAG_INDEX]); } - char_printf(d->character, strcat(buf, "\n")); + output += "\n"; + char_printf(d->character, output); } - sprintflag(buf1, MOB_FLAGS(OLC_MOB(d)), NUM_MOB_FLAGS, action_bits); - sprintf(buf, "\nCurrent flags : %s%s%s\nEnter mob flags (0 to quit) : ", cyn, buf1, nrm); - char_printf(d->character, buf); + char_printf(d->character, "\nCurrent flags : {}{}{}\nEnter mob flags (0 to quit) : ", cyn, + sprintflag(MOB_FLAGS(OLC_MOB(d)), NUM_MOB_FLAGS, action_bits), nrm); } #undef FLAG_INDEX @@ -773,18 +771,18 @@ void medit_disp_aff_flags(DescriptorData *d) { char_printf(d->character, ""); #endif for (i = 0; i <= NUM_EFF_FLAGS / columns; ++i) { - *buf = '\0'; + std::string output; for (j = 0; j < columns; ++j) { if (FLAG_INDEX >= NUM_EFF_FLAGS) break; - sprintf(buf, "%s%s%2d%s) %-20.20s", buf, grn, FLAG_INDEX + 1, nrm, effect_flags[FLAG_INDEX]); + output += fmt::format("{}{:2d}{}) {:<20.20}", grn, FLAG_INDEX + 1, nrm, effect_flags[FLAG_INDEX]); } - char_printf(d->character, strcat(buf, "\n")); + output += "\n"; + char_printf(d->character, output); } - sprintflag(buf1, EFF_FLAGS(OLC_MOB(d)), NUM_EFF_FLAGS, effect_flags); - sprintf(buf, "\nCurrent flags : %s%s%s\nEnter aff flags (0 to quit):\n", cyn, buf1, nrm); - char_printf(d->character, buf); + char_printf(d->character, "\nCurrent flags : {}{}{}\nEnter aff flags (0 to quit):\n", cyn, + sprintflag(EFF_FLAGS(OLC_MOB(d)), NUM_EFF_FLAGS, effect_flags), nrm); } #undef FLAG_INDEX @@ -799,8 +797,8 @@ void medit_disp_lifeforces(DescriptorData *d) { char_printf(d->character, ""); #endif for (i = 0; i < NUM_LIFEFORCES; i++) { - sprintf(buf, "%s%2d%s) %s%s%s\n", grn, i, nrm, lifeforces[i].color, capitalize(lifeforces[i].name), nrm); - char_printf(d->character, buf); + char_printf(d->character, "{}{:2d}{}) {}{}{}\n", grn, i, nrm, lifeforces[i].color, + capitalize(lifeforces[i].name), nrm); } char_printf(d->character, "Enter life force number:\n"); } @@ -872,9 +870,6 @@ void medit_disp_menu(DescriptorData *d) { GET_PERCEPTION(mob), GET_HIDDENNESS(mob)); char_printf(d->character, menu.c_str()); - sprintflag(buf1, MOB_FLAGS(mob), NUM_MOB_FLAGS, action_bits); - sprintflag(buf2, EFF_FLAGS(mob), NUM_EFF_FLAGS, effect_flags); - menu = ""; menu += fmt::format("&2&bN&0) Life Force : {}{}&0\n", LIFEFORCE_COLOR(mob), capitalize(LIFEFORCE_NAME(mob))); menu += fmt::format("&2&bO&0) Composition : {}{}&0\n", COMPOSITION_COLOR(mob), capitalize(COMPOSITION_NAME(mob))); @@ -882,8 +877,8 @@ void medit_disp_menu(DescriptorData *d) { menu += fmt::format("&2&bR&0) Load Position : &6{}&0\n", position_types[(int)GET_POS(mob)]); menu += fmt::format("&2&bT&0) Default Pos : &6{}&0\n", position_types[(int)GET_DEFAULT_POS(mob)]); menu += fmt::format("&2&bU&0) Attack Type : &6{}&0\n", attack_hit_text[GET_ATTACK(mob)].singular); - menu += fmt::format("&2&bV&0) Act Flags : &6{}&0\n", buf1); - menu += fmt::format("&2&bW&0) Aff Flags : &6{}&0\n", buf2); + menu += fmt::format("&2&bV&0) Act Flags : &6{}&0\n", sprintflag(MOB_FLAGS(mob), NUM_MOB_FLAGS, action_bits)); + menu += fmt::format("&2&bW&0) Aff Flags : &6{}&0\n", sprintflag(EFF_FLAGS(mob), NUM_EFF_FLAGS, effect_flags)); menu += fmt::format("&2&bS&0) Script : &6{}&0\n", mob->proto_script ? "&6&bSet&0" : "&6Not Set&0"); menu += "&2&bQ&0) Quit\nEnter choice:\n"; char_printf(d->character, menu.c_str()); diff --git a/src/messages.cpp b/src/messages.cpp index 6bd058af..1ac7998f 100644 --- a/src/messages.cpp +++ b/src/messages.cpp @@ -1,77 +1 @@ #include "messages.hpp" - -const char *portal_entry_messages[] = { - "&b$p &0&bflares white as $n enters it and disappears.&0\n", - "&b$p &0&bflares as $n enters it and disappears.&0\n", - "&b$p &0&bvibrates violently as $n enters it and then stops.&0\n", - "\n", -}; - -const char *portal_character_messages[] = { - "", - "&bYou feel your body being ripped apart!&0\n", - "&b$p &0&bvibrates violently as you enter.&0\n", - "&bYour molecules are ripped apart as you enter $p.&0\n", - "&bYou appear in a completely different location!&0\n", - "&9&bYou feel your energy being drained!&0\n", - "&bYour molecules are ripped apart as you enter $p.&0\n\n" - "&bYou catch a glimpse of a giant white leopard!&0\n\n" - "&9&bYou feel your energy being drained!&0\n", - "\n", -}; - -const char *portal_exit_messages[] = { - "$p flares white as $n emerges from it.\n", - "$p flares as $n emerges from it.\n", - "$n appears from nowhere!\n", - "There is a loud POP sound as $n emerges from $p.\n", - "\n", -}; - -/* "okay" etc. */ -const char *OK = "Okay.\n"; -const char *HUH = "Huh?!?\n"; -const char *NOPERSON = "There is no one by that name here.\n"; -const char *NOEFFECT = "Nothing seems to happen.\n"; - -const char *subclass_descrip = - "\n" - " &1&bF&0&3&bi&0&1&be&0&3&br&0&1&by&0&3&bMUD Class " - "System&0\n" - "&1&bF&0&3&bi&0&1&be&0&3&br&0&1&by&0&7&bMUD &0has many various and complex " - "classes for you to play. All players\n" - "start as one of four base classes. Each of the base classes\n" - "possess unknown growth potential throughout gameplay. As you explore \n" - "the realm and advance your experience you may learn of ways to\n" - "specialize your skills and spells into a new subclass.&0\n\n"; - -const char *subclass_descrip2 = - "If you manage to learn of these ways your new class will possess new\n" - "and different skills and spells, thus altering your power within the\n" - "realm. There is no multiclassing in &1&bF&0&3&bi&0&1&be&0&3&br&0&1&by&0&7&bMUD&0. A character may only\n" - "ever be one class or subclass.&0\n"; - - -const char *race_descrip = - "\n" - " &1&bF&0&3&bi&0&1&be&0&3&br&0&1&by&0&3&bMUD Race System&0\n" - "&1&bF&0&3&bi&0&1&be&0&3&br&0&1&by&0&7&bMUD &0has many playable races " - "to choose from which offer a variety\n" - "of playstyles. Each race has unique powers, strengths, weaknesses,\n" - "and ability score limits. Your choice of race and your character's\n" - "alignment will determine what classes and subclasses are available to\n" - "them. No one race can be all classes or subclasses. Class/subclass\n" - "restrictions, innate abilities, and rough ability score capacities\n" - "for each race can be found in the race's help file.\n\n"; - -const char *race_descrip2 = - "Races are divided into two groups: the 'good' races and the 'evil'\n" - "races. However, individual character alignment is fluid and any race\n" - "can become any alignment. Creatures through the world may interact\n" - "with you differently depending on your character alignment and your\n" - "racial group. Players may choose from any of the 'good' races to\n" - "start from. Players may ask a god to create a character from an\n" - "'evil' race once they have demonstrated sufficient knowledge of the\n" - "realm. The 'evil' races are not necessarily stronger or more\n" - "complicated to play than the 'good' races, but the restriction is\n" - "often helpful for new players.\n\n"; \ No newline at end of file diff --git a/src/messages.hpp b/src/messages.hpp index 5c3df31c..c73db5d5 100644 --- a/src/messages.hpp +++ b/src/messages.hpp @@ -1,13 +1,73 @@ #pragma once +#include <array> +#include <string_view> -extern const char *portal_entry_messages[]; -extern const char *portal_character_messages[]; -extern const char *portal_exit_messages[]; -extern const char *OK; -extern const char *HUH; -extern const char *NOPERSON; -extern const char *NOEFFECT; -extern const char *subclass_descrip; -extern const char *subclass_descrip2; -extern const char *race_descrip; -extern const char *race_descrip2; \ No newline at end of file +constexpr std::string_view portal_entry_messages[] = { + "&b$p &0&bflares white as $n enters it and disappears.&0\n", "&b$p &0&bflares as $n enters it and disappears.&0\n", + "&b$p &0&bvibrates violently as $n enters it and then stops.&0\n", "\n"}; + +constexpr std::string_view portal_character_messages[] = {"", + "&bYou feel your body being ripped apart!&0\n", + "&b$p &0&bvibrates violently as you enter.&0\n", + "&bYour molecules are ripped apart as you enter $p.&0\n", + "&bYou appear in a completely different location!&0\n", + "&9&bYou feel your energy being drained!&0\n", + "&bYour molecules are ripped apart as you enter $p.&0\n\n" + "&bYou catch a glimpse of a giant white leopard!&0\n\n" + "&9&bYou feel your energy being drained!&0\n", + "\n" + +}; + +constexpr std::string_view portal_exit_messages[] = {"$p flares white as $n emerges from it.\n", + "$p flares as $n emerges from it.\n", "$n appears from nowhere!\n", + "There is a loud POP sound as $n emerges from $p.\n", "\n" + +}; + +/* "okay" etc. */ +constexpr std::string_view OK = "Okay.\n"; +constexpr std::string_view HUH = "Huh?!?\n"; +constexpr std::string_view NOPERSON = "There is no one by that name here.\n"; +constexpr std::string_view NOEFFECT = "Nothing seems to happen.\n"; + +constexpr std::string_view subclass_descrip = + "\n" + " &1&bF&0&3&bi&0&1&be&0&3&br&0&1&by&0&3&bMUD Class " + "System&0\n" + "&1&bF&0&3&bi&0&1&be&0&3&br&0&1&by&0&7&bMUD &0has many various and complex " + "classes for you to play. All players\n" + "start as one of four base classes. Each of the base classes\n" + "possess unknown growth potential throughout gameplay. As you explore \n" + "the realm and advance your experience you may learn of ways to\n" + "specialize your skills and spells into a new subclass.&0\n\n"; + +constexpr std::string_view subclass_descrip2 = + "If you manage to learn of these ways your new class will possess new\n" + "and different skills and spells, thus altering your power within the\n" + "realm. There is no multiclassing in &1&bF&0&3&bi&0&1&be&0&3&br&0&1&by&0&7&bMUD&0. A character may only\n" + "ever be one class or subclass.&0\n"; + +constexpr std::string_view race_descrip = + "\n" + " &1&bF&0&3&bi&0&1&be&0&3&br&0&1&by&0&3&bMUD Race System&0\n" + "&1&bF&0&3&bi&0&1&be&0&3&br&0&1&by&0&7&bMUD &0has many playable races " + "to choose from which offer a variety\n" + "of playstyles. Each race has unique powers, strengths, weaknesses,\n" + "and ability score limits. Your choice of race and your character's\n" + "alignment will determine what classes and subclasses are available to\n" + "them. No one race can be all classes or subclasses. Class/subclass\n" + "restrictions, innate abilities, and rough ability score capacities\n" + "for each race can be found in the race's help file.\n\n"; + +constexpr std::string_view race_descrip2 = + "Races are divided into two groups: the 'good' races and the 'evil'\n" + "races. However, individual character alignment is fluid and any race\n" + "can become any alignment. Creatures through the world may interact\n" + "with you differently depending on your character alignment and your\n" + "racial group. Players may choose from any of the 'good' races to\n" + "start from. Players may ask a god to create a character from an\n" + "'evil' race once they have demonstrated sufficient knowledge of the\n" + "realm. The 'evil' races are not necessarily stronger or more\n" + "complicated to play than the 'good' races, but the restriction is\n" + "often helpful for new players.\n\n"; \ No newline at end of file diff --git a/src/modify.cpp b/src/modify.cpp index 0f1260d1..f74c7792 100644 --- a/src/modify.cpp +++ b/src/modify.cpp @@ -300,9 +300,9 @@ void parse_action(int command, char *string, DescriptorData *d) { *s = temp; } else strcat(buf, t); - /* - * This is kind of annoying...but some people like it. - */ + /* + * This is kind of annoying...but some people like it. + */ #if 0 sprintf(buf, "%s\n%d line%sshown.\n", buf, total_len, ((total_len != 1) ? "s " : " ")); #endif @@ -891,16 +891,16 @@ void get_paging_input(DescriptorData *d, char *input) { one_argument(input, buf); /* Q is for quit. :) */ - if (LOWER(*buf) == 'q') { + if (to_lower(*buf) == 'q') { d->page_outbuf->clear(); return; } /* R is for refresh */ - else if (LOWER(*buf) == 'r') + else if (to_lower(*buf) == 'r') // Do nothing, just refresh the page ; /* B is for back */ - else if (LOWER(*buf) == 'b') + else if (to_lower(*buf) == 'b') d->paging_curpage--; /* A digit: goto a page */ diff --git a/src/money.cpp b/src/money.cpp index adf14cf2..7efe7f3f 100644 --- a/src/money.cpp +++ b/src/money.cpp @@ -19,6 +19,7 @@ #include "math.hpp" #include "objects.hpp" #include "screen.hpp" +#include "string_utils.hpp" #include "structs.hpp" #include "sysdep.hpp" #include "utils.hpp" @@ -35,79 +36,50 @@ CoinDef coindefs[NUM_COIN_TYPES] = {{"platinum", "plat", "p", "&6&b", "p", 1000} {"silver", nullptr, "s", "&7&b", "s", 10}, {"copper", nullptr, "c", "&3", "c", 1}}; -bool is_coin_name(const char *name, int cointype) { - if (!strcasecmp(name, COIN_NAME(cointype))) +int parse_coin_type(std::string_view name) noexcept { + for (int i = 0; i < NUM_COIN_TYPES; i++) { + if (is_coin_name(name, i)) { + return i; + } + } + return -1; +} + +bool is_coin_name(std::string_view name, int cointype) { + if (matches(name, COIN_NAME(cointype))) return true; - if (COIN_SHORTNAME(cointype) && !strcasecmp(name, COIN_SHORTNAME(cointype))) + if (COIN_SHORTNAME(cointype) && matches(name, COIN_SHORTNAME(cointype))) return true; return false; } -#define APPENDCOIN(coin) sprintf(buf, "%s%s%d" ANRM " %s", buf, COIN_COLOR(coin), coins[coin], COIN_NAME(coin)) - -void statemoney(char *buf, const int coins[]) { - int ctypes = 0, ctype2 = 0, amount = 0, i; - - *buf = '\0'; - - for (i = 0; i < 4; i++) { +std::string statemoney(const Money coins) noexcept { + if (coins[PLATINUM] == 0 && coins[GOLD] == 0 && coins[SILVER] == 0 && coins[COPPER] == 0) { + return "Nothing"; + } + std::vector<std::string> coin_strings; + bool final_plural = false; + for (int i = 0; i < NUM_COIN_TYPES; i++) { if (coins[i]) { - amount += coins[i]; - ctypes++; - if (ctypes == 2) - ctype2 = i; + coin_strings.push_back(fmt::format("{}{} {}", COIN_COLOR(i), coins[i], COIN_NAME(i))); + if (coins[i] > 1) { + final_plural = true; + } } } - - if (coins[PLATINUM]) { - APPENDCOIN(PLATINUM); - if (ctypes == 2) - strcat(buf, " and "); - else if (ctypes > 2) - strcat(buf, ", "); - } - if (coins[GOLD]) { - APPENDCOIN(GOLD); - if (ctypes == 2 && ctype2 != GOLD) - strcat(buf, " and "); - else if (ctypes == 4 || (ctypes == 3 && ctype2 != GOLD)) - strcat(buf, ", "); - else if (ctypes == 3) - strcat(buf, ", and "); - } - if (coins[SILVER]) { - APPENDCOIN(SILVER); - if (ctypes == 2 && ctype2 != SILVER) - strcat(buf, " and "); - else if (ctypes == 4 || (ctypes == 3 && ctype2 == SILVER)) - strcat(buf, ", and "); - } - if (coins[COPPER]) - APPENDCOIN(COPPER); - if (amount == 0) - strcpy(buf, "0 coins"); - else if (amount > 1) - strcat(buf, " coins"); - else - strcat(buf, " coin"); + return join_strings(coin_strings, ", ", ", and ") + fmt::format(" coin{}", final_plural ? "s" : ""); } -#undef APPENDCOIN - -#define COINBRIEF(coin, amount) sprintf(coinbuf, "%s%s%d%s" ANRM, coinbuf, COIN_COLOR(coin), amount, COIN_INITIAL(coin)) - -/* Prints a string about some money, in the requested number of spaces, - * using at most two types of coin. */ -void briefmoney(char *buf, int spaces, int amt) { +/* Prints a string about some money, in the requested number of spaces, using at most two types of coin. */ +std::string briefmoney(int spaces, int amt) noexcept { bool topset = false; bool lowset = false; int toptype = 0, topval = 0, topchars = 0, lowtype = 0, lowval = 0, lowchars = 0, padding = 0; int i, maxval = 9; int coins[4]; - char coinbuf[100]; + std::string coinbuf; - /* Limit the number of digits so that there's 1 spot left over for a - * coin-type designator (p, g, s, or c) */ + /* Limit the number of digits so that there's 1 spot left over for a coin-type designator (p, g, s, or c) */ for (i = 0; i < spaces - 1; i++) { maxval = maxval * 10 + 9; } @@ -123,135 +95,118 @@ void briefmoney(char *buf, int spaces, int amt) { topset = true; toptype = i; topval = coins[i]; - topchars = sprintf(buf, "%d", topval); /* count digits */ + topchars = fmt::formatted_size("{}", topval); /* count digits */ } else if (!lowset) { lowset = true; lowtype = i; lowval = coins[i]; - lowchars = sprintf(buf, "%d", lowval); /* count digits */ + lowchars = fmt::formatted_size("{}", lowval); /* count digits */ } } } - /* If the top coin type and low coin type can't fit within the requested - * space, only the top will be used */ + /* If the top coin type and low coin type can't fit within the requested space, only the top will be used */ if (lowset && lowchars + topchars > spaces - 2) lowset = false; - *buf = '\0'; - if (topset) { - *coinbuf = '\0'; + coinbuf.clear(); padding = spaces - 1 - topchars; - COINBRIEF(toptype, topval > maxval ? maxval : topval); + coinbuf += fmt::format("{}{}{}", COIN_COLOR(toptype), std::min(topval, maxval), COIN_INITIAL(toptype)); if (lowset) { padding -= lowchars + 1; - COINBRIEF(lowtype, lowval); + coinbuf += fmt::format("{}{}{}", COIN_COLOR(lowtype), lowval, COIN_INITIAL(lowtype)); } - sprintf(buf, "%*s%s", padding, "", coinbuf); + return fmt::format("{:>{}}{}", "", padding, coinbuf); } else { - sprintf(buf, "%*s0", spaces - 1, ""); + return fmt::format("{:>{}}0", "", spaces - 1); } } -#undef COINBRIEF - -bool parse_money(char **money, int coins[]) { - char arg[MAX_INPUT_LENGTH]; +std::optional<Money> parse_money(std::string_view input) noexcept { int amount, type; - char *last; - bool found_coins = false; + Money coins; coins[PLATINUM] = 0; coins[GOLD] = 0; coins[SILVER] = 0; coins[COPPER] = 0; - skip_spaces(money); + input = trim(input); + while (!input.empty()) { + // Get the next argument + auto arg = getline(input, ' '); + + // If the argument is a number, then we know it's the amount of coins and the next argument is the type + if (is_integer(arg)) { + amount = svtoi(arg); + arg = getline(input, ' '); + } else { + // If we're here, it's most likely a combination of amount and type + // We need to split the string into the amount and type + size_t pos = 0; + while (pos < arg.size() && isdigit(arg[pos])) { + ++pos; + } + if (pos == 0) { + return std::nullopt; + } + amount = svtoi(arg.substr(0, pos)); + arg.remove_prefix(pos); + } - while (**money) { - *money = any_one_arg(last = *money, arg); - if (!*arg) - break; - else if (!is_number(arg)) { - *money = last; - break; /* Not a number! */ + if (amount <= 0) { + log("SYSERR: parse_money: Attempt to create {} money.", amount); + return std::nullopt; } - amount = atoi(arg); - *money = any_one_arg(*money, arg); - if (!*arg || (type = parse_obj_name(nullptr, arg, nullptr, NUM_COIN_TYPES, coindefs, sizeof(CoinDef))) < 0) { - *money = last; - break; + + // Find the type of coin + for (type = 0; type < NUM_COIN_TYPES; ++type) { + if (is_coin_name(arg, type)) { + break; + } } coins[type] += amount; - found_coins = true; } - return found_coins; + return coins; } -void money_desc(int amount, char **shortdesc, char **keywords) { - static char sdbuf[128], kwbuf[128]; - - if (amount <= 0) { - log("SYSERR: Try to create negative or 0 money."); - strcpy(sdbuf, "an erroneous object"); - strcpy(kwbuf, "erroneous object"); - } +std::string money_desc(int amount) { if (amount == 1) { - strcpy(sdbuf, "a single coin"); - strcpy(kwbuf, "single coin"); + return "single coin"; } else if (amount <= 9) { - strcpy(sdbuf, "a tiny pile of coins"); - strcpy(kwbuf, "tiny pile coins"); + return "tiny pile coins"; } else if (amount <= 20) { - strcpy(sdbuf, "a handful of coins"); - strcpy(kwbuf, "handful coins"); + return "handful coins"; } else if (amount <= 75) { - strcpy(sdbuf, "a little pile of coins"); - strcpy(kwbuf, "little pile coins"); + return "little pile coins"; } else if (amount <= 200) { - strcpy(sdbuf, "a small pile of coins"); - strcpy(kwbuf, "small pile coins"); + return "small pile coins"; } else if (amount <= 1000) { - strcpy(sdbuf, "a pile of coins"); - strcpy(kwbuf, "pile coins"); + return "pile coins"; } else if (amount <= 5000) { - strcpy(sdbuf, "a big pile of coins"); - strcpy(kwbuf, "big pile coins"); + return "big pile coins"; } else if (amount <= 10000) { - strcpy(sdbuf, "a large heap of coins"); - strcpy(kwbuf, "large heap coins"); + return "large heap coins"; } else if (amount <= 20000) { - strcpy(sdbuf, "a huge mound of coins"); - strcpy(kwbuf, "huge mound coins"); + return "huge mound coins"; } else if (amount <= 75000) { - strcpy(sdbuf, "an enormous mound of coins"); - strcpy(kwbuf, "enormous mound coins"); + return "enormous mound coins"; } else if (amount <= 150000) { - strcpy(sdbuf, "a small mountain of coins"); - strcpy(kwbuf, "small mountain coins"); + return "small mountain coins"; } else if (amount <= 250000) { - strcpy(sdbuf, "a mountain of coins"); - strcpy(kwbuf, "mountain coins"); + return "mountain coins"; } else if (amount <= 500000) { - strcpy(sdbuf, "a huge mountain of coins"); - strcpy(kwbuf, "huge mountain coins"); + return "huge mountain coins"; } else if (amount <= 1000000) { - strcpy(sdbuf, "an enormous mountain of coins"); - strcpy(kwbuf, "enormous mountain coins"); + return "enormous mountain coins"; } else { - strcpy(sdbuf, "an absolutely colossal mountain of coins"); - strcpy(kwbuf, "colossal mountain coins"); + return "colossal mountain coins"; } - - if (shortdesc) - *shortdesc = sdbuf; - if (keywords) - *keywords = kwbuf; } -ObjData *create_money(const int coins[]) { +ObjData *create_money(const Money coins) { ObjData *obj; int amount = coins[PLATINUM] + coins[GOLD] + coins[SILVER] + coins[COPPER]; int which; @@ -289,34 +244,52 @@ ObjData *create_money(const int coins[]) { which = SILVER; else if (coins[COPPER]) which = COPPER; - obj->name = strdup(fmt::format("{} coin", COIN_NAME(which)).c_str()); - obj->short_description = strdup(fmt::format("a {}", obj->name).c_str()); - obj->description = strdup(fmt::format("A single {} is lying here.", obj->name).c_str()); + strcpy(obj->name, fmt::format("{} coin", COIN_NAME(which)).c_str()); + strcpy(obj->short_description, fmt::format("a {}", obj->name).c_str()); + strcpy(obj->description, fmt::format("A single {} is lying here.", obj->name).c_str()); obj->ex_description->keyword = strdup(obj->name); - obj->ex_description->description = strdup(fmt::format("A shiny {}!", obj->name).c_str()); + strcpy(obj->ex_description->description, fmt::format("A shiny {}!", obj->name).c_str()); } else { - money_desc(amount, &obj->short_description, &obj->name); - obj->name = strdup(obj->name); - obj->short_description = strdup(obj->short_description); - obj->ex_description->keyword = strdup(obj->name); - obj->description = strdup(fmt::format("{} is lying here.", obj->short_description).c_str()); - cap_by_color(obj->description); + auto guess = [](int amount, int scale) -> int { + return ((amount / scale) + random_number(0, amount / scale)) * scale; + }; + obj->name = strdup(money_desc(amount).c_str()); + obj->short_description = strdup(fmt::format("a {}", obj->name).c_str()); + obj->ex_description->keyword = obj->name; + obj->description = strdup(fmt::format("{} is lying here.", capitalize_first(obj->short_description)).c_str()); if (amount < 10) obj->ex_description->description = strdup(fmt::format("There are {} coins.", amount).c_str()); else if (amount < 100) obj->ex_description->description = - strdup(fmt::format("There are about {} coins.", (amount / 10) * 10).c_str()); + strdup(fmt::format("There are about {} coins.", guess(amount, 10)).c_str()); else if (amount < 1000) obj->ex_description->description = - strdup(fmt::format("It looks to be about {} coins.", (amount / 100) * 100).c_str()); + strdup(fmt::format("It looks to be about {} coins.", guess(amount, 100)).c_str()); else if (amount < 100000) obj->ex_description->description = - strdup(fmt::format("You guess there are maybe {} coins.", - ((amount / 1000) + random_number(0, amount / 1000)) * 1000) - .c_str()); + strdup(fmt::format("You guess there are maybe {} coins.", guess(amount, 1000)).c_str()); else obj->ex_description->description = strdup("There are a LOT of coins."); } return obj; } + +bool charge_char(CharData *ch, int amount) noexcept { + if (amount <= 0) { + log("SYSERR: charge_char: Attempt to charge {:d} money.", amount); + return false; + } + + if (GET_CASH(ch) < amount) { + return false; + } + + for (int i = 0; i < NUM_COIN_TYPES; ++i) { + while (amount >= COIN_SCALE(i) && GET_COINS(ch)[i] > 0) { + amount -= COIN_SCALE(i); + GET_COINS(ch)[i]--; + } + } + return true; +} \ No newline at end of file diff --git a/src/money.hpp b/src/money.hpp index 55a14779..1632ea33 100644 --- a/src/money.hpp +++ b/src/money.hpp @@ -15,6 +15,168 @@ #include "structs.hpp" #include "sysdep.hpp" +#include <nlohmann/json.hpp> + +using json = nlohmann::json; + +#define PLATINUM_SCALE 1000 +#define GOLD_SCALE 100 +#define SILVER_SCALE 10 +#define COPPER_SCALE 1 + +constexpr int platinum_scale = 1000; +constexpr int gold_scale = 100; +constexpr int silver_scale = 10; +constexpr int copper_scale = 1; + +class Money { + public: + Money() = default; + Money(int platinum, int gold, int silver, int copper) + : platinum_(platinum), gold_(gold), silver_(silver), copper_(copper) {} + Money(int *money) + : platinum_(money[PLATINUM]), gold_(money[GOLD]), silver_(money[SILVER]), copper_(money[COPPER]) {} + Money(const json &j) noexcept { + platinum_ = j.value("platinum", 0); + gold_ = j.value("gold", 0); + silver_ = j.value("silver", 0); + copper_ = j.value("copper", 0); + } + + // Return the total value of the money in copper + [[nodiscard]] int value() const noexcept { + return platinum_ * platinum_scale + gold_ * gold_scale + silver_ * silver_scale + copper_ * copper_scale; + } + [[nodiscard]] bool is_zero() const noexcept { return platinum_ == 0 && gold_ == 0 && silver_ == 0 && copper_ == 0; } + + [[nodiscard]] int platinum() const noexcept { return platinum_; } + [[nodiscard]] int gold() const noexcept { return gold_; } + [[nodiscard]] int silver() const noexcept { return silver_; } + [[nodiscard]] int copper() const noexcept { return copper_; } + + [[nodiscard]] bool can_afford(const Money &cost) const noexcept { return value() >= cost.value(); } + [[nodiscard]] bool can_afford(int cost) const noexcept { return value() >= cost; } + + // Charge them for the cost, starting with the highest coin type + [[nodiscard]] bool charge(int cost) noexcept { + if (cost <= 0 || value() < cost) { + return false; + } + + int coin_scales[] = {platinum_scale, gold_scale, silver_scale, copper_scale}; + + int remaining = cost; + for (int i = 0; i < NUM_COIN_TYPES; ++i) { + while (remaining >= coin_scales[i] && (*this)[i] > 0) { + remaining -= coin_scales[i]; + (*this)[i]--; + } + } + return true; + } + [[nodiscard]] bool charge(const Money &cost) noexcept { return charge(cost.value()); } + + // Operators for arithmetic + Money operator+(const Money &other) const noexcept { + return Money(platinum_ + other.platinum_, gold_ + other.gold_, silver_ + other.silver_, + copper_ + other.copper_); + } + Money operator+=(const Money &other) noexcept { + platinum_ += other.platinum_; + gold_ += other.gold_; + silver_ += other.silver_; + copper_ += other.copper_; + return *this; + } + Money operator-(const Money &other) const noexcept { + return Money(platinum_ - other.platinum_, gold_ - other.gold_, silver_ - other.silver_, + copper_ - other.copper_); + } + Money operator-=(const Money &other) noexcept { + platinum_ -= other.platinum_; + gold_ -= other.gold_; + silver_ -= other.silver_; + copper_ -= other.copper_; + return *this; + } + Money operator*(int multiplier) const noexcept { + return Money(platinum_ * multiplier, gold_ * multiplier, silver_ * multiplier, copper_ * multiplier); + } + Money operator*=(int multiplier) noexcept { + platinum_ *= multiplier; + gold_ *= multiplier; + silver_ *= multiplier; + copper_ *= multiplier; + return *this; + } + Money operator/(int divisor) const noexcept { + return Money(platinum_ / divisor, gold_ / divisor, silver_ / divisor, copper_ / divisor); + } + Money operator/=(int divisor) noexcept { + platinum_ /= divisor; + gold_ /= divisor; + silver_ /= divisor; + copper_ /= divisor; + return *this; + } + + // Get operator by index + int operator[](int index) const noexcept { + switch (index) { + case PLATINUM: + return platinum_; + case GOLD: + return gold_; + case SILVER: + return silver_; + case COPPER: + return copper_; + default: + return 0; + } + } + // Set operator by index + int &operator[](int index) noexcept { + switch (index) { + case PLATINUM: + return platinum_; + case GOLD: + return gold_; + case SILVER: + return silver_; + case COPPER: + return copper_; + default: + break; + } + static int dummy = 0; + return dummy; // Return a dummy reference if index is invalid + } + // Comparison operators + bool operator==(const Money &other) const noexcept { + return platinum_ == other.platinum_ && gold_ == other.gold_ && silver_ == other.silver_ && + copper_ == other.copper_; + } + bool operator!=(const Money &other) const noexcept { return !(*this == other); } + bool operator<(const Money &other) const noexcept { + return platinum_ < other.platinum_ && gold_ < other.gold_ && silver_ < other.silver_ && copper_ < other.copper_; + } + bool operator<=(const Money &other) const noexcept { return *this < other || *this == other; } + bool operator>(const Money &other) const noexcept { return !(*this <= other); } + bool operator>=(const Money &other) const noexcept { return !(*this < other); } + + // Json serialization + [[nodiscard]] nlohmann::json to_json() const noexcept { + return nlohmann::json{{"platinum", platinum_}, {"gold", gold_}, {"silver", silver_}, {"copper", copper_}}; + } + + private: + int platinum_{0}; + int gold_{0}; + int silver_{0}; + int copper_{0}; +}; + struct CoinDef { const char *name; const char *shortname; @@ -29,9 +191,9 @@ extern CoinDef coindefs[NUM_COIN_TYPES]; #define VALID_COIN(coin) (coin >= 0 && coin < NUM_COIN_TYPES) #define COIN_NAME(coin) (VALID_COIN(coin) ? coindefs[coin].name : "coin") #define COIN_SHORTNAME(coin) (VALID_COIN(coin) ? coindefs[coin].shortname : "c") -bool is_coin_name(const char *name, int cointype); #define COIN_COLOR(coin) (VALID_COIN(coin) ? coindefs[coin].color : "&9&b") #define COIN_INITIAL(coin) (VALID_COIN(coin) ? coindefs[coin].initial : "?") +#define COIN_SCALE(coin) (VALID_COIN(coin) ? coindefs[coin].scale : 1) /* Old below */ @@ -48,8 +210,12 @@ bool is_coin_name(const char *name, int cointype); ((coins)[PLATINUM] * PLATINUM_SCALE + (coins)[GOLD] * GOLD_SCALE + (coins)[SILVER] * SILVER_SCALE + \ (coins)[COPPER] * COPPER_SCALE) -void statemoney(char *buf, const int coins[]); -bool parse_money(char **money, int coins[]); -void briefmoney(char *buf, int spaces, int amt); -void money_desc(int amount, const char **shortdesc, const char **keywords); -ObjData *create_money(const int coins[]); +[[nodiscard]] bool is_coin_name(std::string_view name, int cointype); +[[nodiscard]] std::string statemoney(const Money coins) noexcept; +[[nodiscard]] std::optional<Money> parse_money(std::string_view input) noexcept; +[[nodiscard]] std::string briefmoney(int spaces, int amt) noexcept; +[[nodiscard]] ObjData *create_money(const Money coins); +[[nodiscard]] int parse_coin_type(std::string_view name) noexcept; + +// Charge a character an amount of money, returning true if successful. +[[nodiscard]] bool charge_char(CharData *ch, int amount) noexcept; diff --git a/src/movement.cpp b/src/movement.cpp index 9384a701..ff8f2071 100644 --- a/src/movement.cpp +++ b/src/movement.cpp @@ -266,12 +266,13 @@ void falling_yell(CharData *ch) { continue; /* No yell - you'll receive "<person> falls screaming from above" */ else { - sprintf(buf2, "the %s", dirs[backdir]); + sprintf(buf2, "the %s", dirs[backdir].data()); dirstr = buf2; } - sprintf(buf, "You hear a %s %s from %s, which quickly fades.", random_number(0, 10) < 5 ? "surprised" : "sudden", - random_number(0, 10) < 6 ? "shriek" : "yelp", dirstr); + sprintf(buf, "You hear a %s %s from %s, which quickly fades.", + random_number(0, 10) < 5 ? "surprised" : "sudden", random_number(0, 10) < 6 ? "shriek" : "yelp", + dirstr); ch->in_room = EXIT_NDEST(world[was_in].exits[dir]); act(buf, false, ch, 0, 0, TO_ROOM); @@ -467,8 +468,8 @@ void disband_group(CharData *master, bool verbose, bool forceful) { g->groupee->group_master = nullptr; master->groupees = g->next; if (verbose) - act(forceful ? "&2The group has been disbanded.&0" : "&2$n &2has disbanded the group.&0", false, - master, 0, g->groupee, TO_VICT); + act(forceful ? "&2The group has been disbanded.&0" : "&2$n &2has disbanded the group.&0", false, master, 0, + g->groupee, TO_VICT); free(g); } } @@ -534,8 +535,7 @@ void ungroup(CharData *ch, bool verbose, bool forceful) { continue; } if (verbose) - act(forceful ? "&2$n &2has been kicked out of your group!&0" - : "&2$n &2has left your group!&0", + act(forceful ? "&2$n &2has been kicked out of your group!&0" : "&2$n &2has left your group!&0", true, ch, 0, g->next->groupee, TO_VICT); g = g->next; } @@ -544,10 +544,10 @@ void ungroup(CharData *ch, bool verbose, bool forceful) { ch->group_master = nullptr; if (verbose) { - act(forceful ? "&2You have been kicked out of your group.&0" : "&2You have left your group!&0", - false, ch, 0, 0, TO_CHAR); - act(forceful ? "&2You have kicked $n &2out of your group.&0" : "&2$n &2has left your group!&0", - false, ch, 0, master, TO_VICT); + act(forceful ? "&2You have been kicked out of your group.&0" : "&2You have left your group!&0", false, + ch, 0, 0, TO_CHAR); + act(forceful ? "&2You have kicked $n &2out of your group.&0" : "&2$n &2has left your group!&0", false, + ch, 0, master, TO_VICT); } } } else diff --git a/src/objects.cpp b/src/objects.cpp index fd1b41dc..95f9ef99 100644 --- a/src/objects.cpp +++ b/src/objects.cpp @@ -79,15 +79,15 @@ static int max_value(ObjData *obj, int val) { case ITEM_PORTAL: switch (val) { case VAL_PORTAL_ENTRY_MSG: - for (max = 0; *portal_entry_messages[max] != '\n'; ++max) + for (max = 0; portal_entry_messages[max].front() != '\n'; ++max) ; break; case VAL_PORTAL_CHAR_MSG: - for (max = 0; *portal_character_messages[max] != '\n'; ++max) + for (max = 0; portal_character_messages[max].front() != '\n'; ++max) ; break; case VAL_PORTAL_EXIT_MSG: - for (max = 0; *portal_exit_messages[max] != '\n'; ++max) + for (max = 0; portal_exit_messages[max].front() != '\n'; ++max) ; break; } diff --git a/src/oedit.cpp b/src/oedit.cpp index f794ce19..d6c811f2 100644 --- a/src/oedit.cpp +++ b/src/oedit.cpp @@ -13,6 +13,7 @@ * CircleMUD is based on DikuMUD, Copyright (C) 1990, 1991. * ***************************************************************************/ +#include "bitflags.hpp" #include "board.hpp" #include "casting.hpp" #include "comm.hpp" @@ -564,19 +565,18 @@ void oedit_disp_wall_block_dirs(DescriptorData *d) { */ void oedit_disp_container_flags_menu(DescriptorData *d) { get_char_cols(d->character); - sprintbit(GET_OBJ_VAL(OLC_OBJ(d), VAL_CONTAINER_BITS), container_bits, buf1); #if defined(CLEAR_SCREEN) char_printf(d->character, ""); #endif - sprintf(buf, - "%s1%s) CLOSEABLE\n" - "%s2%s) PICKPROOF\n" - "%s3%s) CLOSED\n" - "%s4%s) LOCKED\n" - "Container flags: %s%s%s\n" - "Enter flag, 0 to quit:\n", - grn, nrm, grn, nrm, grn, nrm, grn, nrm, cyn, buf1, nrm); - char_printf(d->character, buf); + char_printf(d->character, + "%s1%s) CLOSEABLE\n" + "%s2%s) PICKPROOF\n" + "%s3%s) CLOSED\n" + "%s4%s) LOCKED\n" + "Container flags: %s%s%s\n" + "Enter flag, 0 to quit:\n", + grn, nrm, grn, nrm, grn, nrm, grn, nrm, cyn, + sprintbit(GET_OBJ_VAL(OLC_OBJ(d), VAL_CONTAINER_BITS), container_bits), nrm); } /* @@ -635,22 +635,20 @@ void oedit_disp_aff_flags(DescriptorData *d) { #endif for (i = 0; i <= NUM_EFF_FLAGS / columns; ++i) { - *buf = '\0'; + std::string output; for (j = 0; j < columns; ++j) { if (FLAG_INDEX >= NUM_EFF_FLAGS) break; - sprintf(buf, "%s%s%2d%s) %-20.20s", buf, grn, FLAG_INDEX + 1, nrm, effect_flags[FLAG_INDEX]); + output += fmt::format("{}{:2d}{}) {:20.20s}", grn, FLAG_INDEX + 1, nrm, effect_flags[FLAG_INDEX]); } - char_printf(d->character, strcat(buf, "\n")); + output += "\n"; + char_printf(d->character, output); } - *buf1 = '\0'; - sprintflag(buf1, GET_OBJ_EFF_FLAGS(OLC_OBJ(d)), NUM_EFF_FLAGS, effect_flags); - sprintf(buf, - "\nSpell flags: %s%s%s\n" - "Enter spell flag, 0 to quit : ", - cyn, buf1, nrm); - char_printf(d->character, buf); + char_printf(d->character, + "\nSpell flags: {}{}{}\n" + "Enter spell flag, 0 to quit : ", + cyn, sprintflag(GET_OBJ_EFF_FLAGS(OLC_OBJ(d)), NUM_EFF_FLAGS, effect_flags), nrm); } #undef FLAG_INDEX @@ -704,11 +702,12 @@ void oedit_disp_apply_menu(DescriptorData *d) { #endif for (i = 0; i <= NUM_APPLY_TYPES / columns; ++i) { - *buf = '\0'; + std::string output; for (j = 0; j < columns; ++j) if (TYPE_INDEX < NUM_APPLY_TYPES) - sprintf(buf, "%s%s%2d%s) %-20.20s ", buf, grn, TYPE_INDEX, nrm, apply_types[TYPE_INDEX]); - char_printf(d->character, strcat(buf, "\n")); + output += fmt::format("{}{:2d}{}) {:20.20s} ", grn, TYPE_INDEX + 1, nrm, apply_types[TYPE_INDEX]); + output += "\n"; + char_printf(d->character, output); } char_printf(d->character, "\nEnter apply type (0 is no apply):\n"); @@ -731,12 +730,13 @@ void oedit_disp_weapon_menu(DescriptorData *d) { #endif for (i = 0; i <= NUM_ATTACK_TYPES / columns; ++i) { - *buf = '\0'; + std::string output; for (j = 0; j < columns; ++j) if (TYPE_INDEX < NUM_ATTACK_TYPES) - sprintf(buf, "%s%s%2d%s) %-20.20s ", buf, grn, TYPE_INDEX + 1, nrm, - attack_hit_text[TYPE_INDEX].singular); - char_printf(d->character, strcat(buf, "\n")); + output += fmt::format("{}{:2d}{}) {:20.20s} ", grn, TYPE_INDEX + 1, nrm, + attack_hit_text[TYPE_INDEX].singular); + output += "\n"; + char_printf(d->character, output); } char_printf(d->character, "\nEnter weapon type:\n"); @@ -757,21 +757,18 @@ void oedit_disp_spells_menu(DescriptorData *d) { /* Fixed to use all spells --gurlaek 7/22/1999 */ for (counter = 0; counter <= MAX_SPELLS; counter++) { if (strcasecmp(skills[counter].name, "!UNUSED!")) { - sprintf(buf, "%s%2d%s) %s%-20.20s %s", grn, counter, nrm, yel, skills[counter].name, - !(++columns % 3) ? "\n" : ""); - char_printf(d->character, buf); + char_printf(d->character, "{}{:2d}{}) {}{:20.20s} {}", grn, counter, nrm, yel, skills[counter].name, + !(++columns % 3) ? "\n" : ""); } } - sprintf(buf, "\n%sEnter spell choice (0 for none):\n", nrm); - char_printf(d->character, buf); + char_printf(d->character, "\n{}Enter spell choice (0 for none):\n", nrm); } -void oedit_disp_portal_messages_menu(DescriptorData *d, const char *messages[]) { +void oedit_disp_portal_messages_menu(DescriptorData *d, const std::string_view messages[]) { int i = 0; - while (*messages[i] != '\n') { - sprintf(buf, "%s%d%s) %s", grn, i, nrm, messages[i]); - char_printf(d->character, buf); + while (messages[i].front() != '\n') { + char_printf(d->character, "{}{}{}) {}", grn, i, nrm, messages[i]); ++i; } } @@ -1011,19 +1008,18 @@ void oedit_disp_extra_menu(DescriptorData *d) { #endif for (i = 0; i <= NUM_ITEM_FLAGS / columns; ++i) { - *buf = '\0'; + std::string output; for (j = 0; j < columns; ++j) if (FLAG_INDEX < NUM_ITEM_FLAGS) - sprintf(buf, "%s%s%2d%s) %-20.20s ", buf, grn, FLAG_INDEX + 1, nrm, extra_bits[FLAG_INDEX]); - char_printf(d->character, strcat(buf, "\n")); + output += fmt::format("{}{:2d}{}) {:20.20s} ", grn, FLAG_INDEX + 1, nrm, extra_bits[FLAG_INDEX]); + output += "\n"; + char_printf(d->character, output); } - sprintflag(buf1, GET_OBJ_FLAGS(OLC_OBJ(d)), NUM_ITEM_FLAGS, extra_bits); - sprintf(buf, - "\nObject flags: %s%s%s\n" - "Enter object extra flag (0 to quit):\n", - cyn, buf1, nrm); - char_printf(d->character, buf); + char_printf(d->character, + "\nObject flags: {}{}{}\n" + "Enter object extra flag (0 to quit):\n", + cyn, sprintflag(GET_OBJ_FLAGS(OLC_OBJ(d)), NUM_ITEM_FLAGS, extra_bits), nrm); } #undef FLAG_INDEX @@ -1041,19 +1037,18 @@ void oedit_disp_wear_menu(DescriptorData *d) { char_printf(d->character, ""); #endif for (i = 0; i <= NUM_ITEM_WEAR_FLAGS / columns; ++i) { - *buf = '\0'; + std::string output; for (j = 0; j < columns; ++j) if (FLAG_INDEX < NUM_ITEM_WEAR_FLAGS) - sprintf(buf, "%s%s%2d%s) %-20.20s ", buf, grn, FLAG_INDEX + 1, nrm, wear_bits[FLAG_INDEX]); - char_printf(d->character, strcat(buf, "\n")); + output += fmt::format("{}{:2d}{}) {:20.20s} ", grn, FLAG_INDEX + 1, nrm, wear_bits[FLAG_INDEX]); + output += "\n"; + char_printf(d->character, output); } - sprintbit(GET_OBJ_WEAR(OLC_OBJ(d)), wear_bits, buf1); - sprintf(buf, - "\nWear flags: %s%s%s\n" - "Enter wear flag, 0 to quit:\n", - cyn, buf1, nrm); - char_printf(d->character, buf); + char_printf(d->character, + "\nWear flags: {}{}{}\n" + "Enter wear flag, 0 to quit:\n", + cyn, sprintbit(GET_OBJ_WEAR(OLC_OBJ(d)), wear_bits), nrm); } #undef FLAG_INDEX @@ -1136,31 +1131,32 @@ void oedit_disp_obj_values(DescriptorData *d) { GET_OBJ_VAL(obj, VAL_MONEY_GOLD), GET_OBJ_VAL(obj, VAL_MONEY_SILVER), GET_OBJ_VAL(obj, VAL_MONEY_COPPER), nrm); break; - case ITEM_PORTAL: - i = real_room(GET_OBJ_VAL(obj, VAL_PORTAL_DESTINATION)); - sprinttype(GET_OBJ_VAL(obj, VAL_PORTAL_ENTRY_MSG), portal_entry_messages, buf1); - sprinttype(GET_OBJ_VAL(obj, VAL_PORTAL_CHAR_MSG), portal_character_messages, buf2); - sprinttype(GET_OBJ_VAL(obj, VAL_PORTAL_EXIT_MSG), portal_exit_messages, arg); - sprintf(buf, - " Target Room : %s%s (%d)%s\n" - " Entry Message : %s%s%s" - " Char Message : %s%s%s" - " Exit Message : %s%s%s\n", - cyn, i == NOWHERE ? "Invalid Room" : world[i].name, GET_OBJ_VAL(obj, VAL_PORTAL_DESTINATION), nrm, cyn, - buf1, nrm, cyn, buf2, nrm, cyn, arg, nrm); - break; + case ITEM_PORTAL: { + int room = real_room(GET_OBJ_VAL(obj, VAL_PORTAL_DESTINATION)); + + auto buffer = fmt::format( + " Target Room : {}{} ({}){}\n" + " Entry Message : {}{}{}" + " Char Message : {}{}{}" + " Exit Message : {}{}{}\n", + cyn, room == NOWHERE ? "Invalid Room" : world[room].name, GET_OBJ_VAL(obj, VAL_PORTAL_DESTINATION), nrm, + cyn, sprinttype(GET_OBJ_VAL(obj, VAL_PORTAL_ENTRY_MSG), portal_entry_messages), nrm, cyn, + sprinttype(GET_OBJ_VAL(obj, VAL_PORTAL_CHAR_MSG), portal_character_messages), nrm, cyn, + sprinttype(GET_OBJ_VAL(obj, VAL_PORTAL_EXIT_MSG), portal_exit_messages), nrm); + strcpy(buf, buffer.c_str()); + } break; case ITEM_SPELLBOOK: sprintf(buf, " Pages : %s%d%s\n", cyn, GET_OBJ_VAL(obj, VAL_SPELLBOOK_PAGES), nrm); break; - case ITEM_WALL: - sprinttype(GET_OBJ_VAL(obj, VAL_WALL_DIRECTION), dirs, buf1); - sprintf(buf, - " Direction : %s%s%s\n" - " Dispelable : %s%s%s\n" - " Hitpoints : %s%d%s\n", - cyn, buf1, nrm, cyn, YESNO(GET_OBJ_VAL(obj, VAL_WALL_DISPELABLE)), nrm, cyn, - GET_OBJ_VAL(obj, VAL_WALL_HITPOINTS), nrm); - break; + case ITEM_WALL: { + auto output = fmt::format( + " Direction : {}{}{}\n" + " Dispelable : {}{}{}\n" + " Hitpoints : {}{}{}\n", + cyn, sprinttype(GET_OBJ_VAL(obj, VAL_WALL_DIRECTION), dirs), nrm, cyn, + YESNO(GET_OBJ_VAL(obj, VAL_WALL_DISPELABLE)), nrm, cyn, GET_OBJ_VAL(obj, VAL_WALL_HITPOINTS), nrm); + strcpy(buf, output.c_str()); + } break; case ITEM_BOARD: sprintf(buf, " Board Title : %s%s%s\n", cyn, board(GET_OBJ_VAL(obj, VAL_BOARD_NUMBER))->title, nrm); break; @@ -1179,64 +1175,58 @@ void oedit_disp_menu(DescriptorData *d) { obj = OLC_OBJ(d); get_char_cols(d->character); - /*. Build buffers for first part of menu . */ - sprintflag(buf2, GET_OBJ_FLAGS(obj), NUM_ITEM_FLAGS, extra_bits); - /* * Build first half of menu. */ - sprintf(buf, + char_printf(d->character, #if defined(CLEAR_SCREEN) - ".[H.[J" + ".[H.[J" #endif - "-- Item: '&5%s&0' vnum: [&2%5d&0]\n" - "%s1%s) Namelist : %s%s\n" - "%s2%s) S-Desc : %s%s\n" - "%s3%s) L-Desc :-\n%s%s\n" - "%s4%s) A-Desc :-\n%s%s" - "%s5%s) Type : %s%s\n" - "%s6%s) Extra flags : %s%s\n", - (obj->short_description && *obj->short_description) ? obj->short_description : "undefined", OLC_NUM(d), grn, - nrm, yel, (obj->name && *obj->name) ? obj->name : "undefined", grn, nrm, yel, - (obj->short_description && *obj->short_description) ? obj->short_description : "undefined", grn, nrm, yel, - (obj->description && *obj->description) ? obj->description : "undefined", grn, nrm, yel, - (obj->action_description && *obj->action_description) ? obj->action_description : "<not set>\n", grn, nrm, - cyn, OBJ_TYPE_NAME(obj), grn, nrm, cyn, buf2); + "-- Item: '&5{}&0' vnum: [&2{:5d}&0]\n" + "{}1{}) Namelist : {}{}\n" + "{}2{}) S-Desc : {}{}\n" + "{}3{}) L-Desc :-\n{}{}\n" + "{}4{}) A-Desc :-\n{}{}" + "{}5{}) Type : {}{}\n" + "{}6{}) Extra flags : {}{}\n", + (obj->short_description && *obj->short_description) ? obj->short_description : "undefined", OLC_NUM(d), + grn, nrm, yel, (obj->name && *obj->name) ? obj->name : "undefined", grn, nrm, yel, + (obj->short_description && *obj->short_description) ? obj->short_description : "undefined", grn, nrm, + yel, (obj->description && *obj->description) ? obj->description : "undefined", grn, nrm, yel, + (obj->action_description && *obj->action_description) ? obj->action_description : "<not set>\n", grn, + nrm, cyn, OBJ_TYPE_NAME(obj), grn, nrm, cyn, + sprintflag(GET_OBJ_FLAGS(obj), NUM_ITEM_FLAGS, extra_bits)); /* * Send first half. */ - char_printf(d->character, buf); /*. Build second half of menu . */ - sprintbit(GET_OBJ_WEAR(obj), wear_bits, buf1); - sprintf(buf, - "%s7%s) Wear flags : %s%s\n" - "%s8%s) Weight : %s%.2f\n" - "%s9%s) Cost : %s%d\n" - "%sA%s) Timer : %s%d\n" - "%sB%s) Level : %s%d\n" - "%sC%s) Hiddenness : %s%ld\n" - "%sD%s) Values : %s%d %d %d %d %d %d %d%s\n", - grn, nrm, cyn, buf1, grn, nrm, cyn, GET_OBJ_WEIGHT(obj), grn, nrm, cyn, GET_OBJ_COST(obj), grn, nrm, cyn, - GET_OBJ_TIMER(obj), grn, nrm, cyn, GET_OBJ_LEVEL(obj), grn, nrm, cyn, GET_OBJ_HIDDENNESS(obj), grn, nrm, - cyn, GET_OBJ_VAL(obj, 0), GET_OBJ_VAL(obj, 1), GET_OBJ_VAL(obj, 2), GET_OBJ_VAL(obj, 3), - GET_OBJ_VAL(obj, 4), GET_OBJ_VAL(obj, 5), GET_OBJ_VAL(obj, 6), nrm); - char_printf(d->character, buf); + char_printf(d->character, + "{}7{}) Wear flags : {}{}\n" + "{}8{}) Weight : {}{:.2f}\n" + "{}9{}) Cost : {}{}\n" + "{}A{}) Timer : {}{}\n" + "{}B{}) Level : {}{}\n" + "{}C{}) Hiddenness : {}{}\n" + "{}D{}) Values : {}{} {} {} {} {} {} {}{}\n", + grn, nrm, cyn, sprintbit(GET_OBJ_WEAR(obj), wear_bits), grn, nrm, cyn, GET_OBJ_WEIGHT(obj), grn, nrm, + cyn, GET_OBJ_COST(obj), grn, nrm, cyn, GET_OBJ_TIMER(obj), grn, nrm, cyn, GET_OBJ_LEVEL(obj), grn, nrm, + cyn, GET_OBJ_HIDDENNESS(obj), grn, nrm, cyn, GET_OBJ_VAL(obj, 0), GET_OBJ_VAL(obj, 1), + GET_OBJ_VAL(obj, 2), GET_OBJ_VAL(obj, 3), GET_OBJ_VAL(obj, 4), GET_OBJ_VAL(obj, 5), GET_OBJ_VAL(obj, 6), + nrm); oedit_disp_obj_values(d); - *buf1 = '\0'; - sprintflag(buf1, GET_OBJ_EFF_FLAGS(obj), NUM_EFF_FLAGS, effect_flags); - sprintf(buf, - "%sE%s) Applies menu\n" - "%sF%s) Extra descriptions menu\n" - "%sG%s) Spell applies : &6%s&0\n" - "%sS%s) Script : %s%s\n" - "%sQ%s) Quit\n" - "Enter choice:\n", - grn, nrm, grn, nrm, grn, nrm, buf1, grn, nrm, cyn, obj->proto_script ? "Set." : "Not Set.", grn, nrm); - char_printf(d->character, buf); + char_printf(d->character, + "{}E{}) Applies menu\n" + "{}F{}) Extra descriptions menu\n" + "{}G{}) Spell applies : &6{}&0\n" + "{}S{}) Script : {}{}\n" + "{}Q{}) Quit\n" + "Enter choice:\n", + grn, nrm, grn, nrm, grn, nrm, sprintflag(GET_OBJ_EFF_FLAGS(obj), NUM_EFF_FLAGS, effect_flags), grn, nrm, + cyn, obj->proto_script ? "Set." : "Not Set.", grn, nrm); OLC_MODE(d) = OEDIT_MAIN_MENU; } diff --git a/src/pfiles.cpp b/src/pfiles.cpp index 602837d0..7d373d63 100644 --- a/src/pfiles.cpp +++ b/src/pfiles.cpp @@ -150,9 +150,8 @@ bool write_objects(ObjData *obj, FILE *fl, int location) { if (obj) { /* - * Traverse the list in reverse order so when they are loaded - * and placed back on the char using obj_to_char, they will be - * in the correct order. + * Traverse the list in reverse order so when they are loaded and placed back on the char using obj_to_char, + * they will be in the correct order. */ write_objects(obj->next_content, fl, location); @@ -278,7 +277,7 @@ void show_rent(CharData *ch, char *argument) { if (!fl) return; - name[0] = UPPER(name[0]); + name[0] = to_upper(name[0]); if (!get_line(fl, buf)) { char_printf(ch, "Error reading reading rent code.\n"); @@ -311,8 +310,8 @@ void show_rent(CharData *ch, char *argument) { act("\n$N is wearing:", false, ch, 0, tch, TO_CHAR); for (i = 0; i < NUM_WEARS; ++i) if (GET_EQ(tch, wear_order_index[i])) - list_objects(GET_EQ(tch, wear_order_index[i]), ch, strlen(where[wear_order_index[i]]), 0, - where[wear_order_index[i]]); + list_objects(GET_EQ(tch, wear_order_index[i]), ch, strlen(where[wear_order_index[i]].data()), 0, + where[wear_order_index[i]].data()); } if (tch->carrying) { @@ -665,7 +664,7 @@ bool build_object(FILE *fl, ObjData **objp, int *location) { num = atoi(line); f = atof(line); - switch (UPPER(*tag)) { + switch (to_upper(*tag)) { case 'A': if (!strcasecmp(tag, "adesc")) obj->action_description = fread_string(fl, "build_object"); @@ -1026,7 +1025,10 @@ void load_quests(CharData *ch) { last_var = nullptr; while (n < qnum_vars) { - fscanf(fl, "%s %s\n", var_name, var_val); + if (fscanf(fl, "%s %s\n", var_name, var_val) != 2) { + log("SYSERR: Failed to read quest variable for player {}", GET_NAME(ch)); + break; + } if (last_var == nullptr) { CREATE(curr->variables, QuestVariableList, 1); @@ -1220,14 +1222,23 @@ static bool load_binary_objects(CharData *ch) { return false; if (!feof(fl)) - fread(&rent, sizeof(rent_info), 1, fl); + if (fread(&rent, sizeof(rent_info), 1, fl) != 1) { + log("SYSERR: Failed to read rent info from file."); + fclose(fl); + return true; + } for (j = 0; j < MAX_CONTAINER_DEPTH; j++) cont_row[j] = nullptr; /* empty all cont lists (you never know ...) */ while (!feof(fl)) { eq = 0; - fread(&object, sizeof(obj_file_elem), 1, fl); + if (fread(&object, sizeof(obj_file_elem), 1, fl) != 1) { + if (!feof(fl)) { + log("SYSERR: Failed to read object data from file."); + } + break; + } if (ferror(fl)) { perror("Reading object file: load_binary_objects()"); fclose(fl); @@ -1531,11 +1542,22 @@ bool convert_player_obj_file(char *player_name, CharData *ch) { return false; } - fread(&rent, sizeof(rent_info), 1, fl); + if (fread(&rent, sizeof(rent_info), 1, fl) != 1) { + log("SYSERR: Failed to read rent info from file."); + fclose(fl); + return false; + } write_rent_code(fnew, rent.rentcode); while (!feof(fl)) { - fread(&object, sizeof(obj_file_elem), 1, fl); + if (fread(&object, sizeof(obj_file_elem), 1, fl) != 1) { + if (!feof(fl)) { + log("SYSERR: Failed to read object data from file."); + } + fclose(fl); + fclose(fnew); + return false; + } if (ferror(fl)) { perror("Reading player object file: convert_player_obj_file()"); fclose(fl); @@ -1628,7 +1650,6 @@ void save_player(CharData *ch) { GET_LOADROOM(ch) = GET_HOMEROOM(ch); break; case QUIT_TIMEOUT: - case QUIT_HOTBOOT: case QUIT_PURGE: case QUIT_AUTOSAVE: default: diff --git a/src/players.cpp b/src/players.cpp index 31e61dbf..bc42d1d6 100644 --- a/src/players.cpp +++ b/src/players.cpp @@ -10,6 +10,7 @@ #include "players.hpp" +#include "bitflags.hpp" #include "casting.hpp" #include "chars.hpp" #include "charsize.hpp" @@ -47,6 +48,7 @@ /* local functions */ static void load_effects(FILE *fl, CharData *ch); static void load_skills(FILE *fl, CharData *ch); +static void load_stored(FILE *fl, CharData *ch); static void load_spellcasts(FILE *fl, CharData *ch); static void scan_slash(const char *line, int *cur, int *max); static void write_aliases_ascii(FILE *file, CharData *ch); @@ -151,7 +153,7 @@ int get_pfilename(const char *name, char *filename, int mode) { return 0; } - sprintf(filename, "%s/%c/%c%s%s", prefix, UPPER(*name), UPPER(*name), name + 1, suffix); + sprintf(filename, "%s/%c/%c%s%s", prefix, to_upper(*name), to_upper(*name), name + 1, suffix); return 1; } @@ -226,8 +228,8 @@ int create_player_index_entry(char *name) { CREATE(player_table[pos].name, char, strlen(name) + 1); /* copy lowercase equivalent of name to table field, cap first char */ - *player_table[pos].name = UPPER(*name); - for (i = 1; (player_table[pos].name[i] = LOWER(name[i])); ++i) + *player_table[pos].name = to_upper(*name); + for (i = 1; (player_table[pos].name[i] = to_lower(name[i])); ++i) ; player_table[pos].name[i] = '\0'; @@ -251,9 +253,8 @@ void save_player_index(void) { for (i = 0; i <= top_of_p_table; i++) if (*player_table[i].name) { - sprintascii(bits, player_table[i].flags); fprintf(index_file, "%ld %s %d %s %ld\n", player_table[i].id, player_table[i].name, player_table[i].level, - *bits ? bits : "0", (long)player_table[i].last); + sprintascii(player_table[i].flags).c_str(), (long)player_table[i].last); } fprintf(index_file, "~\n"); @@ -334,6 +335,7 @@ int load_player(const char *name, CharData *ch) { if (!ch->player_specials) CREATE(ch->player_specials, PlayerSpecialData, 1); + ch->player_specials->stored = std::unordered_map<int, int>(); GET_PFILEPOS(ch) = id; @@ -408,14 +410,14 @@ int load_player(const char *name, CharData *ch) { GET_NATURAL_CON(ch) = num; else if (!strcasecmp(tag, "cash")) load_coins(line, GET_COINS(ch)); - else if (!strcasecmp(tag, "clan")) - load_clan(line, ch); else if (!strcasecmp(tag, "currenttitle")) GET_TITLE(ch) = strdup(line); else if (!strcasecmp(tag, "composition")) BASE_COMPOSITION(ch) = num; else if (!strcasecmp(tag, "cooldowns")) load_cooldowns(fl, ch); + else if (!strcasecmp(tag, "clan")) + ch->player_specials->clan_id = num; else goto bad_tag; break; @@ -595,7 +597,9 @@ int load_player(const char *name, CharData *ch) { load_spellcasts(fl, ch); else if (!strcasecmp(tag, "strength")) GET_NATURAL_STR(ch) = num; - else + else if (!strcasecmp(tag, "stored")) { + load_stored(fl, ch); + } else goto bad_tag; break; @@ -843,6 +847,8 @@ void save_player_char(CharData *ch) { fprintf(fl, "home: %d\n", GET_HOMEROOM(ch)); fprintf(fl, "lifeforce: %d\n", GET_LIFEFORCE(ch)); fprintf(fl, "composition: %d\n", BASE_COMPOSITION(ch)); + if (ch->player_specials->clan_id > 0) + fprintf(fl, "clan: %d\n", ch->player_specials->clan_id); fprintf(fl, "id: %ld\n", GET_IDNUM(ch)); fprintf(fl, "birthtime: %ld\n", (long)ch->player.time.birth); @@ -880,6 +886,11 @@ void save_player_char(CharData *ch) { fprintf(fl, " %d", GET_SAVE(ch, i)); fprintf(fl, "\n"); + fprintf(fl, "stored:\n"); + for (auto &it : ch->player_specials->stored) + fprintf(fl, "%d %d\n", it.first, it.second); + fprintf(fl, "0 0\n"); + if (GET_WIMP_LEV(ch)) fprintf(fl, "wimpy: %d\n", GET_WIMP_LEV(ch)); if (GET_FREEZE_LEV(ch)) @@ -938,8 +949,6 @@ void save_player_char(CharData *ch) { } if (GET_PAGE_LENGTH(ch) != DEFAULT_PAGE_LENGTH) fprintf(fl, "pagelength: %d\n", GET_PAGE_LENGTH(ch)); - if (GET_CLAN(ch)) - fprintf(fl, "clan: %d\n", GET_CLAN(ch)->number); if (GET_LOG_VIEW(ch)) fprintf(fl, "logview: %d\n", GET_LOG_VIEW(ch)); @@ -1121,8 +1130,7 @@ void write_ascii_flags(FILE *fl, flagvector flags[], int num_flags) { char flagbuf[FLAGBLOCK_SIZE + 1]; for (i = 0; i < FLAGVECTOR_SIZE(num_flags); ++i) { - sprintascii(flagbuf, flags[i]); - fprintf(fl, "%s%s", i ? " " : "", flagbuf); + fprintf(fl, "%s%s", i ? " " : "", sprintascii(flags[i]).c_str()); } } @@ -1150,6 +1158,18 @@ static void load_effects(FILE *fl, CharData *ch) { } while (num != 0); } +static void load_stored(FILE *fl, CharData *ch) { + int vnum = 0, amount = 0; + char line[MAX_INPUT_LENGTH + 1]; + + do { + get_line(fl, line); + sscanf(line, "%d %d", &vnum, &amount); + if (vnum > 0 && amount > 0) + GET_STORED(ch)[vnum] = amount; + } while (vnum > 0); +} + static void load_skills(FILE *fl, CharData *ch) { int skill = 0, proficiency = 0; char line[MAX_INPUT_LENGTH + 1]; @@ -1261,7 +1281,8 @@ void load_ascii_flags(flagvector flags[], int num_flags, char *line) { while (line && *line) { if (FLAGVECTOR_SIZE(num_flags) <= i) { if (*line != '0') { - log("SYSERR: load_ascii_flags: attempting to read in flags for block {:d}, but only {} blocks allowed " + log("SYSERR: load_ascii_flags: attempting to read in flags for block {:d}, but only {} blocks " + "allowed " "for flagvector type", i, FLAGVECTOR_SIZE(num_flags)); } @@ -1272,13 +1293,6 @@ void load_ascii_flags(flagvector flags[], int num_flags, char *line) { } } -static void load_clan(char *line, CharData *ch) { - Clan *clan = find_clan(line); - ch->player_specials->clan = find_clan_membership_in_clan(GET_NAME(ch), clan); - if (GET_CLAN_MEMBERSHIP(ch)) - GET_CLAN_MEMBERSHIP(ch)->player = ch; -} - void add_perm_title(CharData *ch, char *line) { int i; if (!GET_PERM_TITLES(ch)) { @@ -1304,8 +1318,11 @@ void init_player(CharData *ch) { int i; /* Make sure the character has a player structure */ - if (!ch->player_specials) + if (!ch->player_specials) { CREATE(ch->player_specials, PlayerSpecialData, 1); + ch->player_specials->stored = std::unordered_map<int, int>(); + ch->player_specials->clan_id = CLAN_ID_NONE; /* No clan initially */ + } init_retained_comms(ch); @@ -1318,7 +1335,7 @@ void init_player(CharData *ch) { } GET_TITLE(ch) = nullptr; - GET_PROMPT(ch) = strdup(default_prompts[DEFAULT_PROMPT][1]); + GET_PROMPT(ch) = strdup(default_prompts[DEFAULT_PROMPT][1].data()); GET_LDESC(ch) = nullptr; ch->player.description = nullptr; ch->player.time.birth = time(0); @@ -1419,7 +1436,7 @@ void send_save_description(CharData *ch, CharData *dest, bool entering) { room = GET_SAVEROOM(ch); if (real_room(room) != NOWHERE) { - sprintf(buf1, "%s (%d)", world[real_room(room)].name, room); + sprintf(buf1, "%s (%d)", world[real_room(room)].name.c_str(), room); } else { sprintf(buf1, "&1&bNOWHERE&0 (&5&b%d&0)", room); } diff --git a/src/players.hpp b/src/players.hpp index eb8da399..ab2f20bf 100644 --- a/src/players.hpp +++ b/src/players.hpp @@ -21,7 +21,6 @@ #define QUIT_RENT 1 /* yes no here */ #define QUIT_CRYO 2 /* yes no here */ #define QUIT_TIMEOUT 3 /* yes no home */ -#define QUIT_HOTBOOT 4 /* yes yes home */ #define QUIT_QUITMORT 5 /* no no temple */ #define QUIT_QUITIMM 6 /* yes no here */ #define QUIT_CAMP 7 /* yes no here */ diff --git a/src/prefs.cpp b/src/prefs.cpp index afbfc71e..c3a7bb72 100644 --- a/src/prefs.cpp +++ b/src/prefs.cpp @@ -168,75 +168,76 @@ ACMD(do_toggle) { for (; *fields[i].cmd != '\n'; ++i) if (is_abbrev(arg, fields[i].cmd)) if (GET_LEVEL(tch) >= fields[i].level || (i == SCMD_ANON && PRV_FLAGGED(tch, PRV_ANON_TOGGLE))) - if (i != SCMD_NOCLANCOMM || GET_CLAN(tch)) - break; + // if (i != SCMD_NOCLANCOMM || GET_CLAN(tch)) + // break; - if (!*arg || *fields[i].cmd == '\n') { - /* Show a player his/her fields. */ + if (!*arg || *fields[i].cmd == '\n') { + /* Show a player his/her fields. */ - if (*arg) { - if (GET_LEVEL(ch) < LVL_GOD || !(tch = find_char_around_char(ch, find_vis_by_name(ch, arg)))) { - char_printf(ch, "Toggle what!?\n"); - return; - } - /* Handle switched/shapechanged players. */ - tch = REAL_CHAR(tch); - } + if (*arg) { + if (GET_LEVEL(ch) < LVL_GOD || + !(tch = find_char_around_char(ch, find_vis_by_name(ch, arg)))) { + char_printf(ch, "Toggle what!?\n"); + return; + } + /* Handle switched/shapechanged players. */ + tch = REAL_CHAR(tch); + } - if (IS_NPC(tch)) { - act("$N is an NPC. They don't have toggles!", false, ch, 0, tch, TO_CHAR); - return; - } + if (IS_NPC(tch)) { + act("$N is an NPC. They don't have toggles!", false, ch, 0, tch, TO_CHAR); + return; + } - strcpy(buf, - " FieryMUD TOGGLES! (See HELP TOGGLE)\n" - "===============================================================\n"); - for (column = i = 0; *fields[i].cmd != '\n'; ++i) { - if (i != SCMD_ANON || !PRV_FLAGGED(tch, PRV_ANON_TOGGLE)) - if (fields[i].level > GET_LEVEL(tch)) - continue; - if (i == SCMD_NOCLANCOMM && !GET_CLAN(tch)) - continue; + strcpy(buf, + " FieryMUD TOGGLES! (See HELP TOGGLE)\n" + "===============================================================\n"); + for (column = i = 0; *fields[i].cmd != '\n'; ++i) { + if (i != SCMD_ANON || !PRV_FLAGGED(tch, PRV_ANON_TOGGLE)) + if (fields[i].level > GET_LEVEL(tch)) + continue; + // if (i == SCMD_NOCLANCOMM && !GET_CLAN(tch)) + // continue; - set = false; - switch (i) { - case SCMD_WIMPY: - if ((set = (1 && GET_WIMP_LEV(tch)))) - sprintf(buf2, "%d", GET_WIMP_LEV(tch)); - else - strcpy(buf2, "NO"); - break; - case SCMD_PAGELENGTH: - if (GET_PAGE_LENGTH(tch) == 0) - strcpy(buf2, "NONE"); - else - sprintf(buf2, "%d", GET_PAGE_LENGTH(tch)); - set = GET_PAGE_LENGTH(tch); - break; - case SCMD_AUTOINVIS: - if (GET_AUTOINVIS(tch) == -1) { - strcpy(buf2, "NO"); - } else { - set = true; - sprintf(buf2, "%d", GET_AUTOINVIS(tch)); - } - break; - default: - set = 1 && PRF_FLAGGED(tch, fields[i].bitvector); - strcpy(buf2, YESNO(set)); - break; - } - sprintf(buf, "%s %s%11s %5s&0 %s", buf, set ? QHWHT : QWHT, fields[i].cmd, buf2, - column == 2 ? "\n" : "| "); - if (++column >= 3) - column = 0; - } - if (column) - strcat(buf, "\n"); - strcat(buf, "===============================================================\n"); - char_printf(ch, buf); - return; - } + set = false; + switch (i) { + case SCMD_WIMPY: + if ((set = (1 && GET_WIMP_LEV(tch)))) + sprintf(buf2, "%d", GET_WIMP_LEV(tch)); + else + strcpy(buf2, "NO"); + break; + case SCMD_PAGELENGTH: + if (GET_PAGE_LENGTH(tch) == 0) + strcpy(buf2, "NONE"); + else + sprintf(buf2, "%d", GET_PAGE_LENGTH(tch)); + set = GET_PAGE_LENGTH(tch); + break; + case SCMD_AUTOINVIS: + if (GET_AUTOINVIS(tch) == -1) { + strcpy(buf2, "NO"); + } else { + set = true; + sprintf(buf2, "%d", GET_AUTOINVIS(tch)); + } + break; + default: + set = 1 && PRF_FLAGGED(tch, fields[i].bitvector); + strcpy(buf2, YESNO(set)); + break; + } + sprintf(buf, "%s %s%11s %5s&0 %s", buf, set ? QHWHT : QWHT, fields[i].cmd, buf2, + column == 2 ? "\n" : "| "); + if (++column >= 3) + column = 0; + } + if (column) + strcat(buf, "\n"); + strcat(buf, "===============================================================\n"); + char_printf(ch, buf); + return; + } if (IS_NPC(tch)) return; diff --git a/src/privileges.cpp b/src/privileges.cpp index 0eaa73e1..7f16d4a4 100644 --- a/src/privileges.cpp +++ b/src/privileges.cpp @@ -34,7 +34,7 @@ #include <fmt/format.h> struct privflagdef prv_flags[NUM_PRV_FLAGS] = { - {"clan admin", LVL_ADMIN, clan_admin_check}, + {"clan admin", LVL_ADMIN, nullptr}, {"title", LVL_GAMEMASTER, nullptr}, {"anon toggle", LVL_ATTENDANT, nullptr}, {"auto gain", LVL_ATTENDANT, nullptr}, diff --git a/src/quest.cpp b/src/quest.cpp index c148ac98..be74f13e 100644 --- a/src/quest.cpp +++ b/src/quest.cpp @@ -232,8 +232,8 @@ void perform_quest(TrigData *t, char *argument, CharData *ch, ObjData *obj, Room } /* Room trigger error string */ else if (room) { - sprintf(error_string, "QUEST ERROR: %s (%d) tried to {} [%s on quest %s in trigger %d]", room->name, room->vnum, - GET_NAME(vict), quest_name, GET_TRIG_VNUM(t)); + sprintf(error_string, "QUEST ERROR: %s (%d) tried to {} [%s on quest %s in trigger %d]", room->name.c_str(), + room->vnum, GET_NAME(vict), quest_name, GET_TRIG_VNUM(t)); } /* Other error string */ else { diff --git a/src/queue.cpp b/src/queue.cpp index 6717edff..6ce44c9b 100644 --- a/src/queue.cpp +++ b/src/queue.cpp @@ -102,6 +102,9 @@ void *queue_head(Queue *q) { void *data; int i; + if (!q) + return nullptr; + i = pulse % NUM_EVENT_QUEUES; if (!q->head[i]) @@ -136,6 +139,9 @@ void queue_free(Queue *q) { QElement *qe, *next_qe; Event *event; + if (!q) + return; + for (i = 0; i < NUM_EVENT_QUEUES; i++) for (qe = q->head[i]; qe; qe = next_qe) { next_qe = qe->next; diff --git a/src/redit.cpp b/src/redit.cpp index 4e7dd6c2..8d5f41c1 100644 --- a/src/redit.cpp +++ b/src/redit.cpp @@ -10,6 +10,7 @@ * Copyright 1996 Harvey Gilpin. * ***************************************************************************/ +#include "bitflags.hpp" #include "comm.hpp" #include "conf.hpp" #include "constants.hpp" @@ -75,8 +76,8 @@ void redit_setup_existing(DescriptorData *d, int real_num) { /* * Allocate space for all strings. */ - room->name = strdup(world[real_num].name ? world[real_num].name : "undefined"); - room->description = strdup(world[real_num].description ? world[real_num].description : "undefined\n"); + room->name = world[real_num].name.empty() ? "undefined" : world[real_num].name; + room->description = world[real_num].description.empty() ? "undefined\n" : world[real_num].description; /* * Exits - We allocate only if necessary. */ @@ -304,14 +305,14 @@ void redit_save_to_disk(int zone_num) { room = (world + realcounter); /*. Remove the '\n' sequences from description . */ - strcpy(buf1, room->description ? room->description : "Empty"); + strcpy(buf1, !room->description.empty() ? room->description.c_str() : "Empty"); strip_string(buf1); /* * Forget making a buffer, lets just write the thing now. */ - fprintf(fp, "#%d\n%s~\n%s~\n%d %ld %d\n", counter, room->name ? room->name : "undefined", buf1, - zone_table[room->zone].number, room->room_flags[0], room->sector_type); + fprintf(fp, "#%d\n%s~\n%s~\n%d %ld %d\n", counter, room->name.empty() ? "undefined" : room->name.c_str(), + buf1, zone_table[room->zone].number, room->flags[0], room->sector_type); /* * Handle exits. @@ -393,11 +394,6 @@ void free_room(RoomData *room) { int i; ExtraDescriptionData *cur, *next; - if (room->name) - free(room->name); - if (room->description) - free(room->description); - /* * Free exits. */ @@ -523,19 +519,18 @@ void redit_disp_flag_menu(DescriptorData *d) { #endif for (i = 0; i <= NUM_ROOM_FLAGS / columns; ++i) { - *buf = '\0'; + std::string output; for (j = 0; j < columns; ++j) if (FLAG_INDEX < NUM_ROOM_FLAGS) - sprintf(buf, "%s%s%2d%s) %-20.20s ", buf, grn, FLAG_INDEX + 1, nrm, room_bits[FLAG_INDEX]); - char_printf(d->character, strcat(buf, "\n")); + output += fmt::format("{}{:2d}{}) {:20.20s} ", grn, FLAG_INDEX + 1, nrm, room_bits[FLAG_INDEX]); + output += "\n"; + char_printf(d->character, output); } - sprintflag(buf1, OLC_ROOM(d)->room_flags, NUM_ROOM_FLAGS, room_bits); - sprintf(buf, - "\nRoom flags: %s%s%s\n" - "Enter room flags, 0 to quit : ", - cyn, buf1, nrm); - char_printf(d->character, buf); + char_printf(d->character, + "\nRoom flags: {}{}{}\n" + "Enter room flags, 0 to quit : ", + cyn, sprintflag(OLC_ROOM(d)->flags, room_bits), nrm); OLC_MODE(d) = REDIT_FLAGS; } @@ -576,35 +571,31 @@ void redit_disp_menu(DescriptorData *d) { get_char_cols(d->character); room = OLC_ROOM(d); - sprintflag(buf1, room->room_flags, NUM_ROOM_FLAGS, room_bits); - sprintf(buf2, "%s", sectors[room->sector_type].name); - sprintf(buf, + char_printf(d->character, #if defined(CLEAR_SCREEN) - ".[H.[J" + ".[H.[J" #endif - "-- Room: '&5%s&0' vnum: [&2%5d&0]\n" - "%s1%s) Name : %s%s\n" - "%s2%s) Description :\n%s%s\n" - "%s3%s) Room flags : %s%s\n" - "%s4%s) Sector type : %s%s\n", - room->name, OLC_NUM(d), grn, nrm, yel, room->name, grn, nrm, yel, room->description, grn, nrm, cyn, buf1, - grn, nrm, cyn, buf2); - - sprintf(buf, "%s%s5%s) Exit north : %s%s\n", buf, grn, nrm, cyn, exit_dest_desc(room->exits[NORTH])); - - sprintf(buf, "%s%s6%s) Exit east : %s%s\n", buf, grn, nrm, cyn, exit_dest_desc(room->exits[EAST])); - sprintf(buf, "%s%s7%s) Exit south : %s%s\n", buf, grn, nrm, cyn, exit_dest_desc(room->exits[SOUTH])); - sprintf(buf, "%s%s8%s) Exit west : %s%s\n", buf, grn, nrm, cyn, exit_dest_desc(room->exits[WEST])); - sprintf(buf, "%s%s9%s) Exit up : %s%s\n", buf, grn, nrm, cyn, exit_dest_desc(room->exits[UP])); - sprintf(buf, "%s%sA%s) Exit down : %s%s\n", buf, grn, nrm, cyn, exit_dest_desc(room->exits[DOWN])); - - sprintf(buf, - "%s%sF%s) Extra descriptions menu\n" - "%sS%s) Script : %s%s\n" - "%sQ%s) Quit\n" - "Enter choice:\n", - buf, grn, nrm, grn, nrm, cyn, room->proto_script ? "Set." : "Not Set.", grn, nrm); - char_printf(d->character, buf); + "-- Room: '&5{}&0' vnum: [&2{:5d}&0]\n" + "{}1{}) Name : {}{}\n" + "{}2{}) Description :\n{}{}\n" + "{}3{}) Room flags : {}{}\n" + "{}4{}) Sector type : {}{}\n", + room->name, OLC_NUM(d), grn, nrm, yel, room->name, grn, nrm, yel, room->description, grn, nrm, cyn, + sprintflag(room->flags, room_bits), grn, nrm, cyn, sectors[room->sector_type].name); + + char_printf(d->character, "{}5{}) Exit north : {}{}\n", grn, nrm, cyn, exit_dest_desc(room->exits[NORTH])); + char_printf(d->character, "{}6{}) Exit east : {}{}\n", grn, nrm, cyn, exit_dest_desc(room->exits[EAST])); + char_printf(d->character, "{}7{}) Exit south : {}{}\n", grn, nrm, cyn, exit_dest_desc(room->exits[SOUTH])); + char_printf(d->character, "{}8{}) Exit west : {}{}\n", grn, nrm, cyn, exit_dest_desc(room->exits[WEST])); + char_printf(d->character, "{}9{}) Exit up : {}{}\n", grn, nrm, cyn, exit_dest_desc(room->exits[UP])); + char_printf(d->character, "{}A{}) Exit down : {}{}\n", grn, nrm, cyn, exit_dest_desc(room->exits[DOWN])); + + char_printf(d->character, + "{}F{}) Extra descriptions menu\n" + "{}S{}) Script : {}{}\n" + "{}Q{}) Quit\n" + "Enter choice:\n", + grn, nrm, grn, nrm, cyn, room->proto_script ? "Set." : "Not Set.", grn, nrm); OLC_MODE(d) = REDIT_MAIN_MENU; } @@ -655,15 +646,18 @@ void redit_parse(DescriptorData *d, char *arg) { char_printf(d->character, "Enter room name:]\n"); OLC_MODE(d) = REDIT_NAME; break; - case '2': + case '2': { OLC_MODE(d) = REDIT_DESC; #if defined(CLEAR_SCREEN) string_to_output(d, "\x1B[H\x1B[J"); #endif string_to_output(d, "Enter room description: (/s saves /h for help)\n\n"); - string_write(d, &OLC_ROOM(d)->description, MAX_ROOM_DESC); + char *temp_desc = strdup(OLC_ROOM(d)->description.c_str()); + string_write(d, &temp_desc, MAX_ROOM_DESC); + OLC_ROOM(d)->description = temp_desc ? temp_desc : ""; + free(temp_desc); OLC_VAL(d) = 1; - break; + } break; case '3': redit_disp_flag_menu(d); break; @@ -723,8 +717,6 @@ void redit_parse(DescriptorData *d, char *arg) { return; break; case REDIT_NAME: - if (OLC_ROOM(d)->name) - free(OLC_ROOM(d)->name); if (strlen(arg) > MAX_ROOM_NAME) arg[MAX_ROOM_NAME - 1] = '\0'; OLC_ROOM(d)->name = strdup((arg && *arg) ? arg : "undefined"); @@ -747,7 +739,7 @@ void redit_parse(DescriptorData *d, char *arg) { /* * Toggle the bit. */ - TOGGLE_FLAG(OLC_ROOM(d)->room_flags, number - 1); + TOGGLE_FLAG(OLC_ROOM(d)->flags, number - 1); redit_disp_flag_menu(d); } return; diff --git a/src/rooms.cpp b/src/rooms.cpp index 2b5744ee..1030d4cb 100644 --- a/src/rooms.cpp +++ b/src/rooms.cpp @@ -7,6 +7,7 @@ * FieryMUD Copyright (C) 1998, 1999, 2000 by the Fiery Consortium * ***************************************************************************/ +#include "bitflags.hpp" #include "comm.hpp" #include "conf.hpp" #include "constants.hpp" @@ -72,18 +73,9 @@ const struct sectordef sectors[NUM_SECTORS] = { {"Avernus", "&5&b", 1, 0, 0, true, false, "(yes, you can camp here)", "(don't use)"}, }; -const char *room_bits[NUM_ROOM_FLAGS + 1] = { - "DARK", "DEATH", "!MOB", "INDOORS", "PEACEFUL", "SOUNDPROOF", "!TRACK", "!MAGIC", - "TUNNEL", "PRIVATE", "GODROOM", "HOUSE", "HCRSH", "ATRIUM", "OLC", "*BFS_MARK*", - "NOWELL", "NORECALL", "UNDERDARK", "!SUMMON", "NOSHIFT", "GUILDHALL", "!SCAN", "ALT_EXIT", - "MAP", "ALWAYSLIT", "ARENA", "OBSERVATORY", "\n"}; - -const char *room_effects[NUM_ROOM_EFF_FLAGS + 1] = {"FOG", "DARKNESS", "CONT_LIGHT", "FOREST", - "CIRCLE_FIRE", "ISOLATION", "\n"}; - /* act.movement.c */ void cantgo_msg(CharData *ch, int dir) { - const char *bumpinto = nullptr; + std::string_view bumpinto; if (!CONFUSED(ch)) { char_printf(ch, "Alas, you cannot go that way...\n"); @@ -235,10 +227,9 @@ void cantgo_msg(CharData *ch, int dir) { break; } - if (bumpinto) { + if (!bumpinto.empty()) { act("Oops! You bumped into $T!", false, ch, 0, bumpinto, TO_CHAR); - sprintf(buf, "$n tried to walk away and bumped into %s!", bumpinto); - act(buf, true, ch, 0, 0, TO_ROOM); + act(fmt::format("$n tried to walk away and bumped into {}!", bumpinto), true, ch, 0, 0, TO_ROOM); } } @@ -340,10 +331,9 @@ void open_door(CharData *ch, room_num roomnum, int dir, bool quiet) { if (ch && !quiet) { char_printf(ch, OK); - sprintf(buf, "$n opens the %s.", exit_name(exit)); - act(buf, false, ch, 0, 0, TO_ROOM); - send_gmcp_room(ch); + act(fmt::format("$n opens the %s.", exit_name(exit)), false, ch, 0, 0, TO_ROOM); } + send_gmcp_room(ch); } void close_door(CharData *ch, room_num roomnum, int dir, bool quiet) { @@ -392,8 +382,7 @@ void close_door(CharData *ch, room_num roomnum, int dir, bool quiet) { if (ch && !quiet) { char_printf(ch, OK); - sprintf(buf, "$n closes the %s.", exit_name(exit)); - act(buf, false, ch, 0, 0, TO_ROOM); + act(fmt::format("$n closes the %s.", exit_name(exit)), false, ch, 0, 0, TO_ROOM); send_gmcp_room(ch); } } @@ -476,15 +465,11 @@ void unlock_door(CharData *ch, room_num roomnum, int dir, bool quiet) { if (ch && !quiet) { if (key) { - sprintf(buf, "*Click* You unlock the %s with $p.", exit_name(exit)); - act(buf, false, ch, key, 0, TO_CHAR); - sprintf(buf, "$n unlocks the %s with $p.", exit_name(exit)); - act(buf, false, ch, key, 0, TO_ROOM); + act(fmt::format("*Click* You unlock the {} with $p.", exit_name(exit)), false, ch, key, 0, TO_CHAR); + act(fmt::format("$n unlocks the {} with $p.", exit_name(exit)), false, ch, key, 0, TO_ROOM); } else { - sprintf(buf, "*Click* You unlock the %s.", exit_name(exit)); - act(buf, false, ch, 0, 0, TO_CHAR); - sprintf(buf, "$n unlocks the %s.", exit_name(exit)); - act(buf, false, ch, 0, 0, TO_ROOM); + act(fmt::format("*Click* You unlock the {}.", exit_name(exit)), false, ch, 0, 0, TO_CHAR); + act(fmt::format("$n unlocks the {}.", exit_name(exit)), false, ch, 0, 0, TO_ROOM); } send_gmcp_room(ch); @@ -571,15 +556,11 @@ void lock_door(CharData *ch, room_num roomnum, int dir, bool quiet) { if (ch && !quiet) { if (key) { - sprintf(buf, "*Click* You lock the %s with $p.", exit_name(exit)); - act(buf, false, ch, key, 0, TO_CHAR); - sprintf(buf, "$n locks the %s with $p.", exit_name(exit)); - act(buf, false, ch, key, 0, TO_ROOM); + act(fmt::format("*Click* You lock the %s with $p.", exit_name(exit)), false, ch, key, 0, TO_CHAR); + act(fmt::format("$n locks the %s with $p.", exit_name(exit)), false, ch, key, 0, TO_ROOM); } else { - sprintf(buf, "*Click* You lock the %s.", exit_name(exit)); - act(buf, false, ch, 0, 0, TO_CHAR); - sprintf(buf, "$n locks the %s.", exit_name(exit)); - act(buf, false, ch, 0, 0, TO_ROOM); + act(fmt::format("*Click* You lock the %s.", exit_name(exit)), false, ch, 0, 0, TO_CHAR); + act(fmt::format("$n locks the %s.", exit_name(exit)), false, ch, 0, 0, TO_ROOM); } send_gmcp_room(ch); @@ -655,8 +636,7 @@ void pick_door(CharData *ch, room_num roomnum, int dir) { /* Feedback to this room. */ char_printf(ch, "The lock yields to your skills.\n"); - sprintf(buf, "$n skillfully picks the lock on the %s.", exit_name(exit)); - act(buf, false, ch, 0, 0, TO_ROOM); + act(fmt::format("$n skillfully picks the lock on the {}.", exit_name(exit)), false, ch, 0, 0, TO_ROOM); send_gmcp_room(ch); /* Skill improvement. */ @@ -673,13 +653,13 @@ void send_auto_exits(CharData *ch, int roomnum) { int dir; RoomData *room, *dest; Exit *exit; + std::string buf; room = &world[roomnum]; - *buf = '\0'; if (ROOM_EFF_FLAGGED(roomnum, ROOM_EFF_ISOLATION)) { if (GET_LEVEL(ch) >= LVL_IMMORT) - strcpy(buf, " &5&b<&0&5isolation&b>&0"); + buf = " &5&b<&0&5isolation&b>&0"; else { char_printf(ch, "&2Obvious exits: &6None&0.\n"); return; @@ -688,64 +668,58 @@ void send_auto_exits(CharData *ch, int roomnum) { for (dir = 0; dir < NUM_OF_DIRS; dir++) { if ((exit = room->exits[dir]) && ((dest = EXIT_DEST(exit))) && can_see_exit(ch, roomnum, exit)) { - sprintf(buf, "%s &0-&6%s", buf, capdirs[dir]); + buf += fmt::format(" &0-&6{}", capdirs[dir]); if (EXIT_IS_CLOSED(exit)) - sprintf(buf, "%s#", buf); + buf += "#"; if (EXIT_IS_HIDDEN(exit)) - sprintf(buf, "%s(hidden)", buf); + buf += "(hidden)"; } } - char_printf(ch, "&2Obvious exits:{}&0\n", *buf ? buf : " &6None&0."); + char_printf(ch, "&2Obvious exits:{}&0\n", buf.empty() ? " &6None&0." : buf); } void send_full_exits(CharData *ch, int roomnum) { int dir; - RoomData *room, *dest; + RoomData *room = &world[roomnum], *dest; Exit *exit; - - room = &world[roomnum]; - *buf = '\0'; + std::string buf; if (ROOM_EFF_FLAGGED(roomnum, ROOM_EFF_ISOLATION)) { if (GET_LEVEL(ch) < LVL_IMMORT) { char_printf(ch, "Obvious exits:\n None.\n"); return; } else { - strcpy(buf, "&9&b(&0exits obscured by &5isolation&0&9&b)&0\n"); + buf = "&9&b(&0exits obscured by &5isolation&0&9&b)&0\n"; } } for (dir = 0; dir < NUM_OF_DIRS; dir++) { if ((exit = room->exits[dir]) && ((dest = EXIT_DEST(exit))) && can_see_exit(ch, roomnum, exit)) { if (GET_LEVEL(ch) >= LVL_IMMORT && PRF_FLAGGED(ch, PRF_ROOMFLAGS)) { - strcpy(buf1, " &9&b[&0"); - /* only show keyword if there is one */ + buf += " &9&b[&0"; if (exit->keyword) { - /* Only show key vnum if there is one */ if (exit->key == NOTHING) - sprintf(buf1, "%s&2%s&0: ", buf1, exit->keyword); + buf += fmt::format("&2{}&0: ", exit->keyword); else - sprintf(buf1, "%s&2%s&0 (key %d): ", buf1, exit->keyword, exit->key); + buf += fmt::format("&2{}&0 (key {}): ", exit->keyword, exit->key); } - sprintbit(exit->exit_info, exit_bits, buf1 + strlen(buf1)); - strcat(buf1, "&9&b]&0"); - sprintf(buf2, "%-5s - [%5d] %s%s\n", dirs[dir], dest->vnum, dest->name, buf1); + buf += fmt::format("{}&9&b]&0", sprintbit(exit->exit_info, exit_bits)); + buf += fmt::format("{:<5} - [{:5}] {}\n", dirs[dir], dest->vnum, dest->name); } else { - sprintf(buf2, "%-5s - ", dirs[dir]); + buf += fmt::format("{:<5} - ", dirs[dir]); if (EXIT_IS_CLOSED(exit)) - sprintf(buf2, "%s(%s %s closed)", buf2, exit_name(exit), isplural(exit_name(exit)) ? "are" : "is"); + buf += fmt::format("({} {} closed)", exit_name(exit), isplural(exit_name(exit)) ? "are" : "is"); else if (IS_DARK(exit->to_room) && !CAN_SEE_IN_DARK(ch)) - strcat(buf2, "Too dark to tell"); + buf += "Too dark to tell"; else - strcat(buf2, dest->name); - strcat(buf2, "\n"); + buf += dest->name; + buf += "\n"; } - strcat(buf, cap_by_color(buf2)); } } - if (*buf) { + if (!buf.empty()) { char_printf(ch, "Obvious exits:\n{}&0", buf); } else char_printf(ch, "There are no obvious exits.\n"); diff --git a/src/rooms.hpp b/src/rooms.hpp index ad04296f..aae78d79 100644 --- a/src/rooms.hpp +++ b/src/rooms.hpp @@ -98,23 +98,30 @@ struct sectordef { }; extern const struct sectordef sectors[NUM_SECTORS]; -extern const char *room_bits[NUM_ROOM_FLAGS + 1]; -extern const char *room_effects[NUM_ROOM_EFF_FLAGS + 1]; + +constexpr std::array<std::string_view, NUM_ROOM_FLAGS> room_bits = { + "DARK", "DEATH", "!MOB", "INDOORS", "PEACEFUL", "SOUNDPROOF", "!TRACK", + "!MAGIC", "TUNNEL", "PRIVATE", "GODROOM", "HOUSE", "HCRSH", "ATRIUM", + "OLC", "*BFS_MARK*", "NOWELL", "NORECALL", "UNDERDARK", "!SUMMON", "NOSHIFT", + "GUILDHALL", "!SCAN", "ALT_EXIT", "MAP", "ALWAYSLIT", "ARENA", "OBSERVATORY"}; + +constexpr std::array<std::string_view, NUM_ROOM_EFF_FLAGS> room_effects = {"FOG", "DARKNESS", "CONT_LIGHT", + "FOREST", "CIRCLE_FIRE", "ISOLATION"}; struct RoomData { room_num vnum; /* Room's vnum */ int zone; /* Room zone (for resetting) */ int sector_type; /* sector type (move/hide) */ - char *name; /* Rooms name 'You are ...' */ - char *description; /* Shown when entered */ + std::string name; /* Rooms name 'You are ...' */ + std::string description; /* Shown when entered */ ExtraDescriptionData *ex_description; /* for examine/look */ Exit *exits[NUM_OF_DIRS]; /* DEATH,DARK ... etc */ - flagvector room_flags[FLAGVECTOR_SIZE(NUM_ROOM_FLAGS)]; + flagvector flags[FLAGVECTOR_SIZE(NUM_ROOM_FLAGS)]; /* bitvector for spells/skills */ - flagvector room_effects[FLAGVECTOR_SIZE(NUM_ROOM_EFF_FLAGS)]; + flagvector effects[FLAGVECTOR_SIZE(NUM_ROOM_EFF_FLAGS)]; int light; /* Number of light sources in room */ SPECIAL(*func); diff --git a/src/sedit.cpp b/src/sedit.cpp index 470c40cc..7d4ea38c 100644 --- a/src/sedit.cpp +++ b/src/sedit.cpp @@ -12,6 +12,7 @@ * CircleMUD is based on DikuMUD, Copyright (C) 1990, 1991. * ***************************************************************************/ +#include "bitflags.hpp" #include "comm.hpp" #include "conf.hpp" #include "constants.hpp" @@ -692,8 +693,9 @@ void sedit_shop_flags_menu(DescriptorData *d) { for (i = 0; i < NUM_SHOP_FLAGS; i++) { char_printf(d->character, "{}{:2d}{}) {:<20s} {}", grn, i + 1, nrm, shop_bits[i], !(++count % 2) ? "\n" : ""); } - sprintbit(S_BITVECTOR(OLC_SHOP(d)), shop_bits, buf1); - char_printf(d->character, "\nCurrent Shop Flags : {}{}{}\nEnter choice:\n", cyn, buf1, nrm); + + char_printf(d->character, "\nCurrent Shop Flags : {}{}{}\nEnter choice:\n", cyn, + sprintbit(S_BITVECTOR(OLC_SHOP(d)), shop_bits), nrm); OLC_MODE(d) = SEDIT_SHOP_FLAGS; } @@ -710,11 +712,10 @@ void sedit_no_trade_menu(DescriptorData *d) { char_printf(d->character, "{}{:2d}{}) {:<20s} {}", grn, i + 1, nrm, trade_letters[i], !(++count % 2) ? "\n" : ""); } - sprintbit(S_NOTRADE(OLC_SHOP(d)), trade_letters, buf1); char_printf(d->character, "\nCurrently won't trade with: {}{}{}\n" "Enter choice:\n", - cyn, buf1, nrm); + cyn, sprintbit(S_NOTRADE(OLC_SHOP(d)), trade_letters), nrm); OLC_MODE(d) = SEDIT_NOTRADE; } @@ -745,46 +746,43 @@ void sedit_disp_menu(DescriptorData *d) { shop = OLC_SHOP(d); get_char_cols(d->character); - sprintbit(S_NOTRADE(shop), trade_letters, buf1); - sprintbit(S_BITVECTOR(shop), shop_bits, buf2); - sprintf(buf, + char_printf(d->character, #if defined(CLEAR_SCREEN) - ".[H.[J" + ".[H.[J" #endif - "-- Shop Number : [%s%d%s]\n" - "%s0%s) Keeper : [%s%d%s] %s%s\n" - "%s1%s) Open 1 : %s%4d%s %s2%s) Close 1 : %s%4d\n" - "%s3%s) Open 2 : %s%4d%s %s4%s) Close 2 : %s%4d\n" - "%s5%s) Sell rate : %s%1.2f%s %s6%s) Buy rate : %s%1.2f\n" - "%s7%s) Keeper no item : %s%s\n" - "%s8%s) Player no item : %s%s\n" - "%s9%s) Keeper no cash : %s%s\n" - "%sA%s) Player no cash : %s%s\n", - cyn, OLC_NUM(d), nrm, grn, nrm, cyn, S_KEEPER(shop) == -1 ? -1 : mob_index[S_KEEPER(shop)].vnum, nrm, yel, - S_KEEPER(shop) == -1 ? "None" : mob_proto[S_KEEPER(shop)].player.short_descr, grn, nrm, cyn, S_OPEN1(shop), - nrm, grn, nrm, cyn, S_CLOSE1(shop), grn, nrm, cyn, S_OPEN2(shop), nrm, grn, nrm, cyn, S_CLOSE2(shop), grn, - nrm, cyn, S_BUYPROFIT(shop), nrm, grn, nrm, cyn, S_SELLPROFIT(shop), grn, nrm, yel, S_NOITEM1(shop), grn, - nrm, yel, S_NOITEM2(shop), grn, nrm, yel, S_NOCASH1(shop), grn, nrm, yel, S_NOCASH2(shop)); - char_printf(d->character, buf); + "-- Shop Number : [{}{}{}]\n" + "{}0{}) Keeper : [{}{}{}] {}{}\n" + "{}1{}) Open 1 : {}{:4d}{} {}2{}) Close 1 : {}{:4d}\n" + "{}3{}) Open 2 : {}{:4d}{} {}4{}) Close 2 : {}{:4d}\n" + "{}5{}) Sell rate : {}{:1.2f}{} {}6{}) Buy rate : {}{:1.2f}\n" + "{}7{}) Keeper no item : {}{}\n" + "{}8{}) Player no item : {}{}\n" + "{}9{}) Keeper no cash : {}{}\n" + "{}A{}) Player no cash : {}{}\n", + cyn, OLC_NUM(d), nrm, grn, nrm, cyn, S_KEEPER(shop) == -1 ? -1 : mob_index[S_KEEPER(shop)].vnum, nrm, + yel, S_KEEPER(shop) == -1 ? "None" : mob_proto[S_KEEPER(shop)].player.short_descr, grn, nrm, cyn, + S_OPEN1(shop), nrm, grn, nrm, cyn, S_CLOSE1(shop), grn, nrm, cyn, S_OPEN2(shop), nrm, grn, nrm, cyn, + S_CLOSE2(shop), grn, nrm, cyn, S_BUYPROFIT(shop), nrm, grn, nrm, cyn, S_SELLPROFIT(shop), grn, nrm, yel, + S_NOITEM1(shop), grn, nrm, yel, S_NOITEM2(shop), grn, nrm, yel, S_NOCASH1(shop), grn, nrm, yel, + S_NOCASH2(shop)); - sprintf(buf, + char_printf(d->character, #if defined(CLEAR_SCREEN) - ".[H.[J" + ".[H.[J" #endif - "%sB%s) Keeper no buy : %s%s\n" - "%sC%s) Buy sucess : %s%s\n" - "%sD%s) Sell sucess : %s%s\n" - "%sE%s) No Trade With : %s%s\n" - "%sF%s) Shop flags : %s%s\n" - "%sR%s) Rooms Menu\n" - "%sP%s) Products Menu\n" - "%sT%s) Accept Types Menu\n" - "%sQ%s) Quit\n" - "Enter Choice:\n", - grn, nrm, yel, S_NOBUY(shop), grn, nrm, yel, S_BUY(shop), grn, nrm, yel, S_SELL(shop), grn, nrm, cyn, buf1, - grn, nrm, cyn, buf2, grn, nrm, grn, nrm, grn, nrm, grn, nrm); - - char_printf(d->character, buf); + "{}B{}) Keeper no buy : {}{}\n" + "{}C{}) Buy sucess : {}{}\n" + "{}D{}) Sell sucess : {}{}\n" + "{}E{}) No Trade With : {}{}\n" + "{}F{}) Shop flags : {}{}\n" + "{}R{}) Rooms Menu\n" + "{}P{}) Products Menu\n" + "{}T{}) Accept Types Menu\n" + "{}Q{}) Quit\n" + "Enter Choice:\n", + grn, nrm, yel, S_NOBUY(shop), grn, nrm, yel, S_BUY(shop), grn, nrm, yel, S_SELL(shop), grn, nrm, cyn, + sprintbit(S_NOTRADE(shop), trade_letters), grn, nrm, cyn, sprintbit(S_BITVECTOR(shop), shop_bits), grn, + nrm, grn, nrm, grn, nrm, grn, nrm); OLC_MODE(d) = SEDIT_MAIN_MENU; } diff --git a/src/shop.cpp b/src/shop.cpp index 60e93db2..7df56948 100644 --- a/src/shop.cpp +++ b/src/shop.cpp @@ -14,6 +14,7 @@ #include "shop.hpp" #include "act.hpp" +#include "bitflags.hpp" #include "chars.hpp" #include "class.hpp" #include "comm.hpp" @@ -44,21 +45,16 @@ int top_shop; ShopData *shop_index; /* Constant list for printing out who we sell to */ -const char *trade_letters[] = {"Good", /* First, the alignment based ones */ - "Evil", "Neutral", "Magic User", /* Then the class based ones */ - "Cleric", "Thief", "Warrior", "\n"}; -const char *operator_str[] = {"[({", "])}", "|+", "&*", "^'"}; -const char *shop_bits[] = {"WILL_FIGHT", "USES_BANK", "\n"}; - -const char *msg_not_open_yet = "Come back later!"; -const char *msg_not_reopen_yet = "Sorry, we have closed, but come back later."; -const char *msg_closed_for_day = "Sorry, come back tomorrow."; -const char *msg_no_steal_here = "$n is a bloody thief!"; -const char *msg_no_see_char = "I don't trade with someone I can't see!"; -const char *msg_no_sell_align = "Get out of here before I call the guards!"; -const char *msg_no_sell_class = "We don't serve your kind here!"; -const char *msg_no_used_wandstaff = "I don't buy used up wands or staves!"; -const char *msg_cant_kill_keeper = "Get out of here before I call the guards!"; + +constexpr std::string_view msg_not_open_yet = "Come back later!"; +constexpr std::string_view msg_not_reopen_yet = "Sorry, we have closed, but come back later."; +constexpr std::string_view msg_closed_for_day = "Sorry, come back tomorrow."; +constexpr std::string_view msg_no_steal_here = "$n is a bloody thief!"; +constexpr std::string_view msg_no_see_char = "I don't trade with someone I can't see!"; +constexpr std::string_view msg_no_sell_align = "Get out of here before I call the guards!"; +constexpr std::string_view msg_no_sell_class = "We don't serve your kind here!"; +constexpr std::string_view msg_no_used_wandstaff = "I don't buy used up wands or staves!"; +constexpr std::string_view msg_cant_kill_keeper = "Get out of here before I call the guards!"; /* Forward/External function declarations */ ACMD(do_tell); @@ -74,7 +70,7 @@ int is_ok_char(CharData *keeper, CharData *ch, int shop_nr) { char buf[200]; if (!(CAN_SEE(keeper, ch))) { - do_say(keeper, strdup(msg_no_see_char), cmd_say, 0); + do_say(keeper, strdup(msg_no_see_char.data()), cmd_say, 0); return (false); } if (IS_GOD(ch)) @@ -82,7 +78,7 @@ int is_ok_char(CharData *keeper, CharData *ch, int shop_nr) { if ((IS_GOOD(ch) && NOTRADE_GOOD(shop_nr)) || (IS_EVIL(ch) && NOTRADE_EVIL(shop_nr)) || (IS_NEUTRAL(ch) && NOTRADE_NEUTRAL(shop_nr))) { - sprintf(buf, "%s %s", GET_NAME(ch), msg_no_sell_align); + sprintf(buf, "%s %s", GET_NAME(ch), msg_no_sell_align.data()); do_tell(keeper, buf, cmd_tell, 0); return (false); } @@ -91,7 +87,7 @@ int is_ok_char(CharData *keeper, CharData *ch, int shop_nr) { if ((IS_MAGIC_USER(ch) && NOTRADE_MAGIC_USER(shop_nr)) || (IS_CLERIC(ch) && NOTRADE_CLERIC(shop_nr)) || (IS_ROGUE(ch) && NOTRADE_THIEF(shop_nr)) || (IS_WARRIOR(ch) && NOTRADE_WARRIOR(shop_nr))) { - sprintf(buf, "%s %s", GET_NAME(ch), msg_no_sell_class); + sprintf(buf, "%s %s", GET_NAME(ch), msg_no_sell_class.data()); do_tell(keeper, buf, cmd_tell, 0); return (false); } @@ -103,12 +99,12 @@ int is_open(CharData *keeper, int shop_nr, int msg) { *buf = 0; if (SHOP_OPEN1(shop_nr) > time_info.hours) - strcpy(buf, msg_not_open_yet); + strcpy(buf, msg_not_open_yet.data()); else if (SHOP_CLOSE1(shop_nr) < time_info.hours) { if (SHOP_OPEN2(shop_nr) > time_info.hours) - strcpy(buf, msg_not_reopen_yet); + strcpy(buf, msg_not_reopen_yet.data()); else if (SHOP_CLOSE2(shop_nr) < time_info.hours) - strcpy(buf, msg_closed_for_day); + strcpy(buf, msg_closed_for_day.data()); } if (!(*buf)) return (true); @@ -160,7 +156,7 @@ int find_oper_num(char token) { int index; for (index = 0; index <= MAX_OPER; index++) - if (strchr(operator_str[index], token)) + if (strchr(operator_str[index].data(), token)) return (index); return (NOTHING); } @@ -186,14 +182,15 @@ int evaluate_expression(ObjData *obj, char *expr) { end = ptr; while (*ptr && !isspace(*ptr) && (find_oper_num(*ptr) == NOTHING)) ptr++; - strncpy(name, end, ptr - end); - name[ptr - end] = 0; - for (index = 0; *extra_bits[index] != '\n'; index++) - if (!strcasecmp(name, extra_bits[index])) { + size_t name_len = std::min((size_t)(ptr - end), sizeof(name) - 1); + strncpy(name, end, name_len); + name[name_len] = '\0'; + for (index = 0; extra_bits[index].front() != '\n'; index++) + if (!strcasecmp(name, extra_bits[index].data())) { push(&vals, OBJ_FLAGGED(obj, index)); break; } - if (*extra_bits[index] == '\n') + if (extra_bits[index].front() == '\n') push(&vals, isname(name, obj->name)); } else { if (temp != OPER_OPEN_PAREN) @@ -246,7 +243,8 @@ int trade_with(ObjData *item, int shop_nr) { for (counter = 0; SHOP_BUYTYPE(shop_nr, counter) != NOTHING; counter++) if (SHOP_BUYTYPE(shop_nr, counter) == GET_OBJ_TYPE(item)) { if ((GET_OBJ_VAL(item, VAL_WAND_CHARGES_LEFT) == 0) && - ((GET_OBJ_TYPE(item) == ITEM_WAND) || (GET_OBJ_TYPE(item) == ITEM_STAFF) || (GET_OBJ_TYPE(item) == ITEM_INSTRUMENT))) + ((GET_OBJ_TYPE(item) == ITEM_WAND) || (GET_OBJ_TYPE(item) == ITEM_STAFF) || + (GET_OBJ_TYPE(item) == ITEM_INSTRUMENT))) return OBJECT_DEAD; else if (evaluate_expression(item, SHOP_BUYWORD(shop_nr, counter))) return OBJECT_OK; @@ -314,12 +312,12 @@ char *times_message(ObjData *obj, char *name, int num) { ptr = name; else ptr++; - strncpy(buf, ptr, 200); - buf[199] = 0; + strncpy(buf, ptr, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; } if (num > 1) - sprintf(END_OF(buf), " (x %d)", num); + snprintf(END_OF(buf), sizeof(buf) - strlen(buf), " (x %d)", num); return buf; } @@ -453,7 +451,6 @@ int inspect_price(CharData *ch, CharData *keeper, ObjData *obj, int shop_nr) { return (price); } - void apply_getcash(CharData *ch, int cash) { GET_PLATINUM(ch) += PLATINUM_PART(cash); GET_GOLD(ch) += GOLD_PART(cash); @@ -473,7 +470,8 @@ void apply_cost(int cost, CharData *ch) { int haveP, haveG, haveS, haveC; if (cost > GET_CASH(ch)) { - log(LogSeverity::Warn, LVL_GOD, "ERR: {} being charged {} but doesn't have that much money", GET_NAME(ch), cost); + log(LogSeverity::Warn, LVL_GOD, "ERR: {} being charged {} but doesn't have that much money", GET_NAME(ch), + cost); return; } @@ -690,7 +688,7 @@ ObjData *get_selling_obj(CharData *ch, char *name, CharData *keeper, int shop_nr sprintf(buf, shop_index[shop_nr].do_not_buy, GET_NAME(ch)); break; case OBJECT_DEAD: - sprintf(buf, "%s %s", GET_NAME(ch), msg_no_used_wandstaff); + sprintf(buf, "%s %s", GET_NAME(ch), msg_no_used_wandstaff.data()); break; default: log("Illegal return value of {:d} from trade_with() (shop.c)", result); @@ -964,7 +962,7 @@ std::string list_object(CharData *keeper, ObjData *obj, CharData *ch, int cnt, i bp = ((int)(buy_price(ch, keeper, obj, shop_nr))); else if (service == SERVICE_PRICE) bp = ((int)(inspect_price(ch, keeper, obj, shop_nr))); - + return (fmt::format("{:<56} &0&b&6{:3d}&0p,&b&3{:d}&0g,&0{:d}s,&0&3{:d}&0c\n", strip_ansi(buf.c_str()), PLATINUM_PART(bp), GOLD_PART(bp), SILVER_PART(bp), COPPER_PART(bp))); } @@ -1020,7 +1018,8 @@ void shopping_list(char *arg, CharData *ch, CharData *keeper, int shop_nr) { void shopping_inspect(char *arg, CharData *ch, CharData *keeper, int shop_nr) { char tempstr[200], buf[MAX_STRING_LENGTH], name[MAX_INPUT_LENGTH]; ObjData *obj, *last_obj = 0; - int copperamt = 0, cnt = 0, index = 0;; + int copperamt = 0, cnt = 0, index = 0; + ; bool amount = 0, any = false; int counter; @@ -1038,7 +1037,8 @@ void shopping_inspect(char *arg, CharData *ch, CharData *keeper, int shop_nr) { if (!any) { any = true; paging_printf(ch, " ## Lvl Item Cost\n"); - paging_printf(ch, "--- --- ------------------------------------------------ -------------\n"); + paging_printf(ch, + "--- --- ------------------------------------------------ -------------\n"); } if (!last_obj) { last_obj = obj; @@ -1189,7 +1189,7 @@ SPECIAL(shop_keeper) { return (false); if (CMD_IS("steal")) { - sprintf(argm, "$N shouts '%s'", msg_no_steal_here); + sprintf(argm, "$N shouts '%s'", msg_no_steal_here.data()); do_action(keeper, GET_NAME(ch), cmd_slap, 0); act(argm, false, ch, 0, keeper, TO_CHAR); return (true); @@ -1222,7 +1222,7 @@ int ok_damage_shopkeeper(CharData *ch, CharData *victim) { for (index = 0; index < top_shop; index++) if ((GET_MOB_RNUM(victim) == SHOP_KEEPER(index)) && !SHOP_KILL_CHARS(index)) { do_action(victim, GET_NAME(ch), cmd_slap, 0); - sprintf(buf, "%s %s", GET_NAME(ch), msg_cant_kill_keeper); + sprintf(buf, "%s %s", GET_NAME(ch), msg_cant_kill_keeper.data()); do_tell(victim, buf, cmd_tell, 0); return (false); } @@ -1305,7 +1305,10 @@ int read_type_list(FILE *shop_f, ShopBuyData *list, int new_format, int max) { if (!new_format) return (read_list(shop_f, list, 0, max, LIST_TRADE)); do { - fgets(buf, MAX_STRING_LENGTH - 1, shop_f); + if (!fgets(buf, MAX_STRING_LENGTH - 1, shop_f)) { + fprintf(stderr, "Error reading shop file.\n"); + exit(1); + } if ((ptr = strchr(buf, ';')) != nullptr) *ptr = 0; else @@ -1431,14 +1434,14 @@ char *customer_string(int shop_nr, int detailed) { static char buf[256]; *buf = 0; - for (index = 0; *trade_letters[index] != '\n'; index++, cnt *= 2) + for (index = 0; trade_letters[index].front() != '\n'; index++, cnt *= 2) if (!(SHOP_TRADE_WITH(shop_nr) & cnt)) if (detailed) { if (*buf) strcat(buf, ", "); - strcat(buf, trade_letters[index]); + strcat(buf, trade_letters[index].data()); } else - sprintf(END_OF(buf), "%c", *trade_letters[index]); + sprintf(END_OF(buf), "%c", *trade_letters[index].data()); else if (!detailed) strcat(buf, "_"); @@ -1466,7 +1469,7 @@ void list_detailed_shop(CharData *ch, int shop_nr) { if (index) strcat(buf, ", "); if ((temp = real_room(SHOP_ROOM(shop_nr, index))) != NOWHERE) - sprintf(buf1, "%s (#%d)", world[temp].name, world[temp].vnum); + sprintf(buf1, "%s (#%d)", world[temp].name.c_str(), world[temp].vnum); else sprintf(buf1, "<UNKNOWN> (#%d)", SHOP_ROOM(shop_nr, index)); handle_detailed_list(buf, buf1, ch); @@ -1528,8 +1531,7 @@ void list_detailed_shop(CharData *ch, int shop_nr) { SHOP_SELLPROFIT(shop_nr), SHOP_BUYPROFIT(shop_nr), SHOP_OPEN1(shop_nr), SHOP_CLOSE1(shop_nr), SHOP_OPEN2(shop_nr), SHOP_CLOSE2(shop_nr), "\n"); - sprintbit((long)SHOP_BITVECTOR(shop_nr), shop_bits, buf1); - char_printf(ch, "Bits: {}\n", buf1); + char_printf(ch, "Bits: {}\n", sprintbit((long)SHOP_BITVECTOR(shop_nr), shop_bits)); } void do_stat_shop(CharData *ch, char *arg) { @@ -1584,7 +1586,7 @@ void list_shops(CharData *ch, int start, int end) { SHOP_BUYPROFIT(shop_nr)); strcat(buf2, customer_string(shop_nr, false)); sprintf(END_OF(buf), "%s %6d - %s\n", buf2, SHOP_ROOM(shop_nr, 0), - room == NOWHERE ? "<NOWHERE>" : world[room].name); + room == NOWHERE ? "<NOWHERE>" : world[room].name.c_str()); } } @@ -1599,7 +1601,7 @@ int vnum_shop(char *searchname, CharData *ch) { for (nr = 0; nr < top_shop; nr++) { room = real_room(SHOP_ROOM(nr, 0)); - if (room != NOWHERE && isname(searchname, world[room].name)) { + if (room != NOWHERE && isname(searchname, world[room].name.c_str())) { char_printf(ch, "{:3d}. [{:5d}] ({:5d}) {}\n", ++found, SHOP_NUM(nr), SHOP_ROOM(nr, 0), world[room].name); } } diff --git a/src/shop.hpp b/src/shop.hpp index 34940285..b603c883 100644 --- a/src/shop.hpp +++ b/src/shop.hpp @@ -101,8 +101,6 @@ struct StackData { #define OPER_NOT 4 #define MAX_OPER 4 -extern const char *operator_str[]; - #define SHOP_NUM(i) (shop_index[(i)].vnum) #define SHOP_KEEPER(i) (shop_index[(i)].keeper) #define SHOP_OPEN1(i) (shop_index[(i)].open1) @@ -138,9 +136,6 @@ extern const char *operator_str[]; #define SHOP_KILL_CHARS(i) (IS_SET(SHOP_BITVECTOR(i), WILL_START_FIGHT)) #define SHOP_USES_BANK(i) (IS_SET(SHOP_BITVECTOR(i), WILL_BANK_MONEY)) -extern const char *trade_letters[]; -extern const char *shop_bits[]; - #define MIN_OUTSIDE_BANK 5000 #define MAX_OUTSIDE_BANK 15000 @@ -148,3 +143,9 @@ bool give_shopkeeper_reject(CharData *ch, CharData *vict, ObjData *obj); extern int top_shop; extern ShopData *shop_index; + +constexpr std::string_view trade_letters[] = {"Good", /* First, the alignment based ones */ + "Evil", "Neutral", "Magic User", /* Then the class based ones */ + "Cleric", "Thief", "Warrior", "\n"}; +constexpr std::string_view operator_str[] = {"[({", "])}", "|+", "&*", "^'"}; +constexpr std::string_view shop_bits[] = {"WILL_FIGHT", "USES_BANK", "\n"}; diff --git a/src/skills.cpp b/src/skills.cpp index 55e0f62d..d8723752 100644 --- a/src/skills.cpp +++ b/src/skills.cpp @@ -30,16 +30,14 @@ SkillDef skills[TOP_SKILL_DEFINE + 1]; -const char *talent_types[5] = { - "talent", "spell", "skill", "chant", "song", -}; +std::string_view talent_types[5] = {"talent", "spell", "skill", "chant", "song"}; -const char *targets[NUM_TAR_FLAGS + 1] = {"IGNORE", "CHAR_ROOM", "CHAR_WORLD", "FIGHT_SELF", "FIGHT_VICT", - "SELF_ONLY", "NOT_SELF", "OBJ_INV", "OBJ_ROOM", "OBJ_WORLD", - "OBJ_EQUIP", "STRING", "NIGHT_ONLY", "DAY_ONLY", "OUTDOORS", - "GROUND", "CONTACT", "DIRECT", "\n"}; +std::string_view targets[NUM_TAR_FLAGS + 1] = {"IGNORE", "CHAR_ROOM", "CHAR_WORLD", "FIGHT_SELF", "FIGHT_VICT", + "SELF_ONLY", "NOT_SELF", "OBJ_INV", "OBJ_ROOM", "OBJ_WORLD", + "OBJ_EQUIP", "STRING", "NIGHT_ONLY", "DAY_ONLY", "OUTDOORS", + "GROUND", "CONTACT", "DIRECT", "\n"}; -const char *routines[NUM_ROUTINE_TYPES + 1] = { +std::string_view routines[NUM_ROUTINE_TYPES + 1] = { "DAMAGE", "AFFECT", "UNAFFECT", "POINT", "ALTER_OBJ", "GROUP", "MASS", "AREA", "SUMMON", "CREATION", "MANUAL", "ROOM", "BULK_OBJS", "\n", }; diff --git a/src/skills.hpp b/src/skills.hpp index a1073e48..f6650d8f 100644 --- a/src/skills.hpp +++ b/src/skills.hpp @@ -59,8 +59,9 @@ int level_to_circle(int level); int circle_to_level(int circle); #define IS_QUEST_SPELL(spellnum) (skills[(spellnum)].quest) #define SKILL_LEVEL(ch, skillnum) \ - ((skills[(skillnum)].min_level[(int)GET_CLASS(ch)] <= skills[(skillnum)].min_race_level[(int)GET_RACE(ch)]) ? \ - skills[(skillnum)].min_level[(int)GET_CLASS(ch)] : skills[(skillnum)].min_race_level[(int)GET_RACE(ch)]) + ((skills[(skillnum)].min_level[(int)GET_CLASS(ch)] <= skills[(skillnum)].min_race_level[(int)GET_RACE(ch)]) \ + ? skills[(skillnum)].min_level[(int)GET_CLASS(ch)] \ + : skills[(skillnum)].min_race_level[(int)GET_RACE(ch)]) #define SPELL_CIRCLE(ch, spellnum) (level_to_circle(SKILL_LEVEL(ch, spellnum))) #define CIRCLE_ABBR(ch, spellnum) (circle_abbrev[SPELL_CIRCLE((ch), (spellnum))]) #define SKILL_IS_TARGET(skill, tartype) \ @@ -86,7 +87,7 @@ void race_skill_assign(int skillnum, int race_code, int level); int talent_type(int skill_num); bool get_spell_assignment_circle(CharData *ch, int spell, int *circle_assignment, int *level_assignment); -extern const char *talent_types[5]; -extern const char *targets[NUM_TAR_FLAGS + 1]; -extern const char *routines[NUM_ROUTINE_TYPES + 1]; +extern std::string_view talent_types[5]; +extern std::string_view targets[NUM_TAR_FLAGS + 1]; +extern std::string_view routines[NUM_ROUTINE_TYPES + 1]; extern int skill_sort_info[TOP_SKILL + 1]; diff --git a/src/spec_procs.cpp b/src/spec_procs.cpp index 463930ec..beaa9d66 100644 --- a/src/spec_procs.cpp +++ b/src/spec_procs.cpp @@ -26,7 +26,9 @@ #include "limits.hpp" #include "logging.hpp" #include "math.hpp" +#include "modify.hpp" #include "movement.hpp" +#include "pfiles.hpp" #include "skills.hpp" #include "specprocs.hpp" #include "structs.hpp" @@ -470,35 +472,62 @@ SPECIAL(pet_shop) { * Special procedures for objects * ********************************************************************/ +bool is_object_storable(CharData *ch, ObjData *obj) { + if (OBJ_FLAGGED(obj, ITEM_NODROP)) { + char_printf(ch, "You can't store that because it's CURSED!\n"); + return false; + } else if (GET_OBJ_TYPE(obj) == ITEM_CONTAINER && obj->contains) { + char_printf(ch, "You can't store a container with items in it.\n"); + return false; + } else if (GET_OBJ_TYPE(obj) == ITEM_MONEY) { + char_printf(ch, "You can't store money in the clan vault.\n"); + return false; + } else if (GET_OBJ_VNUM(obj) == -1) { + char_printf(ch, "That item is much too unique to store.\n"); + return false; + } + return true; +} + +int find_obj_in_storage(CharData *ch, char *name) { + for (auto [vnum, amount] : GET_STORED(ch)) { + if (vnum > 0) { + int rnum = real_object(vnum); + auto proto = &obj_proto[rnum]; + if (isname(name, proto->name)) { + return vnum; + } + } + } + return -1; +} + SPECIAL(bank) { - int coins[NUM_COIN_TYPES], i; if (CMD_IS("balance")) { - statemoney(buf, GET_COINS(ch)); - char_printf(ch, "Coins carried: {}\n", buf); - statemoney(buf, GET_BANK_COINS(ch)); - char_printf(ch, "Coins in bank: {}\n", buf); + char_printf(ch, "Coins carried: {}.\n", statemoney(GET_COINS(ch))); + char_printf(ch, "Coins in bank: {}.\n", statemoney(GET_BANK_COINS(ch))); return true; } else if (CMD_IS("deposit")) { - - if (!parse_money(&argument, coins)) { + auto coin_opt = parse_money(argument); + if (!coin_opt) { char_printf(ch, "You can only deposit platinum, gold, silver, and copper coins.\n"); return true; } - for (i = 0; i < NUM_COIN_TYPES; ++i) + auto coins = *coin_opt; + for (int i = 0; i < NUM_COIN_TYPES; ++i) if (coins[i] > GET_COINS(ch)[i]) { char_printf(ch, "You don't have enough {}!\n", COIN_NAME(i)); return true; } act("$n makes a bank transaction.", true, ch, 0, 0, TO_ROOM); - statemoney(buf, coins); - char_printf(ch, "You deposit {}.\n", buf); + char_printf(ch, "You deposit {}.\n", statemoney(coins)); - for (i = 0; i < NUM_COIN_TYPES; ++i) { + for (int i = 0; i < NUM_COIN_TYPES; ++i) { GET_COINS(ch)[i] -= coins[i]; GET_BANK_COINS(ch)[i] += coins[i]; } @@ -511,34 +540,35 @@ SPECIAL(bank) { char_printf(ch, "You don't have any coins to deposit!\n"); else { char_printf(ch, "You dump all your coins on the counter to be deposited.\n"); - for (i = 0; i < NUM_COIN_TYPES; ++i) { + Money coins; + for (int i = 0; i < NUM_COIN_TYPES; ++i) { GET_BANK_COINS(ch)[i] += GET_COINS(ch)[i]; coins[i] = GET_COINS(ch)[i]; GET_COINS(ch)[i] = 0; } - statemoney(buf, coins); - char_printf(ch, "You were carrying {}.\n", buf); + char_printf(ch, "You were carrying {}.\n", statemoney(coins)); } return true; } else if (CMD_IS("withdraw")) { - if (!parse_money(&argument, coins)) { + auto coin_opt = parse_money(argument); + if (!coin_opt) { char_printf(ch, "You can only withdraw platinum, gold, silver, and copper coins.\n"); return true; } - for (i = 0; i < NUM_COIN_TYPES; ++i) + auto coins = *coin_opt; + for (int i = 0; i < NUM_COIN_TYPES; ++i) if (coins[i] > GET_BANK_COINS(ch)[i]) { char_printf(ch, "You don't have enough {} in the bank!\n", COIN_NAME(i)); return true; } act("$n makes a bank transaction.", true, ch, 0, 0, TO_ROOM); - statemoney(buf, coins); - char_printf(ch, "You withdraw {}.\n", buf); + char_printf(ch, "You withdraw {}.\n", statemoney(coins)); - for (i = 0; i < NUM_COIN_TYPES; ++i) { + for (int i = 0; i < NUM_COIN_TYPES; ++i) { GET_BANK_COINS(ch)[i] -= coins[i]; GET_COINS(ch)[i] += coins[i]; } @@ -553,7 +583,7 @@ SPECIAL(bank) { char arg3[MAX_INPUT_LENGTH]; char arg4[MAX_INPUT_LENGTH]; char ctype2[10]; - double exchange_rate; + double exchange_rate = 0.0; int copper, charge; int multto, multfrom; int type1, type2; @@ -562,7 +592,7 @@ SPECIAL(bank) { if (is_number(arg1)) { amount = atoi(arg1); if (!*arg2) { - char_printf(ch, "Exchange {} of what? Platinum?Gold?Silver?Copper?\n", arg1); + char_printf(ch, "Exchange {} of what? Platinum? Gold? Silver? Copper?\n", arg1); return 1; } half_chop(arg2, arg3, arg2); @@ -639,9 +669,10 @@ SPECIAL(bank) { } ok = 0; - exchange_rate = ((17 - (GET_CHA(ch) / 6.0) + random_number(0, 2) - (random_number(0, 4) / 10.0) + - (random_number(0, 9) / 10.0)) / - 100.0); + if (GET_LEVEL(ch) < 100) + exchange_rate = ((17 - (GET_CHA(ch) / 6.0) + random_number(0, 2) - (random_number(0, 4) / 10.0) + + (random_number(0, 9) / 10.0)) / + 100.0); amount = amount * multfrom; charge = (int)(ceil(exchange_rate * amount)); @@ -730,6 +761,183 @@ SPECIAL(bank) { " exchange 10 copper silver\n"); return 1; } + } else if (CMD_IS("items")) { + if (GET_STORED(ch).empty()) { + char_printf(ch, "You have no items stored in the bank.\n"); + return 1; + } + char_printf(ch, "You have {:d} items stored in the bank.\n\n", GET_STORED(ch).size()); + paging_printf(ch, " Amount : Item Name\n"); + paging_printf(ch, "----------------------------------------\n"); + for (auto [vnum, amount] : GET_STORED(ch)) { + if (vnum > 0) { + auto obj = read_object(vnum, VIRTUAL); + if (obj) { + paging_printf(ch, " {:>8} : {}\n", amount, obj->short_description); + extract_obj(obj); + } else { + log("SYSERR: bank error: invalid vnum {} in stored items for {}.", vnum, GET_NAME(ch)); + } + } + } + + start_paging(ch); + return 1; + } else if (CMD_IS("store")) { + // Find the object on the person + char *name = arg; + int amount = 0; + ObjData *obj, *next_obj; + FindContext context; + + argument = one_argument(argument, name); + if (!*name) { + char_printf(ch, "What do you want to store?\n"); + return 1; + } + + int dotmode = find_all_dots(&name); + + if (dotmode == FIND_ALL) { + char_printf(ch, "You can't store all of those at once!\n"); + return 1; + } else if (dotmode == FIND_ALLDOT) { + context = find_vis_by_name(ch, name); + if (!*name) { + char_printf(ch, "What do you want to store all of?\n"); + return 1; + } else if (!(obj = find_obj_in_list(ch->carrying, context))) { + char_printf(ch, "You don't seem to have any {}{}.\n", name, isplural(name) ? "" : "s"); + return 1; + } else { + while (obj) { + next_obj = find_obj_in_list(obj->next_content, context); + if (is_object_storable(ch, obj)) { + obj_from_char(obj); + GET_STORED(ch)[GET_OBJ_VNUM(obj)]++; + extract_obj(obj); + ++amount; + } + obj = next_obj; + } + if (amount) { + char_printf(ch, "You stored {:d} {}{}.\n", amount, name, isplural(name) ? "" : "s"); + } else { + char_printf(ch, "You don't seem to have any {}{}.\n", name, isplural(name) ? "" : "s"); + } + } + } else { + amount = 1; + if (is_number(name)) { + skip_spaces(&argument); + if (*argument) { + amount = atoi(name); + one_argument(argument, name); + } + } + context = find_vis_by_name(ch, name); + + if (!amount) { + char_printf(ch, "So...you don't want to store anything?\n"); + return 1; + } else if (!(obj = find_obj_in_list(ch->carrying, context))) { + char_printf(ch, "You don't seem to have {} {}{}.\n", amount == 1 ? an(name) : "any", arg, + amount == 1 || isplural(name) ? "" : "s"); + } else { + int total = amount; + + while (obj && amount > 0 && is_object_storable(ch, obj)) { + next_obj = find_obj_in_list(obj->next_content, context); + --amount; + GET_STORED(ch)[GET_OBJ_VNUM(obj)]++; + obj_from_char(obj); + extract_obj(obj); + obj = next_obj; + } + + if (total == amount) { + char_printf(ch, "You weren't able to store {} {}{}.\n", amount == 1 ? an(name) : "any", name, + amount == 1 || isplural(name) ? "" : "s"); + } else if (amount) { + char_printf(ch, "You only had {:d} {}{}.\n", total - amount, name, + isplural(name) || total == 1 ? "" : "s"); + } else if (total == 1) { + char_printf(ch, "You stored {}.\n", name); + } else + char_printf(ch, "You stored {:d} {}{}.\n", total, name, isplural(name) || total == 1 ? "" : "s"); + } + } + save_player(ch); + return 1; + } else if (CMD_IS("retrieve")) { + // Find the object on the person + char *name = arg; + int amount = 0, vnum = 0; + ObjData *obj; + + argument = one_argument(argument, name); + if (!*name) { + char_printf(ch, "What do you want to retrieve?\n"); + return 1; + } + + int dotmode = find_all_dots(&name); + std::unordered_map<int, int> &stored = GET_STORED(ch); + if (dotmode == FIND_ALL) { + char_printf(ch, "You can't retrieve all of those at once!\n"); + return 1; + } else if (dotmode == FIND_ALLDOT) { + if (!*name) { + char_printf(ch, "What do you want to retrieve all of?\n"); + } else if ((vnum = find_obj_in_storage(ch, name)) <= 0) { + char_printf(ch, "You don't seem to have any {}{} in storage.\n", name, isplural(name) ? "" : "s"); + } else { + amount = stored[vnum]; + for (int i = 0; i < amount; ++i) { + obj = read_object(vnum, VIRTUAL); + obj_to_char(obj, ch); + } + stored.erase(vnum); + char_printf(ch, "You retrieved {:d} {}{}.\n", amount, name, isplural(name) ? "" : "s"); + } + } else { + amount = 1; + if (is_number(name)) { + skip_spaces(&argument); + if (*argument) { + amount = atoi(name); + one_argument(argument, name); + } + } + + if (!amount) { + char_printf(ch, "So...you don't want to retrieve anything?\n"); + return 1; + } else if ((vnum = find_obj_in_storage(ch, name)) <= 0) { + char_printf(ch, "You don't seem to have any {}{} in storage.\n", name, isplural(name) ? "" : "s"); + } else if (stored[vnum] < amount) { + char_printf(ch, "You only have {:d} {}{} in storage.\n", stored[vnum], name, isplural(name) ? "" : "s"); + char_printf(ch, "You attempted to retrieve {:d} {}{}.\n", amount, name, isplural(name) ? "" : "s"); + } else { + for (int i = 0; i < amount; ++i) { + obj = read_object(vnum, VIRTUAL); + obj_to_char(obj, ch); + stored[vnum]--; + } + + if (stored[vnum] == 0) { + stored.erase(vnum); + } + + if (amount > 1) { + char_printf(ch, "You retrieved {:d} {}{}.\n", amount, name, isplural(name) ? "" : "s"); + } else { + char_printf(ch, "You retrieved {}.\n", name); + } + } + } + save_player(ch); + return 1; } else { return 0; } diff --git a/src/spells.cpp b/src/spells.cpp index aea5e2ac..d9f64f0a 100644 --- a/src/spells.cpp +++ b/src/spells.cpp @@ -180,8 +180,9 @@ ASPELL(spell_banish) { attack(victim, ch); } - /* min val -99, max val 207; at max skill and max roll and max charisma against a max level victim gives a value of 108 */ - roll = random_number(0, 100) + skill + stat_bonus[GET_CHA(ch)].magic - GET_LEVEL(victim); + /* min val -99, max val 207; at max skill and max roll and max charisma against a max level victim gives a value of + * 108 */ + roll = random_number(0, 100) + skill + stat_bonus[GET_CHA(ch)].magic - GET_LEVEL(victim); /* Failure */ if (roll < 50) { @@ -199,7 +200,7 @@ ASPELL(spell_banish) { /* Success */ if (roll > 100) { if (IS_NPC(victim)) { - roll = random_number (0, 100) + (stat_bonus[GET_WIS(ch)].magic * 2); /* min: 0, max: 114 */ + roll = random_number(0, 100) + (stat_bonus[GET_WIS(ch)].magic * 2); /* min: 0, max: 114 */ if (roll > 66) /* 66% chance to wipe victim eq, nears 50% at max wis */ extract_objects(victim); extract_char(victim); @@ -459,8 +460,8 @@ ASPELL(spell_create_water) { return 0; if (GET_OBJ_TYPE(obj) == ITEM_DRINKCON) { - amount = - std::min(GET_OBJ_VAL(obj, VAL_DRINKCON_CAPACITY) - GET_OBJ_VAL(obj, VAL_DRINKCON_REMAINING), 1 + 15 * skill / 2); + amount = std::min(GET_OBJ_VAL(obj, VAL_DRINKCON_CAPACITY) - GET_OBJ_VAL(obj, VAL_DRINKCON_REMAINING), + 1 + 15 * skill / 2); if (amount <= 0) { act("$o seems to be full already.", false, ch, obj, 0, TO_CHAR); } else { @@ -911,7 +912,7 @@ ASPELL(spell_heavens_gate) { GET_OBJ_DECOMP(portal) = 2; CREATE(new_descr, ExtraDescriptionData, 1); new_descr->keyword = strdup("tunnel light portal"); - sprintf(buf, "You can barely make out %s.\n", world[victim->in_room].name); + sprintf(buf, "You can barely make out %s.\n", world[victim->in_room].name.c_str()); new_descr->description = strdup(buf); new_descr->next = portal->ex_description; portal->ex_description = new_descr; @@ -928,7 +929,7 @@ ASPELL(spell_heavens_gate) { GET_OBJ_DECOMP(tportal) = 2; CREATE(new_tdescr, ExtraDescriptionData, 1); new_tdescr->keyword = strdup("tunnel light portal"); - sprintf(buf, "You can barely make out %s.\n", world[ch->in_room].name); + sprintf(buf, "You can barely make out %s.\n", world[ch->in_room].name.c_str()); new_tdescr->description = strdup(buf); new_tdescr->next = tportal->ex_description; tportal->ex_description = new_tdescr; @@ -974,7 +975,7 @@ ASPELL(spell_hells_gate) { GET_OBJ_DECOMP(portal) = 2; CREATE(new_descr, ExtraDescriptionData, 1); new_descr->keyword = strdup("portal hole gate"); - sprintf(buf, "You can barely make out %s.\n", world[victim->in_room].name); + sprintf(buf, "You can barely make out %s.\n", world[victim->in_room].name.c_str()); new_descr->description = strdup(buf); new_descr->next = portal->ex_description; portal->ex_description = new_descr; @@ -987,7 +988,7 @@ ASPELL(spell_hells_gate) { GET_OBJ_DECOMP(tportal) = 2; CREATE(new_tdescr, ExtraDescriptionData, 1); new_tdescr->keyword = strdup("portal hole gate"); - sprintf(buf, "You can barely make out %s.\n", world[ch->in_room].name); + sprintf(buf, "You can barely make out %s.\n", world[ch->in_room].name.c_str()); new_tdescr->description = strdup(buf); new_tdescr->next = tportal->ex_description; tportal->ex_description = new_tdescr; @@ -1434,7 +1435,8 @@ ASPELL(spell_major_paralysis) { return 0; if (!attack_ok(ch, victim, true)) return CAST_RESULT_CHARGE; - if (mag_savingthrow(victim, SAVING_PARA) || skill - GET_LEVEL(victim) > random_number(0, 70) || MOB_FLAGGED(victim, MOB_NOCHARM)) { + if (mag_savingthrow(victim, SAVING_PARA) || skill - GET_LEVEL(victim) > random_number(0, 70) || + MOB_FLAGGED(victim, MOB_NOCHARM)) { if (MOB_FLAGGED(victim, MOB_NOCHARM)) act("&7&b$N cannot be paralyzed!&0", false, ch, 0, victim, TO_CHAR); @@ -1478,8 +1480,8 @@ ASPELL(spell_minor_creation) { return 0; } half_chop(ch->casting.misc, buf, buf2); - while (*minor_creation_items[i] != '\n') { - if (is_abbrev(buf, minor_creation_items[i])) { + while (minor_creation_items[i].front() != '\n') { + if (is_abbrev(buf, minor_creation_items[i].data())) { found = 1; break; } else @@ -1542,7 +1544,7 @@ ASPELL(spell_moonwell) { GET_OBJ_DECOMP(portal) = 2; CREATE(new_descr, ExtraDescriptionData, 1); new_descr->keyword = strdup("well gate moonwell"); - sprintf(buf, "You can barely make out %s.\n", world[victim->in_room].name); + sprintf(buf, "You can barely make out %s.\n", world[victim->in_room].name.c_str()); new_descr->description = strdup(buf); new_descr->next = portal->ex_description; portal->ex_description = new_descr; @@ -1560,7 +1562,7 @@ ASPELL(spell_moonwell) { GET_OBJ_DECOMP(tportal) = 2; CREATE(new_tdescr, ExtraDescriptionData, 1); new_tdescr->keyword = strdup("well gate moonwell"); - sprintf(buf, "You can barely make out %s.\n", world[ch->in_room].name); + sprintf(buf, "You can barely make out %s.\n", world[ch->in_room].name.c_str()); new_tdescr->description = strdup(buf); new_tdescr->next = tportal->ex_description; tportal->ex_description = new_tdescr; @@ -1934,11 +1936,11 @@ static int search_for_doors(CharData *ch) { IS_SET(CH_EXIT(ch, door)->exit_info, EX_HIDDEN)) { sprintf(buf, "You have found%s hidden %s %s.&0", CH_EXIT(ch, door)->keyword && isplural(CH_EXIT(ch, door)->keyword) ? "" : " a", - CH_EXIT(ch, door)->keyword ? "$F" : "door", dirpreposition[door]); + CH_EXIT(ch, door)->keyword ? "$F" : "door", dirpreposition[door].data()); act(buf, false, ch, 0, CH_EXIT(ch, door)->keyword, TO_CHAR); sprintf(buf, "$n has found%s hidden %s %s.", CH_EXIT(ch, door)->keyword && isplural(CH_EXIT(ch, door)->keyword) ? "" : " a", - CH_EXIT(ch, door)->keyword ? "$F" : "door", dirpreposition[door]); + CH_EXIT(ch, door)->keyword ? "$F" : "door", dirpreposition[door].data()); act(buf, false, ch, 0, CH_EXIT(ch, door)->keyword, TO_ROOM); REMOVE_BIT(CH_EXIT(ch, door)->exit_info, EX_HIDDEN); send_gmcp_room(ch); @@ -2257,7 +2259,7 @@ void create_magical_wall(int room, int power, int dir, int spell, char *material else if (dir == DOWN) sprintf(buf2, "%sA wall of %s lies downwards.&0", mcolor, material); else - sprintf(buf2, "%sA wall of %s lies to the %s.&0", mcolor, material, dirs[dir]); + sprintf(buf2, "%sA wall of %s lies to the %s.&0", mcolor, material, dirs[dir].data()); wall->description = strdup(buf2); sprintf(buf, "%sa wall of %s&0", mcolor, material); wall->short_description = strdup(buf); @@ -2301,7 +2303,7 @@ ASPELL(spell_magical_wall) { half_chop(ch->casting.misc, buf, buf2); for (i = 0; i < NUM_OF_DIRS; i++) { - if (is_abbrev(buf, dirs[i])) + if (is_abbrev(buf, dirs[i].data())) dir = i; } @@ -2675,7 +2677,7 @@ ASPELL(spell_summon) { "{} just tried to summon you to: {}.\n" "{} failed because you have summon protection on.\n" "Type NOSUMMON to allow other players to summon you.\n", - GET_NAME(ch), world[ch->in_room].name, + GET_NAME(ch), world[ch->in_room].name.c_str(), (ch->player.sex == SEX_MALE) ? "He" : ((ch->player.sex == SEX_FEMALE) ? "She" : "They")); char_printf(ch, "You failed because {} has summon protection on.\n", GET_NAME(victim)); @@ -2810,7 +2812,7 @@ ASPELL(spell_locate_object) { if (o->carried_by) char_printf(ch, "{} is being carried by {}.\n", capitalize(o->short_description), PERS(o->carried_by, ch)); else if (o->in_room != NOWHERE) - char_printf(ch, "{} is in {}.\n", capitalize(o->short_description), world[o->in_room].name); + char_printf(ch, "{} is in {}.\n", capitalize(o->short_description), world[o->in_room].name.c_str()); else if (o->in_obj) char_printf(ch, "{} is in {}.\n", capitalize(o->short_description), o->in_obj->short_description); else if (o->worn_by) diff --git a/src/string_utils.cpp b/src/string_utils.cpp index 9b0f6f44..6e2ef702 100644 --- a/src/string_utils.cpp +++ b/src/string_utils.cpp @@ -17,13 +17,14 @@ #include "utils.hpp" #include <algorithm> +#include <numeric> #include <ranges> #include <string> #include <utility> -std::string ellipsis(const std::string str, int maxlen) { +std::string ellipsis(const std::string_view str, int maxlen) { if (str.length() < maxlen - 3) - return str; + return std::string(str); std::string result; bool in_code = false; @@ -42,25 +43,6 @@ std::string ellipsis(const std::string str, int maxlen) { return result + "..."; } -void sprintbit(long bitvector, const char *names[], char *result) { - long i; - char *orig_pos = result; - - /* Assuming 8 bits to a byte... */ - for (i = 0; *names[i] != '\n'; i++) { - if (IS_SET(bitvector, (1 << i))) { - strcpy(result, names[i]); - result += strlen(result); - *(result++) = ' '; - } - } - - if (orig_pos == result) - strcpy(result, "NO BITS"); - else - *result = '\0'; /* Nul terminate */ -} - std::string cap_string(std::string_view str) { std::string result; bool cap_next = true; @@ -77,6 +59,11 @@ std::string cap_string(std::string_view str) { return result; } +// Tests to see if the string_view is a (POSITIVE) integer +[[nodiscard]] bool is_integer(std::string_view sv) noexcept { + return !sv.empty() && std::ranges::all_of(trim(sv), ::isdigit); +} + std::string capitalize_first(std::string_view sv) { std::string result(sv); // Uppercase the first non-colour-sequence letter. @@ -94,95 +81,97 @@ std::string capitalize_first(std::string_view sv) { return result; } -void sprinttype(int type, const char *names[], char *result) { - int nr = 0; - - while (type && *names[nr] != '\n') { - type--; - nr++; - } +std::string to_lower(std::string_view str) { + std::string result; + std::transform(str.begin(), str.end(), std::back_inserter(result), ::tolower); + return result; +} - if (*names[nr] != '\n') - strcpy(result, names[nr]); - else { - strcpy(result, "UNDEFINED"); - log("SYSERR: Unknown type {} in sprinttype.", type); - } +bool is_equals(const std::string_view &lhs, const std::string_view &rhs) { + auto to_lower{std::ranges::views::transform(::tolower)}; + return std::ranges::equal(lhs | to_lower, rhs | to_lower); } -void sprintflag(char *result, flagvector flags[], int num_flags, const char *names[]) { - int i, nr = 0; - char *orig_pos = result; +bool matches_start(std::string_view lhs, std::string_view rhs) { + if (lhs.size() > rhs.size() || lhs.empty()) + return false; + return is_equals(lhs, rhs.substr(0, lhs.size())); +} - for (i = 0; i < num_flags; ++i) { - if (IS_FLAGGED(flags, i)) { - if (*names[nr] != '\n') - strcpy(result, names[nr]); - else - strcpy(result, "UNDEFINED"); - result += strlen(result); - *(result++) = ' '; +std::string filter_characters(const std::string_view input, bool (*filter_func)(char)) { + std::string result = ""; + for (char c : input) { + if (filter_func(c)) { + result += c; } - if (*names[nr] != '\n') - ++nr; } - - if (orig_pos == result) - strcpy(result, "NO FLAGS"); - else - *(result - 1) = '\0'; /* Nul terminate */ + return result; } -int sprintascii(char *out, flagvector bits) { - int i, j = 0; - /* 32 bits, don't just add letters to try to get more unless flagvector is - * also as large. */ - const char *flags = "abcdefghijklmnopqrstuvwxyzABCDEF"; - - for (i = 0; flags[i]; ++i) - if (bits & (1 << i)) - out[j++] = flags[i]; +void skip_spaces(std::string str) { str.erase(0, str.find_first_not_of(" \t\f\n\r")); } - if (j == 0) /* Didn't write anything. */ - out[j++] = '0'; +int svtoi(std::string_view sv, int default_value) noexcept { + int value = default_value; + auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value); + if (ec == std::errc()) { + return value; + } + return value; +} - /* Nul terminate the output string. */ - out[j++] = '\0'; - return j; +bool string_in_list(std::string_view target, const std::list<std::string_view> stringList) { + for (std::string_view element : stringList) { + if (target == element) { + return true; + } + } + return false; } -bool is_equals(const std::string_view &lhs, const std::string_view &rhs) { - auto to_lower{std::ranges::views::transform(::tolower)}; - return std::ranges::equal(lhs | to_lower, rhs | to_lower); +// c++23 +bool matches(std::string_view lhs, std::string_view rhs) { + if (lhs.size() != rhs.size()) + return false; + return std::ranges::all_of(std::ranges::zip_view(lhs, rhs), + [](auto pr) { return tolower(std::get<0>(pr)) == tolower(std::get<1>(pr)); }); } -bool matches_start(std::string_view lhs, std::string_view rhs) { +bool matches_end(std::string_view lhs, std::string_view rhs) { if (lhs.size() > rhs.size() || lhs.empty()) return false; - return is_equals(lhs, rhs.substr(0, lhs.size())); + auto rhs_remaining = rhs.substr(rhs.size() - lhs.size(), lhs.size()); + auto zipped_reverse = std::ranges::zip_view(lhs, rhs_remaining) | std::ranges::views::reverse; + return std::ranges::all_of(zipped_reverse, + [](auto pr) { return tolower(std::get<0>(pr)) == tolower(std::get<1>(pr)); }); } -// c++23 -// bool matches(std::string_view lhs, std::string_view rhs) { -// if (lhs.size() != rhs.size()) -// return false; -// return std::ranges::all_of(std::ranges::zip_view(lhs, rhs), -// [](auto pr) { return tolower(pr.first) == tolower(pr.second); }); -// } - -// bool matches_end(std::string_view lhs, std::string_view rhs) { -// if (lhs.size() > rhs.size() || lhs.empty()) -// return false; -// auto rhs_remaining = rhs.substr(rhs.size() - lhs.size(), lhs.size()); -// auto zipped_reverse = std::ranges::zip_view(lhs, rhs_remaining) | std::ranges::views::reverse; -// return std::ranges::all_of(zipped_reverse, [](auto pr) { return tolower(pr.first) == tolower(pr.second); }); -// } - -// bool matches_inside(std::string_view needle, std::string_view haystack) { -// auto needle_low = needle | std::ranges::views::transform(tolower); -// auto haystack_low = haystack | std::ranges::views::transform(tolower); -// return !std::ranges::search(haystack_low, needle_low).empty(); -// } +bool matches_inside(std::string_view needle, std::string_view haystack) { + auto needle_low = needle | std::ranges::views::transform(tolower); + auto haystack_low = haystack | std::ranges::views::transform(tolower); + return !std::ranges::search(haystack_low, needle_low).empty(); +} + +std::string replace_string(std::string_view str, std::string_view from, std::string_view to) { + std::string result(str); + size_t pos = result.find(from); + while (pos != std::string::npos) { + result.replace(pos, from.length(), to); + pos = result.find(from, pos + to.length()); + } + return result; +} + +std::string to_lowercase(std::string_view str) { + std::string result; + std::transform(str.begin(), str.end(), std::back_inserter(result), ::tolower); + return result; +} + +std::string to_uppercase(std::string_view str) { + std::string result; + std::transform(str.begin(), str.end(), std::back_inserter(result), ::toupper); + return result; +} std::string progress_bar(int current, int level_max, int max) { double percentage = static_cast<double>(current) / max * 100; @@ -208,3 +197,31 @@ std::string progress_bar(int current, int level_max, int max) { return progress_bar; } + +std::string_view getline(std::string_view &input, char delim) { + auto pos = input.find(delim); + if (pos == std::string_view::npos) { + auto line = input; + input.remove_prefix(input.size()); + return line; + } + auto line = input.substr(0, pos); + input.remove_prefix(pos + 1); + return line; +} + +std::string join_strings(const std::vector<std::string> &strings, const std::string_view separator, + const std::string_view last_separator) { + if (strings.empty()) { + return ""; + } + if (strings.size() == 1) { + return std::string(strings[0]); + } + + return std::accumulate(strings.begin(), strings.end() - 1, std::string{}, + [&separator](const std::string &a, const std::string &b) { + return a.empty() ? b : a + std::string(separator) + b; + }) + + std::string(last_separator) + strings.back(); +} \ No newline at end of file diff --git a/src/string_utils.hpp b/src/string_utils.hpp index 33c59977..ad282b39 100644 --- a/src/string_utils.hpp +++ b/src/string_utils.hpp @@ -1,22 +1,24 @@ #pragma once -#include "structs.hpp" - +#include <list> #include <string> #include <string_view> +#include <vector> + +using namespace std::literals::string_view_literals; -[[nodiscard]] std::string ellipsis(const std::string str, int maxlen); +[[nodiscard]] std::string ellipsis(const std::string_view str, int maxlen); -void sprintbit(long vektor, const char *names[], char *result); -void sprinttype(int type, const char *names[], char *result); -void sprintflag(char *result, flagvector flags[], int num_flags, const char *names[]); -int sprintascii(char *out, flagvector bits); -bool is_equals(const std::string_view &lhs, const std::string_view &rhs); +[[nodiscard]] bool is_equals(const std::string_view &lhs, const std::string_view &rhs); // Similar to matches() but checks if rhs starts with lhs, case insensitively. // lhs must be at least one character long and must not be longer than rhs. [[nodiscard]] bool matches_start(std::string_view lhs, std::string_view rhs); +// // Similar to matches_start() but checks if rhs ends with lhs, case insensitively. +// // lhs must be at least one character long and must not be longer than rhs. +[[nodiscard]] bool matches_end(std::string_view lhs, std::string_view rhs); + [[nodiscard]] constexpr std::string_view trim_left(std::string_view s) { return s.substr(std::min(s.find_first_not_of(" \f\n\r\t\v"), s.size())); } @@ -24,19 +26,36 @@ bool is_equals(const std::string_view &lhs, const std::string_view &rhs); [[nodiscard]] constexpr std::string_view trim_right(std::string_view s) { return s.substr(0, std::min(s.find_last_not_of(" \f\n\r\t\v") + 1, s.size())); } - +[[nodiscard]] bool is_integer(std::string_view sv) noexcept; [[nodiscard]] constexpr std::string_view trim(std::string_view s) { return trim_left(trim_right(s)); } [[nodiscard]] std::string capitalize_first(std::string_view sv); -// // Compares two strings: are they referring to the same thing. That currently means "case insensitive -// comparison". -// [[nodiscard]] bool matches(std::string_view lhs, std::string_view rhs); - -// // Similar to matches_start() but checks if rhs ends with lhs, case insensitively. -// // lhs must be at least one character long and must not be longer than rhs. -// [[nodiscard]] bool matches_end(std::string_view lhs, std::string_view rhs); +// Compares two strings: are they referring to the same thing. That currently means "case insensitive comparison". +[[nodiscard]] bool matches(std::string_view lhs, std::string_view rhs); // // Is 'needle' contained inside 'haystack' case insensitively? -// [[nodiscard]] bool matches_inside(std::string_view needle, std::string_view haystack); +[[nodiscard]] bool matches_inside(std::string_view needle, std::string_view haystack); + +[[nodiscard]] std::string filter_characters(std::string_view input, bool (*filter_func)(char)); + +void skip_spaces(std::string_view str); +[[nodiscard]] bool string_in_list(std::string_view target, const std::list<std::string_view> stringList); +[[nodiscard]] std::string replace_string(std::string_view str, std::string_view from, std::string_view to); +[[nodiscard]] int svtoi(std::string_view sv, int default_value = -1) noexcept; +[[nodiscard]] std::string to_lower(std::string_view str); + +[[nodiscard]] std::string progress_bar(int current, int wall = 0, int max = 1000); + +/* Given a string, change all instances of double dollar signs ($$) to single dollar signs ($). When strings come in, + * all $'s are changed to $$'s to avoid having users be able to crash the system if the inputted string is eventually + * sent to act(). If you are using user input to produce screen output AND YOU ARE SURE IT WILL NOT BE SENT THROUGH THE + * act() FUNCTION (i.e., do_gecho, do_title, but NOT do_gsay), you can call delete_doubledollar() to make the output + * look correct. */ +// void delete_doubledollar(std::string &str) { str = replace_string(str, "$$", "$"); } + +[[nodiscard]] std::string to_lowercase(std::string_view str); +[[nodiscard]] std::string to_uppercase(std::string_view str); +[[nodiscard]] std::string_view getline(std::string_view &input, char delim); -[[nodiscard]] std::string progress_bar(int current, int wall = 0, int max = 1000); \ No newline at end of file +[[nodiscard]] std::string join_strings(const std::vector<std::string> &strings, const std::string_view separator, + const std::string_view last_separator); diff --git a/src/structs.hpp b/src/structs.hpp index 6cf2e82e..0a35cfe1 100644 --- a/src/structs.hpp +++ b/src/structs.hpp @@ -16,13 +16,20 @@ #include "sysdep.hpp" #include <list> +#include <memory> #include <string> +#include <unordered_map> #include <vector> +#include <optional> typedef int room_num; typedef int obj_num; typedef int zone_vnum; +// Forward declarations +using ClanID = unsigned int; +constexpr ClanID CLAN_ID_NONE = 0; + #define DAMAGE_WILL_KILL(ch, dmg) (GET_HIT(ch) - dmg <= HIT_DEAD) // TODO: Refactor this file so we don't need all these forward declarations. @@ -46,7 +53,7 @@ typedef unsigned short int ush_int; typedef unsigned long int flagvector; #define FLAGBLOCK_SIZE (flagvector)32 //((flagvector)8 * sizeof(flagvector)) /* 8 bits = 1 byte */ -#define FLAGVECTOR_SIZE(flags) (((flags)-1) / FLAGBLOCK_SIZE + 1) +#define FLAGVECTOR_SIZE(flags) (((flags) - 1) / FLAGBLOCK_SIZE + 1) /* Extra description: used in objects, mobiles, and rooms */ struct ExtraDescriptionData { @@ -161,6 +168,7 @@ struct CharAbilityData { }; /* Char's points. */ +class Money; struct CharPointData { int mana; int max_mana; /* Max move for PC/NPC */ @@ -169,7 +177,7 @@ struct CharPointData { int move; int max_move; /* Max move for PC/NPC */ int armor; /* Internal -100..100, external -10..10 AC */ - int coins[NUM_COIN_TYPES]; + int money[NUM_COIN_TYPES]; int bank[NUM_COIN_TYPES]; long exp; /* The experience of the player */ @@ -237,8 +245,9 @@ struct OLCZoneList { * not allocated in memory for NPCs, but it is for PCs. This structure * can be changed freely. */ -struct ClanMembership; -struct ClanSnoop; +class ClanMembership; +using ClanMembershipPtr = std::shared_ptr<ClanMembership>; + struct GrantType; struct RetainedComms; struct TrophyNode; @@ -266,6 +275,7 @@ struct PlayerSpecialData { sbyte conditions[3]; /* Drunk, full, thirsty */ TrophyNode *trophy; AliasData *aliases; + std::unordered_map<int, int> stored; /* List of stored (banked) items */ flagvector *grant_cache; /* cache of granted commands */ flagvector *revoke_cache; /* cache of revoked commands */ @@ -277,8 +287,8 @@ struct PlayerSpecialData { GrantType *revoke_groups; ubyte page_length; - ClanMembership *clan; - ClanSnoop *clan_snoop; + ClanID clan_id{CLAN_ID_NONE}; /* Clan ID (0 = no clan) */ + OLCZoneList *olc_zones; int lastlevel; int base_hit; @@ -510,17 +520,17 @@ struct message_list { }; struct stat_bonus_type { - sh_int tohit; /* To Hit (THAC0) Bonus/Penalty */ - sh_int todam; /* Damage Bonus/Penalty */ - sh_int defense; /* Armor Class Bonus/Penalty */ - sh_int carry; /* Maximum weight that can be carrried */ - sh_int wield; /* Maximum weight that can be wielded */ - sh_int magic; /* Stat bonus to spells */ - sh_int hpgain; /* Bonus to HP gained at level */ - sh_int skill_small; /* Range -7 to 5 bonus to skills */ - sh_int skill_medium; /* Range -7 to 10 bonus to skills */ - sh_int skill_large; /* Range -7 to 15 bonus to skills */ - sh_int rogue_skills; /* Bonus range for rogue-type skills */ + sh_int tohit; /* To Hit (THAC0) Bonus/Penalty */ + sh_int todam; /* Damage Bonus/Penalty */ + sh_int defense; /* Armor Class Bonus/Penalty */ + sh_int carry; /* Maximum weight that can be carrried */ + sh_int wield; /* Maximum weight that can be wielded */ + sh_int magic; /* Stat bonus to spells */ + sh_int hpgain; /* Bonus to HP gained at level */ + sh_int skill_small; /* Range -7 to 5 bonus to skills */ + sh_int skill_medium; /* Range -7 to 10 bonus to skills */ + sh_int skill_large; /* Range -7 to 15 bonus to skills */ + sh_int rogue_skills; /* Bonus range for rogue-type skills */ }; struct weather_data { diff --git a/src/text.cpp b/src/text.cpp index 421d792c..5606534d 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -194,7 +194,7 @@ void format_text(char **ptr_string, int mode, DescriptorData *d, int maxlen) { } } else { cap_next = false; - *start = UPPER(*start); + *start = to_upper(*start); } total_chars += strlen(start); @@ -274,7 +274,7 @@ char *cap_by_color(char *s) { b += 2; if (*b) - *b = UPPER(*b); + *b = to_upper(*b); return s; } @@ -791,7 +791,7 @@ static void sb_compile_lines(ScreenBuf *sb, size_t start_line) { if (cap_next) { cap_next = false; if (IS_FLAGGED(sb->flags, SB_USE_CAPS)) - *start = UPPER(*start); + *start = to_upper(*start); } /* Don't print a leading space if the last word printed was * hyphenated. diff --git a/src/utils.cpp b/src/utils.cpp index 6f099853..9cccea88 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -12,6 +12,7 @@ #include "utils.hpp" +#include "bitflags.hpp" #include "casting.hpp" #include "comm.hpp" #include "composition.hpp" @@ -311,7 +312,9 @@ int get_line(FILE *fl, char *buf) { do { lines++; - fgets(temp, 256, fl); + if (!fgets(temp, 256, fl)) { + return 0; // Return 0 if fgets fails + } if (*temp) { temp[strlen(temp) - 1] = '\0'; } @@ -738,8 +741,7 @@ char *format_apply(int apply, int modifier) { else sprintf(f, "&3&bInvalid Composition!&0"); } else if (apply > 0) { - sprinttype(apply, apply_types, buf2); - sprintf(f, "%+d to %s", modifier, buf2); + sprintf(f, "%+d to %s", modifier, sprinttype(apply, apply_types).c_str()); } else { sprintf(f, "None."); } diff --git a/src/utils.hpp b/src/utils.hpp index f4385f37..36c25abf 100644 --- a/src/utils.hpp +++ b/src/utils.hpp @@ -19,6 +19,7 @@ #include "text.hpp" #include <math.h> +#include <string_view> /* external declarations and prototypes **********************************/ @@ -104,14 +105,22 @@ void drop_core(CharData *ch, const char *desc); #define ONOFF(a) ((a) ? "ON" : "OFF") /* IS_UPPER and IS_LOWER added by dkoepke */ -#define IS_UPPER(c) ((c) >= 'A' && (c) <= 'Z') -#define IS_LOWER(c) ((c) >= 'a' && (c) <= 'z') -#define LOWER(c) (IS_UPPER(c) ? ((c) + ('a' - 'A')) : (c)) -#define UPPER(c) (IS_LOWER(c) ? ((c) + ('A' - 'a')) : (c)) +constexpr bool is_upper(char c) { return c >= 'A' && c <= 'Z'; } -#define IS_NEWLINE(ch) ((ch) == '\n') -#define AN(string) (strchr("aeiouAEIOU", *string) ? "an" : "a") +constexpr bool is_lower(char c) { return c >= 'a' && c <= 'z'; } + +constexpr char to_lower(char c) { return is_upper(c) ? (c + ('a' - 'A')) : c; } + +constexpr char to_upper(char c) { return is_lower(c) ? (c + ('A' - 'a')) : c; } + +constexpr bool is_newline(char ch) { return ch == '\n'; } + +constexpr const char *an(const char *str) { return (str && strchr("aeiouAEIOU", str[0])) ? "an" : "a"; } + +constexpr const char *an(std::string_view str) { + return (!str.empty() && strchr("aeiouAEIOU", str.front())) ? "an" : "a"; +} /* memory utils **********************************************************/ @@ -188,8 +197,8 @@ extern flagvector *ALL_FLAGS; #define PRF_FLAGS(ch) ((ch)->player_specials->pref) #define PRV_FLAGS(ch) ((ch)->player_specials->privileges) #define EFF_FLAGS(ch) ((ch)->char_specials.effects) -#define ROOM_FLAGS(loc) (world[(loc)].room_flags) -#define ROOM_EFFECTS(loc) (world[(loc)].room_effects) +#define ROOM_FLAGS(loc) (world[(loc)].flags) +#define ROOM_EFFECTS(loc) (world[(loc)].effects) #define IS_NPC(ch) IS_FLAGGED(MOB_FLAGS(ch), MOB_ISNPC) #define IS_MOB(ch) (IS_NPC(ch) && ((ch)->mob_specials.nr > -1)) @@ -265,7 +274,7 @@ extern flagvector *ALL_FLAGS; #define GET_MAX_MOVE(ch) ((ch)->points.max_move) #define GET_MANA(ch) ((ch)->points.mana) #define GET_MAX_MANA(ch) ((ch)->points.max_mana) -#define GET_COINS(ch) ((ch)->points.coins) +#define GET_COINS(ch) ((ch)->points.money) #define GET_PURSE_COINS(ch, coin) (GET_COINS(ch)[coin]) #define GET_PLATINUM(ch) (GET_COINS(ch)[PLAT]) #define GET_GOLD(ch) (GET_COINS(ch)[GOLD]) @@ -438,6 +447,7 @@ extern flagvector *ALL_FLAGS; #define GET_REVOKES(ch) ((ch)->player_specials->revokes) #define GET_GRANT_GROUPS(ch) ((ch)->player_specials->grant_groups) #define GET_REVOKE_GROUPS(ch) ((ch)->player_specials->revoke_groups) +#define GET_STORED(ch) ((ch)->player_specials->stored) /* Mob accessors */ #define GET_EX_HIT(ch) ((ch)->mob_specials.ex_hit) diff --git a/src/vsearch.cpp b/src/vsearch.cpp index 8b3b1943..86e116ae 100644 --- a/src/vsearch.cpp +++ b/src/vsearch.cpp @@ -12,6 +12,7 @@ #include "vsearch.hpp" +#include "bitflags.hpp" #include "casting.hpp" #include "charsize.hpp" #include "class.hpp" @@ -121,7 +122,7 @@ struct VSearchType { int type; char *name; char data; - const char **lookup; + const std::string_view *lookup; }; /* @@ -213,7 +214,7 @@ static char *lowercase(const char *string) { static char lower[50]; char *p = lower; /* - * Gotta use tolower() because LOWER() would cause the ++ to go off + * Gotta use tolower() because to_lower() would cause the ++ to go off * twice...resulting in bad things. (memory overflow -> game lockup) */ while ((*(p++) = tolower(*(string++)))) @@ -451,10 +452,10 @@ bool parse_vsearch_args(CharData *ch, char *argument, int subcmd, int *mode, con argument = delimited_arg(argument, arg, '\''); if (!*arg) { sprintf(buf, "You can search for the following %s types:", modes[type].name); - for (temp = 0; *modes[type].lookup[temp] != '\n'; ++temp) { + for (temp = 0; modes[type].lookup[temp].front() != '\n'; ++temp) { if (!(temp % 4)) strcat(buf, "\n"); - sprintf(buf, "%s%-16s", buf, modes[type].lookup[temp]); + sprintf(buf, "%s%-16s", buf, modes[type].lookup[temp].data()); } char_printf(ch, "{}\n", buf); return false; @@ -646,10 +647,10 @@ bool parse_vsearch_args(CharData *ch, char *argument, int subcmd, int *mode, con delimited_arg(argument, arg, '\''); if (!*arg) { sprintf(buf, "You can search for the following %s flags:", modes[type].name); - for (temp = 0; *modes[type].lookup[temp] != '\n'; ++temp) { + for (temp = 0; modes[type].lookup[temp].front() != '\n'; ++temp) { if (!(temp % 4)) strcat(buf, "\n"); - sprintf(buf, "%s%-16s", buf, modes[type].lookup[temp]); + sprintf(buf, "%s%-16s", buf, modes[type].lookup[temp].data()); } char_printf(ch, "{}\n", buf); return false; @@ -1422,7 +1423,7 @@ ACMD(do_osearch) { std::string vbuf; // How many extra spaces do we need to pad the title to OBJ_TITLE_LENGTH? std::string short_desc = ellipsis(obj->short_description, OBJ_TITLE_LENGTH); - briefmoney(buf, 6, GET_OBJ_COST(obj)); + std::string money_str = briefmoney(6, GET_OBJ_COST(obj)); vbuf = fmt::format("{:4d}. [{}{:5d}{}] {:<{}} {}{:<3d}&0 {}{:>4}&0 {:<{}}&0 ", ++found, grn, obj_index[nr].vnum, nrm, short_desc, OBJ_TITLE_LENGTH + count_color_chars(short_desc) * 2, @@ -1437,8 +1438,8 @@ ACMD(do_osearch) { : GET_OBJ_LEVEL(obj) > 49 ? "&2" : "", GET_OBJ_LEVEL(obj), GET_OBJ_EFFECTIVE_WEIGHT(obj) > 9999 ? "&5" : "", - GET_OBJ_EFFECTIVE_WEIGHT(obj) > 9999 ? 9999 : GET_OBJ_EFFECTIVE_WEIGHT(obj), buf, - 5 + count_color_chars(buf)); + GET_OBJ_EFFECTIVE_WEIGHT(obj) > 9999 ? 9999 : GET_OBJ_EFFECTIVE_WEIGHT(obj), money_str, + 5 + count_color_chars(money_str.c_str())); switch (subcmd) { case SCMD_VWEAR: if ((GET_OBJ_TYPE(obj) == ITEM_ARMOR || GET_OBJ_TYPE(obj) == ITEM_TREASURE) && @@ -1446,8 +1447,8 @@ ACMD(do_osearch) { vbuf += fmt::format(" {:d}ac", GET_OBJ_VAL(obj, VAL_ARMOR_AC)); for (temp = 0; temp < MAX_OBJ_APPLIES; ++temp) if (obj->applies[temp].modifier) { - sprinttype(obj->applies[temp].location, apply_abbrevs, buf); - vbuf += fmt::format("{}{}", obj->applies[temp].modifier, buf); + vbuf += fmt::format("{}{}", obj->applies[temp].modifier, + sprinttype(obj->applies[temp].location, apply_abbrevs)); } break; case SCMD_VLIST: @@ -1575,19 +1576,23 @@ ACMD(do_vwear) { return; } - strcpy(buf, - "Usage: vwear <position> [<field> <query>] [[from] <start_vnum> " - "[to] [<end_vnum>]]\n" - "Possible positions are:"); - for (i = 0; *wear_bits[i] != '\n'; ++i) - sprintf(buf, "%s%s%-15s", buf, !(i % 5) ? "\n" : "", lowercase(wear_bits[i])); - char_printf(ch, strcat(buf, "\n")); + std::string output = + "Usage: vwear <position> [<field> <query>] [[from] <start_vnum> [to] [<end_vnum>]]\n" + "Possible positions are:"; + for (i = 0; wear_bits[i].front() != '\n'; ++i) + output += fmt::format("{}{}", !(i % 5) ? "\n" : "", wear_bits[i]); + output += "\n"; + char_printf(ch, output); } const struct VSearchType vsearch_room_modes[] = { {1, "name", STRING}, /* must be first */ - {1, "title", STRING}, {2, "sector", STRING}, {3, "description", STRING}, - {4, "extra", STRING}, {5, "flags", FLAGS, room_bits}, {6, "triggervnum", INTEGER}, + {1, "title", STRING}, + {2, "sector", STRING}, + {3, "description", STRING}, + {4, "extra", STRING}, + {5, "flags", FLAGS, room_bits.data()}, + {6, "triggervnum", INTEGER}, {0, nullptr, 0}, }; @@ -1624,14 +1629,14 @@ ACMD(do_rsearch) { match = true; break; case 1: - match = string_find(string, world[nr].name, compare); + match = string_find(string, world[nr].name.c_str(), compare); break; case 2: /*match = string_find(string, sectors[SECT(nr)].name, *compare); */ match = string_start(string, sectors[SECT(nr)].name); break; case 3: - match = string_find(string, world[nr].description, compare); + match = string_find(string, world[nr].description.c_str(), compare); break; case 4: match = check_extra_descs(world[nr].ex_description, string); @@ -1723,7 +1728,7 @@ ACMD(do_esearch) { match = (exit->general_description && string_find(string, exit->general_description, compare)); break; case 3: - match = (IS_SET(exit->exit_info, flags[0]) != 0); + match = (exit->exit_info & flags[0]) != 0; break; case 4: match = numeric_compare(exit->key, value, bound, compare); @@ -1741,16 +1746,17 @@ ACMD(do_esearch) { "----- ----- ---------------------------- " "-----------------------\n"); } - if (!exit->keyword) - *buf = '\0'; - else if (exit->key != NOTHING) - sprintf(buf, "%s%s%s (key %d): ", grn, exit->keyword, nrm, exit->key); - else - sprintf(buf, "%s%s%s: ", grn, exit->keyword, nrm); - sprintbit(exit->exit_info, exit_bits, buf + strlen(buf)); - buf[strlen(buf) - 1] = '\0'; /* remove trailing space */ + std::string exit_info; + if (exit->keyword) { + if (exit->key != NOTHING) { + exit_info = fmt::format("{}{}{} (key {}): ", grn, exit->keyword, nrm, exit->key); + } else { + exit_info = fmt::format("{}{}{}: ", grn, exit->keyword, nrm); + } + } paging_printf(ch, "{:4d}. {}{:<4s}{} at [{}{:5d}{}] {:<20s} [{}]\n", ++found, yel, - capitalize(dirs[dir]), nrm, grn, world[nr].vnum, nrm, world[nr].name, buf); + capitalize_first(dirs[dir]), nrm, grn, world[nr].vnum, nrm, world[nr].name, + sprintbit(exit->exit_info, exit_bits)); } } } @@ -1807,7 +1813,7 @@ ACMD(do_ssearch) { for (temp = 0; SHOP_ROOM(nr, temp) != NOWHERE; ++temp) { value = real_room(SHOP_ROOM(nr, temp)); if (value != NOWHERE) - if (string_find(string, world[value].name, compare)) + if (string_find(string, world[value].name.c_str(), compare)) match = true; } break; @@ -1886,28 +1892,25 @@ const struct VSearchType vsearch_trigger_modes[] = { char *t_listdisplay(int nr, int index) { static char tbuf[MAX_INPUT_LENGTH]; - char tbuf2[MAX_INPUT_LENGTH]; + std::string tbuf2; TrigData *trig; trig = trig_index[nr]->proto; switch (trig_index[nr]->proto->attach_type) { case OBJ_TRIGGER: - strcpy(tbuf2, "OBJ "); - sprintbit(GET_TRIG_TYPE(trig), otrig_types, tbuf2 + 4); + tbuf2 = "OBJ " + sprintbit(GET_TRIG_TYPE(trig), otrig_types); break; case WLD_TRIGGER: - strcpy(tbuf2, "WLD "); - sprintbit(GET_TRIG_TYPE(trig), wtrig_types, tbuf2 + 4); + tbuf2 = "WLD " + sprintbit(GET_TRIG_TYPE(trig), wtrig_types); break; case MOB_TRIGGER: - strcpy(tbuf2, "MOB "); - sprintbit(GET_TRIG_TYPE(trig), trig_types, tbuf2 + 4); + tbuf2 = "MOB " + sprintbit(GET_TRIG_TYPE(trig), trig_types); break; default: - sprintf(tbuf2, "%s???%s ", red, nrm); + tbuf2 = fmt::format("{}???{}", red, nrm); } snprintf(tbuf, MAX_INPUT_LENGTH, "%4d. [%s%5d%s] %-40.40s %s\n", index, grn, trig_index[nr]->vnum, nrm, - trig_index[nr]->proto->name, tbuf2); + trig_index[nr]->proto->name, tbuf2.c_str()); return tbuf; } @@ -2003,7 +2006,7 @@ ACMD(do_tsearch) { char_printf(ch, "No matches found.\n"); } -const char *zone_reset_modes[] = {"never", "empty", "normal", "\n"}; +const std::string_view zone_reset_modes[] = {"never", "empty", "normal", "\n"}; const struct VSearchType vsearch_zone_modes[] = { {1, "name", STRING}, /* must be first */ @@ -2128,9 +2131,9 @@ ACMD(do_zsearch) { "--------\n"); } ++found; - sprinttype(zone->reset_mode, zone_reset_modes, buf); + paging_printf(ch, "{:3d} {:<30s} {:3d} {:<6s} {:3d} {:6d} {:5d}\n", zone->number, zone->name, zone->age, - buf, zone->lifespan, zone->zone_factor, zone->top); + sprinttype(zone->reset_mode, zone_reset_modes), zone->lifespan, zone->zone_factor, zone->top); } } if (found) @@ -2143,7 +2146,7 @@ ACMD(do_zsearch) { #define DOOR_RESET_CLOSED (1 << 1) #define DOOR_RESET_LOCKED (1 << 2) #define DOOR_RESET_HIDDEN (1 << 3) -const char *door_reset_modes[] = {"open", "closed", "locked", "hidden", "\n"}; +const std::string_view door_reset_modes[] = {"open", "closed", "locked", "hidden", "\n"}; const struct VSearchType vsearch_zone_command_modes[] = {{1, "mobile", INTEGER}, {2, "object", INTEGER}, @@ -2327,7 +2330,7 @@ ACMD(do_csearch) { cmd_mob != NOWHERE && cmd_mob < top_of_mobt ? mob_proto[cmd_mob].player.short_descr : "a mob", grn, cmd_mob != NOWHERE && cmd_mob < top_of_mobt ? mob_index[cmd_mob].vnum : -1, nrm, - equipment_types[com->arg3], com->arg2); + equipment_types[com->arg3].data(), com->arg2); break; case 'P': sprintf(vbuf + vbuflen, "Put %s (%s%d%s) in %s (%s%d%s), Max: %d\n", @@ -2339,7 +2342,7 @@ ACMD(do_csearch) { grn, obj_index[com->arg2].vnum, nrm); break; case 'D': - sprintf(vbuf + vbuflen, "Set door %s as %s\n", dirs[com->arg2], + sprintf(vbuf + vbuflen, "Set door %s as %s\n", dirs[com->arg2].data(), com->arg3 ? (com->arg3 == 1 ? "closed" @@ -2516,13 +2519,13 @@ ACMD(do_ksearch) { break; } if (skill->routines) { - sprintbit(skill->routines, routines, buf1); + strcpy(buf1, sprintbit(skill->routines, routines).c_str()); if (strlen(buf1) > 12) strcpy(buf1 + 9, "..."); } else strcpy(buf1, "NONE"); if (skill->targets) { - sprintbit(skill->targets, targets, buf2); + strcpy(buf2, sprintbit(skill->targets, targets).c_str()); if (strlen(buf2) > 12) strcpy(buf2 + 9, "..."); } else diff --git a/src/weather.cpp b/src/weather.cpp index 91f4f58f..dd266011 100644 --- a/src/weather.cpp +++ b/src/weather.cpp @@ -27,27 +27,6 @@ * the game boots (and are updated as the game runs). */ -const char *wind_speeds[] = {"", "&6breeze", "&6strong wind", "&4gale-force wind", "&4hurricane-strength &0&6wind", - "\n"}; - -const char *precip[] = {"&6&brain", "&7&bsnow", "\n"}; - -const char *daylight_change[] = { - "&9&bThe night has begun.&0\n", - "&6&bThe &3sun &6rises in the east.&0\n", - "&6&bThe day has begun.&0\n", - "&5&bThe &3&bsun &5slowly disapp&0&5ears in th&9&be west.&0\n", -}; - -const char *seasons[] = {"winter", "spring", "summer", "autumn", "\n"}; - -const char *season_change[] = { - "&7&bWinter takes hold as &0&3Autumn&0 &7&bfades into history...&0\n", - "&2&bThe bite of &7&bWinter &2is gone as &3Spring &2begins.&0\n", - "Spring gives way to Summer.\n", - "Summer passes and Autumn begins.\n", -}; - HemisphereData hemispheres[NUM_HEMISPHERES] = { {"Northwestern", SUN_DARK, WINTER}, {"Northeastern", SUN_LIGHT, SUMMER}, @@ -221,15 +200,16 @@ char *wind_message(int current, int original) { if (current == WIND_NONE) strcpy(buf, "&6The air is calm.&0\n"); else - sprintf(buf, "&6A &0%s&0 &6begins to blow around you.&0\n", wind_speeds[current]); + sprintf(buf, "&6A &0%s&0 &6begins to blow around you.&0\n", wind_speeds[current].data()); } else if (current > original) - sprintf(buf, "&6The &7%s &6increases to a &7%s.&0\n", wind_speeds[original], wind_speeds[current]); + sprintf(buf, "&6The &7%s &6increases to a &7%s.&0\n", wind_speeds[original].data(), + wind_speeds[current].data()); else if (current == original) - sprintf(buf, "&6A &7%s &6is blowing around you.&0\n", wind_speeds[current]); + sprintf(buf, "&6A &7%s &6is blowing around you.&0\n", wind_speeds[current].data()); else if (current != WIND_NONE) - sprintf(buf, "&6The &7%s &6subsides to a &7%s.&0\n", wind_speeds[original], wind_speeds[current]); + sprintf(buf, "&6The &7%s &6subsides to a &7%s.&0\n", wind_speeds[original].data(), wind_speeds[current].data()); else - sprintf(buf, "&6The &7%s &6calms and the air becomes still.&0\n", wind_speeds[original]); + sprintf(buf, "&6The &7%s &6calms and the air becomes still.&0\n", wind_speeds[original].data()); return buf; } @@ -350,13 +330,13 @@ void update_temperature(int zone_rnum) { char *precipitation_message(ZoneData *zone, int original) { if (original > PRECIP_GRAY_CLOUDS) { if (zone->precipitation > original) - sprintf(buf, "&4It starts %sing harder.&0\n", GET_PRECIP_TYPE(zone)); + sprintf(buf, "&4It starts %sing harder.&0\n", GET_PRECIP_TYPE(zone).data()); else if (zone->precipitation == original) - sprintf(buf, "&5It continues to %s.&0\n", GET_PRECIP_TYPE(zone)); + sprintf(buf, "&5It continues to %s.&0\n", GET_PRECIP_TYPE(zone).data()); else if (zone->precipitation > PRECIP_GRAY_CLOUDS) - sprintf(buf, "&5The %s &5starts coming down a little lighter.&0\n", GET_PRECIP_TYPE(zone)); + sprintf(buf, "&5The %s &5starts coming down a little lighter.&0\n", GET_PRECIP_TYPE(zone).data()); else - sprintf(buf, "&5It continues to %s.&0\n", GET_PRECIP_TYPE(zone)); + sprintf(buf, "&5It continues to %s.&0\n", GET_PRECIP_TYPE(zone).data()); } else if (original) { if (zone->precipitation <= PRECIP_GRAY_CLOUDS) { switch (original) { @@ -377,7 +357,7 @@ char *precipitation_message(ZoneData *zone, int original) { return "NULL PRECIPITATION\n"; } } else if (zone->precipitation > PRECIP_GRAY_CLOUDS) - sprintf(buf, "&9&bIt begins to %s.&0\n", GET_PRECIP_TYPE(zone)); + sprintf(buf, "&9&bIt begins to %s.&0\n", GET_PRECIP_TYPE(zone).data()); } else if (zone->precipitation) strcpy(buf, "&4Small &7&bbil&0&7low&bing white &7c&0&7l&6ou&7d&bs&0 " diff --git a/src/weather.hpp b/src/weather.hpp index 6d2e71f7..1b7e74fe 100644 --- a/src/weather.hpp +++ b/src/weather.hpp @@ -134,10 +134,25 @@ struct ClimateData { int allowed_disasters; }; -extern const char *wind_speeds[]; -extern const char *precip[]; -extern const char *daylight_change[]; -extern const char *seasons[]; -extern const char *season_change[]; +constexpr std::string_view wind_speeds[] = { + "", "&6breeze", "&6strong wind", "&4gale-force wind", "&4hurricane-strength &0&6wind", "\n"}; + +constexpr std::string_view precip[] = {"&6&brain", "&7&bsnow", "\n"}; + +constexpr std::string_view daylight_change[] = { + "&9&bThe night has begun.&0\n", + "&6&bThe &3sun &6rises in the east.&0\n", + "&6&bThe day has begun.&0\n", + "&5&bThe &3&bsun &5slowly disapp&0&5ears in th&9&be west.&0\n", +}; + +constexpr std::string_view seasons[] = {"winter", "spring", "summer", "autumn", "\n"}; + +constexpr std::string_view season_change[] = { + "&7&bWinter takes hold as &0&3Autumn&0 &7&bfades into history...&0\n", + "&2&bThe bite of &7&bWinter &2is gone as &3Spring &2begins.&0\n", + "Spring gives way to Summer.\n", + "Summer passes and Autumn begins.\n", +}; extern HemisphereData hemispheres[NUM_HEMISPHERES]; extern ClimateData climates[NUM_CLIMATES]; \ No newline at end of file diff --git a/src/zedit.cpp b/src/zedit.cpp index 3e99c604..b9998900 100644 --- a/src/zedit.cpp +++ b/src/zedit.cpp @@ -298,7 +298,8 @@ void zedit_new_zone(CharData *ch, int vzone_num) { * That quirk has been fixed with the std::max() statement. */ - log(LogSeverity::Warn, std::max(LVL_GOD, GET_INVIS_LEV(ch)), "OLC: {} creates new zone #{:d}", GET_NAME(ch), vzone_num); + log(LogSeverity::Warn, std::max(LVL_GOD, GET_INVIS_LEV(ch)), "OLC: {} creates new zone #{:d}", GET_NAME(ch), + vzone_num); char_printf(ch, "Zone created successfully.\n"); return; @@ -525,7 +526,7 @@ void zedit_save_to_disk(int zone_num) { arg2 = ZCMD.arg2; arg3 = ZCMD.arg3; /*arg4 = ZCMD.arg4; */ - comment = world[ZCMD.arg1].name; + comment = strdup(world[ZCMD.arg1].name.c_str()); break; case 'R': arg1 = world[ZCMD.arg1].vnum; @@ -689,9 +690,9 @@ void delete_command(DescriptorData *d, int pos) { if ((pos >= subcmd) || (pos < 0)) return; - /* - * Ok, let's zap it - */ + /* + * Ok, let's zap it + */ #if defined(DEBUG) log("delete_command called remove_cmd_from_list."); #endif @@ -788,7 +789,7 @@ void zedit_disp_menu(DescriptorData *d) { case 'E': sprintf(buf2, "%sEquip with %s [%s%d%s], %s, Max : %d", MYCMD.if_flag ? " then " : "", obj_proto[MYCMD.arg1].short_description, cyn, obj_index[MYCMD.arg1].vnum, yel, - equipment_types[MYCMD.arg3], MYCMD.arg2); + equipment_types[MYCMD.arg3].data(), MYCMD.arg2); break; case 'P': sprintf(buf2, "%sPut %s [%s%d%s] in %s [%s%d%s], Max : %d", MYCMD.if_flag ? " then " : "", @@ -800,7 +801,7 @@ void zedit_disp_menu(DescriptorData *d) { obj_proto[MYCMD.arg2].short_description, cyn, obj_index[MYCMD.arg2].vnum, yel); break; case 'D': - sprintf(buf2, "%sSet door %s as %s.", MYCMD.if_flag ? " then " : "", dirs[MYCMD.arg2], + sprintf(buf2, "%sSet door %s as %s.", MYCMD.if_flag ? " then " : "", dirs[MYCMD.arg2].data(), MYCMD.arg3 ? ((MYCMD.arg3 == 1) ? "closed" @@ -920,9 +921,8 @@ void zedit_disp_arg2(DescriptorData *d) { char_printf(d->character, "Input the maximum number that can exist on the mud (max 50):\n"); break; case 'D': - while (*dirs[i] != '\n') { - sprintf(buf, "%d) Exit %s.\n", i, dirs[i]); - char_printf(d->character, buf); + while (dirs[i].front() != '\n') { + char_printf(d->character, "{}) Exit {}.\n", i, dirs[i]); i++; } char_printf(d->character, "Enter exit number for door:\n"); @@ -958,11 +958,11 @@ void zedit_disp_arg3(DescriptorData *d) { zedit_disp_sarg(d); break; case 'E': - while (*equipment_types[i] != '\n') { - sprintf(buf, "%2d) %26.26s %2d) %26.26s\n", i, equipment_types[i], i + 1, - (*equipment_types[i + 1] != '\n') ? equipment_types[i + 1] : ""); + while (equipment_types[i].front() != '\n') { + sprintf(buf, "%2d) %26.26s %2d) %26.26s\n", i, equipment_types[i].data(), i + 1, + (equipment_types[i + 1].front() != '\n') ? equipment_types[i + 1].data() : ""); char_printf(d->character, buf); - if (*equipment_types[i + 1] != '\n') + if (equipment_types[i + 1].front() != '\n') i += 2; else break; @@ -1040,8 +1040,8 @@ void zedit_parse(DescriptorData *d, char *arg) { /*. Save the zone in memory . */ char_printf(d->character, "Saving zone info in memory.\n"); zedit_save_internally(d); - log(LogSeverity::Debug, std::max(LVL_GOD, GET_INVIS_LEV(d->character)), "OLC: {} edits zone info for room {:d}.", - GET_NAME(d->character), OLC_NUM(d)); + log(LogSeverity::Debug, std::max(LVL_GOD, GET_INVIS_LEV(d->character)), + "OLC: {} edits zone info for room {:d}.", GET_NAME(d->character), OLC_NUM(d)); /* FALL THROUGH */ case 'n': case 'N': @@ -1330,7 +1330,7 @@ void zedit_parse(DescriptorData *d, char *arg) { /* * Count directions. */ - while (*dirs[i] != '\n') + while (dirs[i].front() != '\n') i++; if ((pos < 0) || (pos > i)) char_printf(d->character, "Try again:\n"); @@ -1373,7 +1373,7 @@ void zedit_parse(DescriptorData *d, char *arg) { * Count number of wear positions. We could use NUM_WEARS, this is * more reliable. */ - while (*equipment_types[i] != '\n') + while (equipment_types[i].front() != '\n') i++; if ((pos < 0) || (pos > i)) char_printf(d->character, "Try again:\n"); @@ -1496,7 +1496,8 @@ void zedit_parse(DescriptorData *d, char *arg) { if (OLC_ZNUM(d) == top_of_zone_table) OLC_ZONE(d)->top = std::max(OLC_ZNUM(d) * 100, std::min(198999, atoi(arg))); else - OLC_ZONE(d)->top = std::max(OLC_ZNUM(d) * 100, std::min(zone_table[OLC_ZNUM(d) + 1].number * 100, atoi(arg))); + OLC_ZONE(d)->top = + std::max(OLC_ZNUM(d) * 100, std::min(zone_table[OLC_ZNUM(d) + 1].number * 100, atoi(arg))); zedit_disp_menu(d); break; /*-------------------------------------------------------------------*/ diff --git a/test_enum.cpp b/test_enum.cpp new file mode 100644 index 00000000..29c108a1 --- /dev/null +++ b/test_enum.cpp @@ -0,0 +1,7 @@ +#include "src/clan.hpp" +#include <iostream> +int main() { + std::cout << "enum_name: " << magic_enum::enum_name(ClanPermission::KICK_MEMBERS) << std::endl; + std::cout << "fmt::format result: " << fmt::format("You need the {} permission to do that.", magic_enum::enum_name(ClanPermission::KICK_MEMBERS)) << std::endl; + return 0; +} diff --git a/test_fuzzy.cpp b/test_fuzzy.cpp new file mode 100644 index 00000000..05899641 --- /dev/null +++ b/test_fuzzy.cpp @@ -0,0 +1,36 @@ +#include "src/function_registration.hpp" +#include "src/arguments.hpp" +#include "src/structs.hpp" +#include <iostream> + +void test_command(CharData *ch, Arguments args) { + std::cout << "Test command executed!" << std::endl; +} + +void clan_members_test(CharData *ch, Arguments args) { + std::cout << "clan_members command executed!" << std::endl; +} + +int main() { + // Register some test functions + FunctionRegistry::register_function("test_command", test_command, 0); + FunctionRegistry::register_function("clan_members", clan_members_test, 0, CommandCategory::CLAN); + + // Create a dummy character for testing + CharData dummy_ch{}; + Arguments dummy_args(""); + + std::cout << "Testing exact match: 'clan_members'" << std::endl; + FunctionRegistry::call_by_abbrev("clan_members", &dummy_ch, dummy_args); + + std::cout << "Testing prefix match: 'clan_memb'" << std::endl; + FunctionRegistry::call_by_abbrev("clan_memb", &dummy_ch, dummy_args); + + std::cout << "Testing fuzzy match: 'clan_memnber'" << std::endl; + FunctionRegistry::call_by_abbrev("clan_memnber", &dummy_ch, dummy_args); + + std::cout << "Testing short fuzzy match (should fail): 'abc'" << std::endl; + FunctionRegistry::call_by_abbrev("abc", &dummy_ch, dummy_args); + + return 0; +} \ No newline at end of file diff --git a/tests/test_arguments.cpp b/tests/test_arguments.cpp new file mode 100644 index 00000000..0499db98 --- /dev/null +++ b/tests/test_arguments.cpp @@ -0,0 +1,362 @@ +#include <catch2/catch_test_macros.hpp> +#include "arguments.hpp" + +TEST_CASE("Arguments basic functionality", "[arguments]") { + SECTION("Construction and get()") { + Arguments args1("hello world test"); + REQUIRE(args1.get() == "hello world test"); + + Arguments args2{"another test"}; + REQUIRE(args2.get() == "another test"); + + Arguments args3(""); + REQUIRE(args3.get() == ""); + REQUIRE(args3.empty()); + } + + SECTION("Trimming whitespace") { + Arguments args(" hello world "); + REQUIRE(args.get() == "hello world"); + REQUIRE_FALSE(args.empty()); + + Arguments args_spaces(" "); + REQUIRE(args_spaces.get() == ""); + REQUIRE(args_spaces.empty()); + } +} + +TEST_CASE("Arguments shift() functionality", "[arguments]") { + SECTION("Basic word shifting") { + Arguments args("hello world test"); + + auto word1 = args.shift(); + REQUIRE(word1 == "hello"); + REQUIRE(args.get() == "world test"); + + auto word2 = args.shift(); + REQUIRE(word2 == "world"); + REQUIRE(args.get() == "test"); + + auto word3 = args.shift(); + REQUIRE(word3 == "test"); + REQUIRE(args.get() == ""); + REQUIRE(args.empty()); + + auto word4 = args.shift(); + REQUIRE(word4 == ""); + } + + SECTION("Quoted string handling") { + Arguments args("\"hello world\" test 'single quoted'"); + + auto quoted1 = args.shift(); + REQUIRE(quoted1 == "hello world"); + REQUIRE(args.get() == "test 'single quoted'"); + + auto word = args.shift(); + REQUIRE(word == "test"); + + auto quoted2 = args.shift(); + REQUIRE(quoted2 == "single quoted"); + } + + SECTION("Unclosed quotes") { + Arguments args("\"unclosed quote test"); + auto result = args.shift(); + REQUIRE(result == "unclosed quote test"); + REQUIRE(args.empty()); + } +} + +TEST_CASE("Arguments command_shift() functionality", "[arguments]") { + SECTION("Non-alpha character extraction") { + Arguments args(";hello world"); + auto cmd = args.command_shift(); + REQUIRE(cmd == ";"); + REQUIRE(args.get() == "hello world"); + + Arguments args2(".test command"); + auto cmd2 = args2.command_shift(); + REQUIRE(cmd2 == "."); + REQUIRE(args2.get() == "test command"); + } + + SECTION("Alpha character - non-strict mode") { + Arguments args("hello world"); + auto cmd = args.command_shift(false); + REQUIRE(cmd == "hello"); + REQUIRE(args.get() == "world"); + } + + SECTION("Alpha character - strict mode") { + Arguments args("hello world"); + auto cmd = args.command_shift(true); + REQUIRE(cmd == ""); + REQUIRE(args.get() == "hello world"); // Should not consume + } +} + +TEST_CASE("Arguments try_shift_number() - NEW BEHAVIOR", "[arguments]") { + SECTION("Successful number parsing - should consume") { + Arguments args("123 hello world"); + auto result = args.try_shift_number(); + + REQUIRE(result.has_value()); + REQUIRE(result.value() == 123); + REQUIRE(args.get() == "hello world"); // Should be consumed + } + + SECTION("Failed parsing - should NOT consume") { + Arguments args("hello 123 world"); + auto result = args.try_shift_number(); + + REQUIRE_FALSE(result.has_value()); + REQUIRE(args.get() == "hello 123 world"); // Should NOT be consumed + } + + SECTION("Empty arguments - should NOT consume") { + Arguments args(""); + auto result = args.try_shift_number(); + + REQUIRE_FALSE(result.has_value()); + REQUIRE(args.get() == ""); + REQUIRE(args.empty()); + } + + SECTION("Whitespace only - should NOT consume") { + Arguments args(" "); + auto result = args.try_shift_number(); + + REQUIRE_FALSE(result.has_value()); + REQUIRE(args.get() == ""); + } + + SECTION("Zero - should consume") { + Arguments args("0 test"); + auto result = args.try_shift_number(); + + REQUIRE(result.has_value()); + REQUIRE(result.value() == 0); + REQUIRE(args.get() == "test"); + } + + SECTION("Negative number - behavior depends on is_integer implementation") { + Arguments args("-123 test"); + auto result = args.try_shift_number(); + + // The behavior here depends on how is_integer handles negative numbers + // If it rejects them, the argument should not be consumed + if (!result.has_value()) { + REQUIRE(args.get() == "-123 test"); // Should not be consumed + } else { + REQUIRE(result.value() == -123); + REQUIRE(args.get() == "test"); // Should be consumed + } + } + + SECTION("Partial number - should NOT consume") { + Arguments args("123abc hello"); + auto result = args.try_shift_number(); + + REQUIRE_FALSE(result.has_value()); + REQUIRE(args.get() == "123abc hello"); // Should NOT be consumed + } +} + +TEST_CASE("Arguments try_shift_number_and_arg() - NEW BEHAVIOR", "[arguments]") { + SECTION("Valid number.item format - should consume") { + Arguments args("3.sword hello world"); + auto result = args.try_shift_number_and_arg(); + + REQUIRE(result.has_value()); + REQUIRE(result->first == 3); + REQUIRE(result->second == "sword"); + REQUIRE(args.get() == "hello world"); // Should be consumed + } + + SECTION("'all.item' format - should consume") { + Arguments args("all.potion rest"); + auto result = args.try_shift_number_and_arg(); + + REQUIRE(result.has_value()); + REQUIRE(result->first == Arguments::MAX_ITEMS); + REQUIRE(result->second == "potion"); + REQUIRE(args.get() == "rest"); // Should be consumed + } + + SECTION("No dot format - should NOT consume") { + Arguments args("sword hello world"); + auto result = args.try_shift_number_and_arg(); + + REQUIRE_FALSE(result.has_value()); + REQUIRE(args.get() == "sword hello world"); // Should NOT be consumed + } + + SECTION("Invalid number format - should NOT consume") { + Arguments args("abc.sword hello"); + auto result = args.try_shift_number_and_arg(); + + REQUIRE_FALSE(result.has_value()); + REQUIRE(args.get() == "abc.sword hello"); // Should NOT be consumed + } + + SECTION("Empty number part - should NOT consume") { + Arguments args(".sword hello"); + auto result = args.try_shift_number_and_arg(); + + REQUIRE_FALSE(result.has_value()); + REQUIRE(args.get() == ".sword hello"); // Should NOT be consumed + } + + SECTION("Empty item part - should NOT consume") { + Arguments args("3. hello"); + auto result = args.try_shift_number_and_arg(); + + REQUIRE_FALSE(result.has_value()); + REQUIRE(args.get() == "3. hello"); // Should NOT be consumed + } + + SECTION("Just a dot - should NOT consume") { + Arguments args(". hello"); + auto result = args.try_shift_number_and_arg(); + + REQUIRE_FALSE(result.has_value()); + REQUIRE(args.get() == ". hello"); // Should NOT be consumed + } + + SECTION("Multiple dots - should consume first part") { + Arguments args("5.item.name hello"); + auto result = args.try_shift_number_and_arg(); + + REQUIRE(result.has_value()); + REQUIRE(result->first == 5); + REQUIRE(result->second == "item.name"); + REQUIRE(args.get() == "hello"); // Should be consumed + } + + SECTION("Zero count - should consume") { + Arguments args("0.sword hello"); + auto result = args.try_shift_number_and_arg(); + + REQUIRE(result.has_value()); + REQUIRE(result->first == 0); + REQUIRE(result->second == "sword"); + REQUIRE(args.get() == "hello"); + } +} + +TEST_CASE("Arguments shift_clean() functionality", "[arguments]") { + SECTION("Double dollar replacement") { + Arguments args("test$$string hello$$world"); + auto result = args.shift_clean(); + REQUIRE(result == "test$string"); + REQUIRE(args.get() == "hello$$world"); + + auto result2 = args.shift_clean(); + REQUIRE(result2 == "hello$world"); + } + + SECTION("No double dollars") { + Arguments args("normal string"); + auto result = args.shift_clean(); + REQUIRE(result == "normal"); + REQUIRE(args.get() == "string"); + } +} + +TEST_CASE("Arguments edge cases", "[arguments]") { + SECTION("Multiple consecutive spaces") { + Arguments args("hello world test"); + auto word1 = args.shift(); + REQUIRE(word1 == "hello"); + REQUIRE(args.get() == "world test"); + + auto word2 = args.shift(); + REQUIRE(word2 == "world"); + REQUIRE(args.get() == "test"); + } + + SECTION("Leading and trailing spaces with shifts") { + Arguments args(" hello world "); + REQUIRE(args.get() == "hello world"); + + auto word1 = args.shift(); + REQUIRE(word1 == "hello"); + REQUIRE(args.get() == "world"); + + auto word2 = args.shift(); + REQUIRE(word2 == "world"); + REQUIRE(args.empty()); + } + + SECTION("Only quotes") { + Arguments args("\"\" ''"); + auto empty1 = args.shift(); + REQUIRE(empty1 == ""); + + auto empty2 = args.shift(); + REQUIRE(empty2 == ""); + + REQUIRE(args.empty()); + } +} + +TEST_CASE("Arguments state preservation on failed try_shift operations", "[arguments]") { + SECTION("Multiple failed try_shift_number calls should not affect state") { + Arguments args("hello world 123"); + + // First failed attempt + auto result1 = args.try_shift_number(); + REQUIRE_FALSE(result1.has_value()); + REQUIRE(args.get() == "hello world 123"); + + // Second failed attempt on same args + auto result2 = args.try_shift_number(); + REQUIRE_FALSE(result2.has_value()); + REQUIRE(args.get() == "hello world 123"); + + // Should still be able to shift normally + auto word = args.shift(); + REQUIRE(word == "hello"); + REQUIRE(args.get() == "world 123"); + + // Now try_shift_number should still fail + auto result3 = args.try_shift_number(); + REQUIRE_FALSE(result3.has_value()); + REQUIRE(args.get() == "world 123"); + + // Skip to the number + [[maybe_unused]] auto consumed = args.shift(); // consume "world" + auto result4 = args.try_shift_number(); + REQUIRE(result4.has_value()); + REQUIRE(result4.value() == 123); + REQUIRE(args.empty()); + } + + SECTION("Mixed successful and failed operations") { + Arguments args("123 hello 456.sword world"); + + // Successful number shift + auto num1 = args.try_shift_number(); + REQUIRE(num1.has_value()); + REQUIRE(num1.value() == 123); + REQUIRE(args.get() == "hello 456.sword world"); + + // Failed number shift + auto num2 = args.try_shift_number(); + REQUIRE_FALSE(num2.has_value()); + REQUIRE(args.get() == "hello 456.sword world"); + + // Successful regular shift + auto word = args.shift(); + REQUIRE(word == "hello"); + REQUIRE(args.get() == "456.sword world"); + + // Successful number_and_arg shift + auto num_item = args.try_shift_number_and_arg(); + REQUIRE(num_item.has_value()); + REQUIRE(num_item->first == 456); + REQUIRE(num_item->second == "sword"); + REQUIRE(args.get() == "world"); + } +} \ No newline at end of file diff --git a/tests/test_clan_basic.cpp b/tests/test_clan_basic.cpp new file mode 100644 index 00000000..3cf4a1d9 --- /dev/null +++ b/tests/test_clan_basic.cpp @@ -0,0 +1,129 @@ +#include <catch2/catch_test_macros.hpp> +#include <catch2/catch_session.hpp> + +int main(int argc, char* argv[]) { + return Catch::Session().run(argc, argv); +} + +#include "clan.hpp" + +TEST_CASE("Basic Clan Test", "[clan]") { + SECTION("ClanRank creation and basic operations") { + PermissionSet perms; + perms.set(static_cast<size_t>(ClanPermission::CLAN_CHAT)); + perms.set(static_cast<size_t>(ClanPermission::SET_DESCRIPTION)); + + ClanRank rank("Test Rank", perms); + + REQUIRE(rank.title() == "Test Rank"); + REQUIRE(rank.has_permission(ClanPermission::CLAN_CHAT)); + REQUIRE(rank.has_permission(ClanPermission::SET_DESCRIPTION)); + REQUIRE_FALSE(rank.has_permission(ClanPermission::INVITE_MEMBERS)); + + // Test permission modification + rank.add_permission(ClanPermission::INVITE_MEMBERS); + REQUIRE(rank.has_permission(ClanPermission::INVITE_MEMBERS)); + + rank.remove_permission(ClanPermission::CLAN_CHAT); + REQUIRE_FALSE(rank.has_permission(ClanPermission::CLAN_CHAT)); + + // Test set_permission + rank.set_permission(ClanPermission::STORE_ITEMS, true); + REQUIRE(rank.has_permission(ClanPermission::STORE_ITEMS)); + + rank.set_permission(ClanPermission::STORE_ITEMS, false); + REQUIRE_FALSE(rank.has_permission(ClanPermission::STORE_ITEMS)); + } + + SECTION("ClanRank comparison") { + PermissionSet perms1; + perms1.set(static_cast<size_t>(ClanPermission::CLAN_CHAT)); + + PermissionSet perms2; + perms2.set(static_cast<size_t>(ClanPermission::SET_DESCRIPTION)); + + ClanRank rank1("Alpha", perms1); + ClanRank rank2("Beta", perms2); + ClanRank rank3("Alpha", perms1); + + REQUIRE(rank1 < rank2); + REQUIRE_FALSE(rank2 < rank1); + REQUIRE(rank1 == rank3); + REQUIRE_FALSE(rank1 == rank2); + } + + SECTION("ClanRepository basic operations") { + // Clear any existing clans + while (clan_repository.count() > 0) { + auto clans = clan_repository.all(); + for (const auto& clan : clans) { + clan_repository.remove(clan->id()); + } + } + + // Create a clan + auto clan = clan_repository.create(1, "Test Clan", "TEST"); + + REQUIRE(clan != nullptr); + REQUIRE(clan->id() == 1); + REQUIRE(clan->name() == "Test Clan"); + REQUIRE(clan->abbreviation() == "TEST"); + REQUIRE(clan->member_count() == 0); + + // Test repository operations + REQUIRE(clan_repository.count() == 1); + + auto found = clan_repository.find_by_id(1); + REQUIRE(found.has_value()); + REQUIRE(found->get()->name() == "Test Clan"); + + found = clan_repository.find_by_name("Test Clan"); + REQUIRE(found.has_value()); + REQUIRE(found->get()->id() == 1); + + found = clan_repository.find_by_abbreviation("TEST"); + REQUIRE(found.has_value()); + REQUIRE(found->get()->id() == 1); + + // Test not found + auto not_found = clan_repository.find_by_id(999); + REQUIRE_FALSE(not_found.has_value()); + + // Clean up + clan_repository.remove(1); + REQUIRE(clan_repository.count() == 0); + } + + SECTION("JSON serialization basic test") { + // Clear any existing clans + while (clan_repository.count() > 0) { + auto clans = clan_repository.all(); + for (const auto& clan : clans) { + clan_repository.remove(clan->id()); + } + } + + // Create a clan with some data + auto clan = clan_repository.create(1, "Test Clan", "TEST"); + + // Test JSON serialization + nlohmann::json j = *clan; + + REQUIRE(j["id"] == 1); + REQUIRE(j["name"] == "Test Clan"); + REQUIRE(j["abbreviation"] == "TEST"); + REQUIRE(j.contains("treasure")); + REQUIRE(j.contains("storage")); + REQUIRE(j.contains("ranks")); + + // Clean up + clan_repository.remove(1); + } + + SECTION("Error handling tests") { + // Test static error strings + REQUIRE(AccessError::ClanNotFound == "Clan not found."); + REQUIRE(AccessError::PermissionDenied == "You do not have permission to do that."); + REQUIRE(AccessError::InvalidOperation == "Invalid operation"); + } +} \ No newline at end of file diff --git a/tests/test_clan_permissions.cpp b/tests/test_clan_permissions.cpp new file mode 100644 index 00000000..3a18e2b8 --- /dev/null +++ b/tests/test_clan_permissions.cpp @@ -0,0 +1,371 @@ +#include <catch2/catch_test_macros.hpp> + +#include "clan.hpp" +#include "chars.hpp" +#include "structs.hpp" +#include "utils.hpp" +#include "logging.hpp" +#include "comm.hpp" +#include "defines.hpp" + +#include <memory> +#include <stdexcept> +#include <string> + +// Test fixture for clan permission testing +class ClanPermissionTestFixture { +public: + ClanPermissionTestFixture() { + // Create a test clan + test_clan = clan_repository.create(1, "TestClan", "TEST"); + + // Create test ranks with different permissions + create_test_ranks(); + + // Create test characters + create_test_characters(); + } + + ~ClanPermissionTestFixture() { + // Clean up + clan_repository.remove(1); + } + +protected: + std::shared_ptr<Clan> test_clan; + std::shared_ptr<CharData> leader_char; + std::shared_ptr<CharData> officer_char; + std::shared_ptr<CharData> member_char; + std::shared_ptr<CharData> clanless_char; + std::shared_ptr<CharData> god_char; + + void init_character_data(CharData* ch, const std::string& name, int level) { + // Initialize minimal character data for testing + ch->player.short_descr = strdup(name.c_str()); + ch->player.level = level; + ch->player_specials = new PlayerSpecialData(); + } + +private: + void create_test_ranks() { + // Leader rank with LEADER_OVERRIDE + PermissionSet leader_perms; + leader_perms.set(static_cast<size_t>(ClanPermission::LEADER_OVERRIDE)); + auto leader_result = test_clan->admin_add_rank(ClanRank("Leader", leader_perms)); + if (!leader_result) { + throw std::runtime_error("Failed to add leader rank: " + leader_result.error()); + } + + // Officer rank with specific permissions + PermissionSet officer_perms; + officer_perms.set(static_cast<size_t>(ClanPermission::CLAN_CHAT)); + officer_perms.set(static_cast<size_t>(ClanPermission::INVITE_MEMBERS)); + officer_perms.set(static_cast<size_t>(ClanPermission::KICK_MEMBERS)); + officer_perms.set(static_cast<size_t>(ClanPermission::PROMOTE_MEMBERS)); + auto officer_result = test_clan->admin_add_rank(ClanRank("Officer", officer_perms)); + if (!officer_result) { + throw std::runtime_error("Failed to add officer rank: " + officer_result.error()); + } + + // Member rank with basic permissions + PermissionSet member_perms; + member_perms.set(static_cast<size_t>(ClanPermission::CLAN_CHAT)); + member_perms.set(static_cast<size_t>(ClanPermission::CLAN_WHO)); + auto member_result = test_clan->admin_add_rank(ClanRank("Member", member_perms)); + if (!member_result) { + throw std::runtime_error("Failed to add member rank: " + member_result.error()); + } + } + + void create_test_characters() { + // Create test characters + leader_char = std::make_shared<CharData>(); + officer_char = std::make_shared<CharData>(); + member_char = std::make_shared<CharData>(); + clanless_char = std::make_shared<CharData>(); + god_char = std::make_shared<CharData>(); + + // Initialize basic character data + init_character_data(leader_char.get(), "TestLeader", 1); + init_character_data(officer_char.get(), "TestOfficer", 1); + init_character_data(member_char.get(), "TestMember", 1); + init_character_data(clanless_char.get(), "TestClanless", 1); + init_character_data(god_char.get(), "TestGod", LVL_IMMORT); + + // Add characters to clan with different ranks + [[maybe_unused]] bool leader_added = test_clan->add_member_by_name("TestLeader", 0); // Leader rank + [[maybe_unused]] bool officer_added = test_clan->add_member_by_name("TestOfficer", 1); // Officer rank + [[maybe_unused]] bool member_added = test_clan->add_member_by_name("TestMember", 2); // Member rank + + // Set clan IDs for members + set_clan_id(leader_char.get(), 1); + set_clan_id(officer_char.get(), 1); + set_clan_id(member_char.get(), 1); + set_clan_id(clanless_char.get(), CLAN_ID_NONE); + set_clan_id(god_char.get(), CLAN_ID_NONE); // Gods don't need clan membership + } +}; + +// Test ClanPermission enum and basic functionality +TEST_CASE("ClanPermission enum values", "[clan][permissions]") { + SECTION("Basic permission values are correct") { + REQUIRE(static_cast<int>(ClanPermission::NONE) == 0); + REQUIRE(static_cast<int>(ClanPermission::CLAN_CHAT) == 1); + REQUIRE(static_cast<int>(ClanPermission::CLAN_WHO) == 2); + REQUIRE(static_cast<int>(ClanPermission::LEADER_OVERRIDE) == 51); + REQUIRE(static_cast<int>(ClanPermission::CLAN_ADMIN) == 52); + } + + SECTION("magic_enum works with ClanPermission") { + auto name = magic_enum::enum_name(ClanPermission::CLAN_CHAT); + REQUIRE(name == "CLAN_CHAT"); + + auto invite_name = magic_enum::enum_name(ClanPermission::INVITE_MEMBERS); + REQUIRE(invite_name == "INVITE_MEMBERS"); + } +} + +// Test ClanRank permission checking +TEST_CASE("ClanRank permission checking", "[clan][permissions][rank]") { + SECTION("Basic permission checking") { + PermissionSet perms; + perms.set(static_cast<size_t>(ClanPermission::CLAN_CHAT)); + perms.set(static_cast<size_t>(ClanPermission::INVITE_MEMBERS)); + + ClanRank rank("TestRank", perms); + + REQUIRE(rank.has_permission(ClanPermission::CLAN_CHAT)); + REQUIRE(rank.has_permission(ClanPermission::INVITE_MEMBERS)); + REQUIRE_FALSE(rank.has_permission(ClanPermission::KICK_MEMBERS)); + } + + SECTION("LEADER_OVERRIDE grants all permissions") { + PermissionSet perms; + perms.set(static_cast<size_t>(ClanPermission::LEADER_OVERRIDE)); + + ClanRank leader_rank("Leader", perms); + + REQUIRE(leader_rank.has_permission(ClanPermission::CLAN_CHAT)); + REQUIRE(leader_rank.has_permission(ClanPermission::INVITE_MEMBERS)); + REQUIRE(leader_rank.has_permission(ClanPermission::KICK_MEMBERS)); + REQUIRE(leader_rank.has_permission(ClanPermission::MANAGE_RANKS)); + REQUIRE(leader_rank.has_permission(ClanPermission::WITHDRAW_FUNDS)); + } + + SECTION("Permission modification methods") { + ClanRank rank("TestRank", PermissionSet{}); + + // Add permission + rank.add_permission(ClanPermission::CLAN_CHAT); + REQUIRE(rank.has_permission(ClanPermission::CLAN_CHAT)); + + // Remove permission + rank.remove_permission(ClanPermission::CLAN_CHAT); + REQUIRE_FALSE(rank.has_permission(ClanPermission::CLAN_CHAT)); + + // Set permission + rank.set_permission(ClanPermission::INVITE_MEMBERS, true); + REQUIRE(rank.has_permission(ClanPermission::INVITE_MEMBERS)); + + rank.set_permission(ClanPermission::INVITE_MEMBERS, false); + REQUIRE_FALSE(rank.has_permission(ClanPermission::INVITE_MEMBERS)); + } +} + +// Test clan permission checking with fixture +TEST_CASE_METHOD(ClanPermissionTestFixture, "Character clan permission checking", "[clan][permissions][character]") { + + SECTION("Leaders have all permissions via LEADER_OVERRIDE") { + REQUIRE(has_clan_permission(leader_char.get(), ClanPermission::CLAN_CHAT)); + REQUIRE(has_clan_permission(leader_char.get(), ClanPermission::INVITE_MEMBERS)); + REQUIRE(has_clan_permission(leader_char.get(), ClanPermission::KICK_MEMBERS)); + REQUIRE(has_clan_permission(leader_char.get(), ClanPermission::MANAGE_RANKS)); + REQUIRE(has_clan_permission(leader_char.get(), ClanPermission::WITHDRAW_FUNDS)); + } + + SECTION("Officers have specific permissions only") { + REQUIRE(has_clan_permission(officer_char.get(), ClanPermission::CLAN_CHAT)); + REQUIRE(has_clan_permission(officer_char.get(), ClanPermission::INVITE_MEMBERS)); + REQUIRE(has_clan_permission(officer_char.get(), ClanPermission::KICK_MEMBERS)); + REQUIRE(has_clan_permission(officer_char.get(), ClanPermission::PROMOTE_MEMBERS)); + + // Should NOT have these permissions + REQUIRE_FALSE(has_clan_permission(officer_char.get(), ClanPermission::MANAGE_RANKS)); + REQUIRE_FALSE(has_clan_permission(officer_char.get(), ClanPermission::WITHDRAW_FUNDS)); + } + + SECTION("Members have limited permissions") { + REQUIRE(has_clan_permission(member_char.get(), ClanPermission::CLAN_CHAT)); + REQUIRE(has_clan_permission(member_char.get(), ClanPermission::CLAN_WHO)); + + // Should NOT have these permissions + REQUIRE_FALSE(has_clan_permission(member_char.get(), ClanPermission::INVITE_MEMBERS)); + REQUIRE_FALSE(has_clan_permission(member_char.get(), ClanPermission::KICK_MEMBERS)); + REQUIRE_FALSE(has_clan_permission(member_char.get(), ClanPermission::MANAGE_RANKS)); + } + + SECTION("Clanless characters have no permissions") { + REQUIRE_FALSE(has_clan_permission(clanless_char.get(), ClanPermission::CLAN_CHAT)); + REQUIRE_FALSE(has_clan_permission(clanless_char.get(), ClanPermission::CLAN_WHO)); + REQUIRE_FALSE(has_clan_permission(clanless_char.get(), ClanPermission::INVITE_MEMBERS)); + } + + SECTION("Gods have all permissions via has_clan_permission_or_god") { + REQUIRE(has_clan_permission_or_god(god_char.get(), ClanPermission::CLAN_CHAT)); + REQUIRE(has_clan_permission_or_god(god_char.get(), ClanPermission::INVITE_MEMBERS)); + REQUIRE(has_clan_permission_or_god(god_char.get(), ClanPermission::KICK_MEMBERS)); + REQUIRE(has_clan_permission_or_god(god_char.get(), ClanPermission::MANAGE_RANKS)); + REQUIRE(has_clan_permission_or_god(god_char.get(), ClanPermission::WITHDRAW_FUNDS)); + } +} + +// Test modern C++23 permission checking with std::expected +TEST_CASE_METHOD(ClanPermissionTestFixture, "Modern permission checking with std::expected", "[clan][permissions][modern]") { + using namespace clan_permissions; + + SECTION("Successful permission check") { + auto result = check_permission(leader_char.get(), ClanPermission::CLAN_CHAT); + REQUIRE(result.has_value()); + } + + SECTION("Failed permission check returns error") { + auto result = check_permission(member_char.get(), ClanPermission::KICK_MEMBERS); + REQUIRE_FALSE(result.has_value()); + + const auto& error = result.error(); + REQUIRE(error.required_permission == ClanPermission::KICK_MEMBERS); + REQUIRE(error.is_clan_member == true); + INFO("Error reason: '" << error.reason << "'"); + REQUIRE(error.reason.find("KICK_MEMBERS") != std::string::npos); + } + + SECTION("Clanless character error") { + auto result = check_permission(clanless_char.get(), ClanPermission::CLAN_CHAT); + REQUIRE_FALSE(result.has_value()); + + const auto& error = result.error(); + REQUIRE(error.required_permission == ClanPermission::CLAN_CHAT); + REQUIRE(error.is_clan_member == false); + REQUIRE(error.reason == "You are not a member of any clan."); + } + + SECTION("Clan membership check") { + auto member_result = check_clan_member(member_char.get()); + REQUIRE(member_result.has_value()); + + auto clanless_result = check_clan_member(clanless_char.get()); + REQUIRE_FALSE(clanless_result.has_value()); + REQUIRE(clanless_result.error().reason == "You are not a member of any clan."); + } +} + +// Test permission wrapper functions +TEST_CASE_METHOD(ClanPermissionTestFixture, "Permission wrapper functions", "[clan][permissions][wrappers]") { + using namespace clan_permissions; + + SECTION("execute_with_clan_permission works for valid permissions") { + bool executed = false; + auto command_func = [&executed]() { executed = true; }; + + bool success = execute_with_clan_permission(leader_char.get(), ClanPermission::CLAN_CHAT, command_func); + REQUIRE(success); + REQUIRE(executed); + } + + SECTION("execute_with_clan_permission fails for invalid permissions") { + bool executed = false; + auto command_func = [&executed]() { executed = true; }; + + bool success = execute_with_clan_permission(member_char.get(), ClanPermission::KICK_MEMBERS, command_func); + REQUIRE_FALSE(success); + REQUIRE_FALSE(executed); + } + + SECTION("execute_with_clan_membership works for clan members") { + bool executed = false; + auto command_func = [&executed]() { executed = true; }; + + bool success = execute_with_clan_membership(member_char.get(), command_func); + REQUIRE(success); + REQUIRE(executed); + } + + SECTION("execute_with_clan_membership fails for non-members") { + bool executed = false; + auto command_func = [&executed]() { executed = true; }; + + bool success = execute_with_clan_membership(clanless_char.get(), command_func); + REQUIRE_FALSE(success); + REQUIRE_FALSE(executed); + } + + SECTION("God override works in wrapper functions") { + bool executed = false; + auto command_func = [&executed]() { executed = true; }; + + bool success = execute_with_clan_permission(god_char.get(), ClanPermission::MANAGE_RANKS, command_func); + REQUIRE(success); + REQUIRE(executed); + } +} + +// Test legacy conversion functions +TEST_CASE("Legacy permission conversion", "[clan][permissions][legacy]") { + using namespace legacy_conversion; + + SECTION("Legacy privilege conversion") { + REQUIRE(convert_legacy_privilege(1) == ClanPermission::SET_DESCRIPTION); // Description + REQUIRE(convert_legacy_privilege(2) == ClanPermission::SET_MOTD); // Motd + REQUIRE(convert_legacy_privilege(3) == ClanPermission::LEADER_OVERRIDE); // Grant + REQUIRE(convert_legacy_privilege(6) == ClanPermission::INVITE_MEMBERS); // Enroll + REQUIRE(convert_legacy_privilege(7) == ClanPermission::KICK_MEMBERS); // Expel + REQUIRE(convert_legacy_privilege(18) == ClanPermission::CLAN_CHAT); // Chat + + // Invalid legacy values + REQUIRE(convert_legacy_privilege(99) == std::nullopt); + REQUIRE(convert_legacy_privilege(-1) == std::nullopt); + } + + SECTION("Legacy bitset conversion") { + std::bitset<64> legacy_bits; + legacy_bits.set(1); // Description + legacy_bits.set(6); // Enroll (invite) + legacy_bits.set(18); // Chat + + auto new_permissions = convert_legacy_permissions(legacy_bits); + + REQUIRE(new_permissions.test(static_cast<size_t>(ClanPermission::SET_DESCRIPTION))); + REQUIRE(new_permissions.test(static_cast<size_t>(ClanPermission::INVITE_MEMBERS))); + REQUIRE(new_permissions.test(static_cast<size_t>(ClanPermission::CLAN_CHAT))); + REQUIRE_FALSE(new_permissions.test(static_cast<size_t>(ClanPermission::KICK_MEMBERS))); + } +} + +// Test error cases and edge conditions +TEST_CASE_METHOD(ClanPermissionTestFixture, "Permission system edge cases", "[clan][permissions][edge]") { + + SECTION("Null character pointer") { + REQUIRE_FALSE(has_clan_permission(nullptr, ClanPermission::CLAN_CHAT)); + REQUIRE_FALSE(has_clan_permission_or_god(nullptr, ClanPermission::CLAN_CHAT)); + } + + SECTION("Invalid permission values") { + ClanRank rank("Test", PermissionSet{}); + + // Test with out-of-bounds permission (should be safe) + auto invalid_perm = static_cast<ClanPermission>(999); + REQUIRE_FALSE(rank.has_permission(invalid_perm)); + } + + SECTION("Character with invalid clan ID") { + auto invalid_char = std::make_shared<CharData>(); + init_character_data(invalid_char.get(), "Invalid", 1); + set_clan_id(invalid_char.get(), 999); // Non-existent clan + + REQUIRE_FALSE(has_clan_permission(invalid_char.get(), ClanPermission::CLAN_CHAT)); + + using namespace clan_permissions; + auto result = check_permission(invalid_char.get(), ClanPermission::CLAN_CHAT); + REQUIRE_FALSE(result.has_value()); + } +} \ No newline at end of file diff --git a/tests/test_clan_snoop.cpp b/tests/test_clan_snoop.cpp new file mode 100644 index 00000000..62208aa3 --- /dev/null +++ b/tests/test_clan_snoop.cpp @@ -0,0 +1,88 @@ +#include <catch2/catch_test_macros.hpp> +#include "clan.hpp" + +TEST_CASE("Clan Snoop Basic Functionality", "[clan][snoop]") { + // Clear any existing clan snoops to start clean + clan_snoop_table.clear(); + + SECTION("Add and remove clan snoop") { + CharData test_char{}; + ClanID test_clan_id = 1; + + // Initially not snooping + REQUIRE_FALSE(is_snooping_clan(&test_char, test_clan_id)); + + // Add snoop + add_clan_snoop(&test_char, test_clan_id); + REQUIRE(is_snooping_clan(&test_char, test_clan_id)); + + // Remove snoop + remove_clan_snoop(&test_char, test_clan_id); + REQUIRE_FALSE(is_snooping_clan(&test_char, test_clan_id)); + } + + SECTION("Multiple clan snooping") { + CharData test_char{}; + ClanID clan1 = 1; + ClanID clan2 = 2; + + // Add multiple snoops + add_clan_snoop(&test_char, clan1); + add_clan_snoop(&test_char, clan2); + + REQUIRE(is_snooping_clan(&test_char, clan1)); + REQUIRE(is_snooping_clan(&test_char, clan2)); + + auto snooped_clans = get_snooped_clans(&test_char); + REQUIRE(snooped_clans.size() == 2); + + // Remove all snoops + remove_all_clan_snoops(&test_char); + REQUIRE_FALSE(is_snooping_clan(&test_char, clan1)); + REQUIRE_FALSE(is_snooping_clan(&test_char, clan2)); + + snooped_clans = get_snooped_clans(&test_char); + REQUIRE(snooped_clans.empty()); + } + + SECTION("Multiple characters snooping same clan") { + CharData char1{}; + CharData char2{}; + ClanID test_clan_id = 1; + + add_clan_snoop(&char1, test_clan_id); + add_clan_snoop(&char2, test_clan_id); + + REQUIRE(is_snooping_clan(&char1, test_clan_id)); + REQUIRE(is_snooping_clan(&char2, test_clan_id)); + + // Remove one character's snoop + remove_clan_snoop(&char1, test_clan_id); + REQUIRE_FALSE(is_snooping_clan(&char1, test_clan_id)); + REQUIRE(is_snooping_clan(&char2, test_clan_id)); + + // Remove the other character's snoop + remove_clan_snoop(&char2, test_clan_id); + REQUIRE_FALSE(is_snooping_clan(&char2, test_clan_id)); + + // Table should be clean + REQUIRE(clan_snoop_table.empty()); + } + + SECTION("Null character handling") { + ClanID test_clan_id = 1; + + // Functions should handle null pointers gracefully + REQUIRE_FALSE(is_snooping_clan(nullptr, test_clan_id)); + + add_clan_snoop(nullptr, test_clan_id); + remove_clan_snoop(nullptr, test_clan_id); + remove_all_clan_snoops(nullptr); + + auto result = get_snooped_clans(nullptr); + REQUIRE(result.empty()); + } + + // Clean up + clan_snoop_table.clear(); +} \ No newline at end of file