Architecture

Technical reference for Gurotopia's modular item system, collision, and world persistence

Item Property System

Every item in Gurotopia is parsed from the official items.dat file at startup. The raw property byte and cat byte in each item definition carry bitfield flags that determine item behavior — alongside the type enum.

Property Flags

Defined in include/items/properties.hpp as enum item_property:

FlagValueDescription
PROP_DROP_SEED1 << 0 (0x01)Drops a seed variant when smashed
PROP_NO_SEED1 << 1 (0x02)Never drops seeds even if type::SEED
PROP_DROPLESS1 << 2 (0x04)Never drops a block when smashed
PROP_PERMANENT1 << 3 (0x08)Block can't be destroyed normally
PROP_MULTI_FACING1 << 4 (0x10)Can face multiple directions
PROP_NO_SHADOW1 << 5 (0x20)Tile casts no shadow
PROP_WRENCHABLE1 << 6 (0x40)Wrench action opens a dialog
PROP_AUTO_PICKUP1 << 7 (0x80)Auto-returns to inventory on break

Category Flags

Defined in enum item_category:

FlagValueDescription
CAT_RETURN1 << 1 (0x02)Can't be destroyed, returns to backpack
CAT_SURPRISING_FRUIT1 << 3 (0x08)Tree bears surprising fruit
CAT_PUBLIC1 << 4 (0x10)Anyone can smash even in locked world
CAT_HOLIDAY1 << 6 (0x40)Only creatable during events
CAT_UNTRADEABLE1 << 7 (0x80)Cannot be dropped or traded

Checking Properties in Code

#include "items/properties.hpp"

// Check a property flag
if (has_property(*item, item_property::PROP_DROPLESS)) {
    // item never drops seeds
}

// Check a category flag
if (has_category(*item, item_category::CAT_RETURN)) {
    // item returns to inventory on break
}

Item Struct

Defined in include/database/items.hpp:

class item {
public:
    u_short id{};
    u_char property{};     // bitfield flags
    u_char cat{};           // category flags
    u_char type{};          // item type (FIST, LOCK, SEED, etc.)
    std::string raw_name{};
    int ingredient{};
    u_char collision{};     // collision type
    u_char hits{};
    int hit_reset{};
    u_char cloth_type{};
    short rarity{};
    int tick{};             // growth/production timer (seconds)
    std::string info{};
    std::array<u_short, 2> splice{};  // splice recipe pair
};

Behavior Registry

The behavior registry replaces hardcoded switch(item->id) blocks with a modular registration system. Each item ID gets its own handler function — no more editing core files to add new items.

Core API

Defined in include/items/registry.hpp:

// Register a behavior for a specific item ID
void item_registry::register_item(
    u_short item_id,
    item_behavior_fn fn,
    const std::string& name = ""
);

// Lookup — returns nullptr if not registered
const item_behavior_fn* item_registry::get_handler(u_short item_id);

Handler Signature

#include "items/registry.hpp"

struct item_context {
    class peer* pPeer;      // player data
    class world* world;     // current world
    class block* block;     // the tile being interacted with
    const class item* item; // the item definition
    struct state state;     // tank packet state
    ENetEvent& event;       // network event
};

enum class item_action : u_char {
    NONE,           // continue with default pipeline
    HANDLED,        // skip remaining defaults
    BREAK,          // break the block
    RETURN_ITEM,    // return item to inventory
    CONSUME,        // consume 1 from inventory
    CANCEL,         // abort silently
};

using item_behavior_fn = std::function<item_action(item_context& ctx)>;

Adding a New Item Behavior

Add the handler and registration in include/items/init_behaviors.cpp:

// 1. Write the handler
static item_action on_my_item(item_context& ctx) {
    // do something with ctx.block, ctx.pPeer, ctx.world
    send_varlist(ctx.event.peer, { "OnTalkBubble", ctx.pPeer->netid, "Hello!", 0u, 1u });
    return item_action::CONSUME;
}

// 2. Register it
void register_item_behaviors() {
    item_registry::register_item(1234, on_my_item, "My Custom Item");
}

That's it — one function, one registration line. No modifying tile_change.cpp or other core files.

Currently Registered Items

IDNameHandler
758Roulette Wheelon_roulette_wheel
1404Door Moveron_door_mover
822Water Bucketon_water_bucket
1866Block Glueon_block_glue
3062Pocket Lighteron_pocket_lighter
2480Megaphoneon_megaphone
408Duct Tapeon_duct_tape
3400Love Potion #8on_love_potion
1488Experience Potionon_exp_potion
3404/3406Lollipopson_lollipop
3478-3492Paint Buckets (8 colors)on_paint_*
1008ATMon_atm
872Chickenon_provider_drop_next
866Cowon_provider_drop_next
1632Coffee Makeron_provider_drop_next
3888Sheepon_provider_drop_next
5116Tea Seton_provider_tea_set
2798Wellon_provider_well
928Science Stationon_provider_science
456Diceon_dice
1300Roshamboon_roshambo
392Heartstoneon_love_chest
3402GBCon_love_chest
9350Super GBCon_love_chest

