[MODULE] MemStore - In-RAM Transient Cross-Map Storage

For scripting examples for learning how to use AMXX.
User avatar
Archive-Poster
Posts: 1136
Gender:
United States of America

[MODULE] MemStore - In-RAM Transient Cross-Map Storage

Post by Archive-Poster »

[AMXX 1.9 / 1.10] MemStore v1.1.0 — Fast In-RAM Key-Value & Ranking Store (Cross-Map Transient Storage)

Hey everyone,

I've been working on a lightweight, high-performance module for AMXX called MemStore and wanted to share it with the community.

Why did I make this?
Quite often I needed to pass transient data between plugins or keep state across map changes (temporary session points, kill streaks, custom inventories, round states, map leaderboards, etc.) without writing to disk via nVault/SQL (which causes unnecessary disk I/O and latency for temporary data) and without abusing localinfo (which has severe GoldSrc string size limits and global pollution).

MemStore acts as a blazingly fast in-RAM key-value store (inspired by Redis concepts) running entirely in server memory. It survives map changes (changelevel) with zero disk writes, supports multiple expiration policies, and safely auto-sweeps expired data.

Main Features:
RAM-Only Storage: Zero disk I/O. Benchmarked at 4–6 million operations/sec with zero noticeable server impact.
Supported Types: Integers (cell), Floats, Strings, and Arrays.
Dynamic Arrays & Schema Validation: mem_get_array() provides an optional &copied_size output parameter returning the exact count of cells copied. This allows saving variable-length arrays (e.g., dynamic inventories, custom loadouts) without reading uninitialized buffer memory, as well as enforcing strict struct-size schema validation.
Unified Namespace Architecture: Each namespace independently maintains both a Key-Value table and a Sorted Rank table. You can store full composite arrays and update leaderboard ranks under the exact same namespace and key without collisions or redundant lookups!
Expiration Policies:
- EXP_PERSISTENT: Stays in RAM across map changes until server shutdown or manual removal.
- EXP_MAP_END: Automatically wiped on the next map change with zero manual cleanup.
- EXP_MAP_COUNT: Retained for N map rotations (e.g., survive for 2 maps).
- EXP_TTL: Monotonic wall-clock expiration in seconds (with automated cleanup).
Built-in Ranking Engine (ZSET): Native sorted leaderboards with score sorting (RANK_DESC or RANK_ASC) and deterministic tie-breaking. Ideal for map top fraggers, speedruns, etc.
Zero-Handle Key Enumeration:
- Index-based key lookup: mem_get_key_at() and mem_get_namespace_count().
- Public callback iteration: mem_iterate_keys(namespace, "MyCallback") without allocating or tracking dynamic Pawn handles.
Type Safety & Inspection: mem_get_key_type() to verify entry types safely before reading.
Memory Protection & Quotas: Native safeguards against unbounded 32-bit memory exhaustion via mem_set_limits() (capping keys per table, array sizes, max namespaces, and aggregate RAM quota).
Linux & Windows Compatibility: Built with C++17 and compiled with -D_GLIBCXX_USE_CXX11_ABI=0 in an Ubuntu 18.04 environment to run out-of-the-box on both ancient and modern Linux distributions.

Quick Example 1: Dynamic Loadout & copied_size across Map Changes
Save only active items, then restore them safely knowing the exact count without reading buffer garbage:

Code: Select all

#include <amxmodx>
#include <memstore>

#define MAX_LOADOUT_SLOTS 10

public SavePlayerLoadout(id)
{
    new authid[MAX_AUTHID_LENGTH];
    get_user_authid(id, authid, charsmax(authid));

    // Player currently has 3 active items in their loadout
    new activeGear[3] = { CSW_AK47, CSW_DEAGLE, CSW_FLASHBANG };

    // Save only 3 cells to RAM; survives 1 map change
    mem_set_array("player_gear", authid, activeGear, sizeof(activeGear), EXP_MAP_COUNT, 1);
}