Collision System

The collision system lives in namespace collision_check (defined in include/items/collision.hpp) and uses the existing enum collision from items.dat.

Collision Types (from items.dat)

enum collision : u_char {
    NO_COLLISION,   // air — free pass
    FULL,           // solid wall
    ON_TOP,         // platform — stand on top, jump through
    IF_ACCESS,      // requires world access
    TOGGLE,         // toggleable barrier
    HORIZONTAL,     // one-way (horizontal)
    IF_VIP,         // VIP entrance
    VERTICAL,       // one-way (vertical)
    ADVENTURE_ITEM,
    ACTIVATE,       // triggers on contact
    BALLOON_WARZ_TEAM,
    IF_GUILD,       // guild entrance
    STEP_ON         // triggered when stepped on
};

Movement Rules

namespace collision_check {

enum class movement_rule : u_char {
    BLOCK,      // solid — cannot pass
    PASS,       // free — walk through
    PLATFORM,   // stand on top, jump through from below
    SLAB,       // half-height block
    ONE_WAY_X,  // pass from left only
    ONE_WAY_Y,  // pass from above only
    ACCESS,     // pass only if in world access list
    VIP,        // pass only if VIP
    GUILD,      // pass only if same guild
    ACTIVATE,   // pass triggers activation
};

Collision Check on Place

Used during tile placement in tile_change.cpp:

if (!collision_check::can_place_at(item->collision, state.pos, state.punch))
    return;

Collision Check on Movement

Integrated in state/movement.cpp — prevents walking through solid walls and handles one-way platforms:

collision_check::movement_rule rule = collision_check::get_movement_rule(
    static_cast<::collision>(item.collision)
);

if (rule == collision_check::movement_rule::BLOCK) {
    // Reject movement through solid tiles
    state_visuals(*event.peer, std::move(state));
    return;
}

World Persistence

World state is saved to MariaDB as a serialized binary blob. Each world has exactly one row in the world_state table.

Database Schema

Created automatically at startup by init_world_database() in include/database/world_db.cpp:

CREATE TABLE IF NOT EXISTS world_state (
    name       VARCHAR(100) PRIMARY KEY,
    owner      INT NOT NULL DEFAULT 0,
    data       MEDIUMBLOB,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Serialization Format

The binary format packs the entire world into a versioned blob:

SectionSizeDescription
Version1 byteFormat version (currently 1)
Metadata16 bytesowner (4) + is_public (1) + lock_state (1) + min_level (1) + weather_x (4) + weather_y (4)
Access list4 + N*4count + array of user IDs
Blocks4 + N * (4+4+4+1+1+8+label)count + each tile: fg, bg, state[4], hits[2], tick (ms), label
Doors4 + N * (string+string+string+x+y)count + each door
Displays4 + N * (4+4+4)count + each display
Random blocks4 + N * (1+4+4)count + each random block
Objects4 + N * (2+2+4+4+4) + 4count + each object + last_object_uid

Save/Load Lifecycle

// Load world from DB (in join_request.cpp)
load_world(*it);
// If no data, generate fresh world:
if (empty) generate_world(*it, big_name);

// Save when last player leaves (in quit_to_exit.cpp)
if (--world->visitors <= 0) {
    save_world(*world);
    worlds.erase(world);
}

// Save all worlds on shutdown (in peer.cpp)
void safe_disconnect_peers(int code) {
    for (auto& w : worlds)
        save_world(w);
    // ...shutdown sequence...
}

Adding New Data to Save

To persist additional world data (e.g., new block fields), update the serialization functions in include/database/world_db.cpp:

  1. Bump the version byte
  2. Add read/write calls in serialize_world() and deserialize_world()
  3. Handle the old version gracefully in the deserializer

File Layout Summary

include/
├── items/
│   ├── properties.hpp      # Property + category flag enums
│   ├── registry.hpp        # Behavior registry class
│   ├── registry.cpp         # Registry implementation
│   ├── init_behaviors.hpp  # Registration function declaration
│   ├── init_behaviors.cpp  # All item handlers + registrations
│   └── collision.hpp       # Collision checking namespace
└── database/
    ├── world.hpp            # World + block structs
    ├── world.cpp            # World functions
    ├── world_db.hpp         # Persistence API
    └── world_db.cpp         # Binary serialization + DB operations