public RestorePlayerLoadout(id)
{
    new authid[MAX_AUTHID_LENGTH];
    get_user_authid(id, authid, charsmax(authid));

    new gearBuffer[MAX_LOADOUT_SLOTS];
    new itemsRestored = 0;

    // 'itemsRestored' receives the exact count (3), preventing uninitialized reads
    if (mem_get_array("player_gear", authid, gearBuffer, sizeof(gearBuffer), itemsRestored))
    {
        server_print("[MemStore] Restored %d active items for %s:", itemsRestored, authid);

        for (new i = 0; i < itemsRestored; i++)
        {
            server_print("  - Slot #%d: CSW ID %d", i + 1, gearBuffer[i]);
        }
    }
}
Quick Example 2: Unified Leaderboard & Composite Record (Best Practice)
Pack stats and strings into an untagged enum array, ranking and storing everything under a single namespace and single key (SteamID):

Code: Select all

#include <amxmodx>
#include <memstore>

new const MAP_STATS_NS[] = "mapstats";

enum _:PlayerStats
{
    STAT_KILLS = 0,
    STAT_DEATHS,
    STAT_NAME[MAX_NAME_LENGTH] // Embedded string in the cell array!
};

public SavePlayerSession(id)
{
    new authid[MAX_AUTHID_LENGTH];
    get_user_authid(id, authid, charsmax(authid));

    new pData[PlayerStats];
    pData[STAT_KILLS]  = get_user_frags(id);
    pData[STAT_DEATHS] = get_user_deaths(id);
    get_user_name(id, pData[STAT_NAME], charsmax(pData[][STAT_NAME]));

    // 1. Save full composite record (stats + nickname) under the player's SteamID
    mem_set_array(MAP_STATS_NS, authid, pData, sizeof(pData), EXP_MAP_END);

    // 2. Rank player by kills under the EXACT SAME namespace and key
    mem_rank_set(MAP_STATS_NS, authid, pData[STAT_KILLS], EXP_MAP_END);
}

public ShowTopLeader(id)
{
    new topAuth[MAX_AUTHID_LENGTH], topKills = 0;

    // Fetch #1 position from leaderboard
    if (mem_rank_get_top(MAP_STATS_NS, 1, topAuth, charsmax(topAuth), topKills, RANK_DESC))
    {
        // Retrieve full record and nickname with schema size validation
        new pData[PlayerStats], copied = 0;
        if (mem_get_array(MAP_STATS_NS, topAuth, pData, sizeof(pData), copied) && copied == PlayerStats)
        {
            client_print(id, print_chat, "[MemStore] #1 Leader: %s with %d kills and %d deaths!",
                pData[STAT_NAME], pData[STAT_KILLS], pData[STAT_DEATHS]);
        }
    }
}
Included Ready-to-Use Plugins in Repository:
example_loadout.sma: Demonstrates variable-length equipment storage across map changes using the &copied_size parameter.
example_top15.sma: Demonstrates unified single-namespace sorted rankings combined with array lookups to display a top-15 map deaths ranking in MOTD or console.
memstore_mapstats.sma: Full production example tracking Kills, Deaths, Knife Kills, and successful C4 Plants/Defuses using ReAPI. Displays a styled HTML/CSS MOTD on intermission (RG_CSGameRules_GoToIntermission) and automatically wipes data on map change via EXP_MAP_END.
test_memstore.sma: Full 30+ automated unit test battery and benchmark suite (amx_memstore_run_all).

Installation:
Copy memstore_amxx.dll (Windows) or memstore_amxx_i386.so (Linux) to addons/amxmodx/modules/
Copy memstore.inc to addons/amxmodx/scripting/include/
Note: You do not need to edit modules.ini! The module auto-loads seamlessly via #pragma loadlib as soon as any plugin including <memstore> is compiled and loaded.

Compatibility:
AMX Mod X 1.9, 1.10 and ReAMXX. Supports both Windows and Linux (32-bit).
Attachments
test_memstore.sma.zip
(7.61 KiB) Downloaded 2 times
example_top15.sma.zip
(1.96 KiB) Downloaded 2 times
example_mapstats.sma.zip
(3.43 KiB) Downloaded 5 times
example_loadout.sma.zip
(1.89 KiB) Downloaded 1 time
memstore-linux32.zip
(150.91 KiB) Downloaded 2 times
memstore-universal.zip
(176.75 KiB) Downloaded 2 times
memstore-win32.zip
(39.39 KiB) Downloaded 3 times
amxx-memstore-1.2.0-sourcecode.zip
(69.28 KiB) Downloaded 2 times

Who is online

Users browsing this forum: No registered users and 0 guests