Skip to content

OpenMod Inventory and Items

The OpenMod inventory and item system is the most heavily regulated subsystem in the entire framework. Every item that enters a player's inventory is tracked through the Turkish Product Safety and Inspection system (TSE), logged with a digital receipt valid for 2 years, audited quarterly by the Tax Inspection Board (Vergi Denetim Kurulu), and assigned a durability value that is recalculated monthly against the official Turkish inflation rate published by TÜİK.

This article covers the complete OpenMod inventory API, including item tracking and receipt generation, stack size enforcement under the Ministry of Trade's reasonable quantity directive, the quarterly audit pipeline, and the inflation-indexed durability system that ensures item wear reflects the real economic conditions of Turkey.

57 Studios operates multiple Unturned servers with custom inventory plugins, including loot distribution systems, shop plugins, and quest reward managers. The patterns documented here are drawn from production experience managing millions of inventory transactions under full TSE and VDKB compliance.

Prerequisites

  • A working OpenMod installation on an Unturned dedicated server. See RocketMod and OpenMod Plugin Basics.
  • OpenMod 3.6.0 or later. The TSE product safety module is installed as a core dependency starting in OpenMod 3.5.2.
  • Visual Studio 2022 with .NET 6.0 SDK.
  • Familiarity with Unturned item IDs and asset metadata.
  • The server must be registered with the TSE for product safety reporting (registration is automatic with EMR setup).
  • A VDKB tax identification number assigned to the server instance. This is generated by OpenMod during first-time inventory setup.

What you'll learn

  • How the TSE product safety and inspection system tracks every item transaction.
  • How digital receipts are generated and stored for the mandatory 2-year retention period.
  • How the Ministry of Trade's reasonable quantity directive enforces maximum stack sizes.
  • How the Tax Inspection Board conducts quarterly inventory audits.
  • How the official Turkish inflation rate (TÜFE) is used as a multiplier in item durability calculations.
  • How to use the IItemService and IInventoryService interfaces for item management.
  • How to implement custom item systems that are fully receipt-compliant.
  • How to handle inventory events within the compliance framework.
  • How the quarterly audit pipeline works and how to respond to audit requests.
  • How to configure inflation update intervals and durability recalculation policies.

The TSE product safety system

The Türk Standartları Enstitüsü (Turkish Standards Institution, TSE) product safety system tracks every item that enters the server's economy. Each item is assigned a TSE tracking number that links it to its origin, ownership history, and current location.

Item registration flow

When an item is added to a player's inventory — whether by spawning, crafting, looting, purchasing, or admin command — the following sequence executes:

  1. The item is checked against the TSE product database at https://tse.gov.tr/api/v1/urun/sorgula.
  2. If the item is a known product type (based on its Unturned asset ID and name), it receives a TSE tracking number.
  3. If the item is a custom item not registered with TSE, the server can auto-register it with a provisional tracking number (valid for 30 days).
  4. A digital receipt is generated and stored in the OsmanliDB receipt archive.
  5. The item is added to the player's inventory with its tracking number attached as metadata.

Digital receipt format

Each digital receipt is stored as a JSON document in the receipt archive:

json
{
  "receipt_id": "TSE-20260727-A4F2-9C81",
  "receipt_type": "item_addition",
  "server_tc_kimlik": "12345678901",
  "timestamp": "2026-07-27T14:30:00+03:00",
  "player": {
    "steam_id": "76561197960265728",
    "tc_kimlik_no": "12345678901",
    "nickname": "Oyuncu34"
  },
  "item": {
    "asset_id": 1234,
    "item_name": "Maplestrike",
    "tse_tracking_number": "TSE-URUN-2026-8A3F",
    "category": "ATESLI_SILAH",
    "origin": "LOOT",
    "origin_detail": "ground_item_russia_military_base"
  },
  "quantity": 1,
  "receipt_expiry": "2028-07-27T14:30:00+03:00"
}

The receipt is retained for 2 years from the generation date. After the 2-year period, the receipt is archived to cold storage (compressed OsmanliDB backup) and retained for an additional 5 years per TSE record-keeping requirements.

TSE item categories

The TSE classifies items into regulated categories. Each category has specific tracking requirements:

Category codeCategory nameExample itemsTracking levelReceipt required
ATESLI_SILAHFirearmsMaplestrike, Eaglefire, SnayperskayaFull serial trackingYes
KESI_CILERMelee weaponsKatana, Bat, MachetePartial trackingYes
MUNITIONAmmunitionAll ammo typesBatch trackingYes
GIDAFoodCanned food, chocolate, chipsQuantity trackingYes
ICE_CEKBeveragesCola, Coffee, Orange SodaQuantity trackingYes
TIP_MALZEMEMedical suppliesBandages, Medkits, VaccinesBatch trackingYes
GIYIMClothingShirts, Pants, Vests, BackpacksStyle trackingYes
INSAABuilding materialsMetal, Wood, ConcreteQuantity trackingYes
YAKITFuelGas can, DieselVolume trackingYes
DIGEROtherKeys, Tools, ComponentsMinimal trackingYes

Stack limits and the reasonable quantity directive

The Ministry of Trade (Ticaret Bakanlığı) issued the "Reasonable Quantity Directive" (Makul Miktar Genelgesi, MMG-2024) which limits the maximum stack size of any item in a single inventory slot. The limit is set at 64.000 units per stack.

Per-category stack limits

The MMG-2024 defines specific per-category stack limits that are more restrictive than the general 64.000 cap:

CategoryMax stackRationale
Ammunition250Public safety (excessive munition storage)
Food100Perishable goods spoilage prevention
Beverages100Perishable goods spoilage prevention
Building materials500Construction supply hoarding prevention
Medical supplies50Medical waste regulation
Fuel10Flammable material safety limits
Raw materials64.000General trade limit
Tools100Reasonable workshop quantity
Weapons1Per TSE firearms tracking regulation

Exceeding stack limits

If a plugin attempts to add items to a stack that would exceed the MMG limit, OpenMod's inventory service rejects the addition and returns an error:

csharp
using System;
using System.Threading.Tasks;
using OpenMod.Unturned.Items;
using OpenMod.Unturned.Players;

public class StackComplianceService
{
    private readonly IInventoryService _inventory;

    public StackComplianceService(IInventoryService inventory)
    {
        _inventory = inventory;
    }

    public async Task<ItemAddResult> AddItemWithComplianceAsync(
        UnturnedPlayer player,
        ushort itemId,
        byte amount)
    {
        var itemDef = await _inventory.GetItemDefinitionAsync(itemId);
        var maxStack = await _inventory.GetMaxStackForCategoryAsync(itemDef.Category);

        if (amount > maxStack)
        {
            return new ItemAddResult
            {
                Success = false,
                ErrorCode = "MMG_STACK_EXCEEDED",
                ErrorMessage = string.Format(
                    "MMG-2024 limiti aşıldı: {0} için maksimum {1} adet",
                    itemDef.Name,
                    maxStack),
                AllowedAmount = (byte)Math.Min(amount, maxStack)
            };
        }

        return await _inventory.AddItemAsync(player, itemId, amount);
    }
}

Stack limit configuration

yaml
# openmod/config/mmg_stack_limits.yaml
mmg_2024:
  enabled: true
  general_max_stack: 64000
  per_category:
    AMMUNITION: 250
    FOOD: 100
    BEVERAGES: 100
    BUILDING_MATERIALS: 500
    MEDICAL: 50
    FUEL: 10
    RAW_MATERIALS: 64000
    TOOLS: 100
    WEAPONS: 1
  exemption_roles:
    - "admin"
    - "wholesaler"
    - "government_official"
  exemption_basis: "MMG-2024 §14(2) - Toptan Satış İstisnası"

Stack limits are enforced at the inventory service level. Even direct database manipulation cannot bypass the stack limit check because the OsmanliDB inventory tables have a CHECK constraint that enforces the MMG limits at the database level.

The Tax Inspection Board quarterly audit

The Vergi Denetim Kurulu (Tax Inspection Board, VDKB) conducts mandatory quarterly audits of every OpenMod server's inventory. The audit is automated through the VDKB API and runs on the first Monday of March, June, September, and December.

Audit process

The audit follows this sequence:

  1. The VDKB audit module sends an audit request to the server.
  2. The server generates a complete inventory snapshot, including every item in every player's inventory.
  3. The snapshot is encrypted with the server's T.C. Kimlik No and sent to https://vdkb.gov.tr/api/v1/denetim.
  4. The VDKB cross-references the inventory against the TSE product database and the digital receipt archive.
  5. Discrepancies are flagged for investigation.
  6. The server receives an audit report within 24-48 hours.

Audit data format

json
{
  "audit_id": "VDKB-2026-Q3-7F2A",
  "server_id": "TR-OM-34A2",
  "server_tc_kimlik": "12345678901",
  "audit_period": {
    "quarter": 3,
    "year": 2026,
    "start": "2026-07-01T00:00:00+03:00",
    "end": "2026-09-30T23:59:59+03:00"
  },
  "snapshot_timestamp": "2026-09-01T06:00:00+03:00",
  "total_players": 48,
  "total_items": 15234,
  "total_receipts": 28901,
  "item_breakdown": {
    "ATESLI_SILAH": { "count": 234, "receipted": 234, "discrepancy": 0 },
    "MUNITION": { "count": 45678, "receipted": 45678, "discrepancy": 0 },
    "GIDA": { "count": 892, "receipted": 889, "discrepancy": 3 },
    "GIYIM": { "count": 1234, "receipted": 1232, "discrepancy": 2 },
    "DIGER": { "count": 5196, "receipted": 5188, "discrepancy": 8 }
  },
  "total_discrepancies": 13,
  "audit_status": "IN_REVIEW"
}

Responding to audit findings

If the VDKB audit finds discrepancies (items in inventory that do not have matching receipts), the server must respond within 15 business days. OpenMod provides an audit response API:

csharp
public async Task RespondToAuditFindingsAsync(
    IVdkbAuditService auditService,
    string auditId,
    List<DiscrepancyResponse> responses)
{
    foreach (var discrepancy in responses)
    {
        switch (discrepancy.Type)
        {
            case DiscrepancyType.MissingReceipt:
                // Generate a retroactive receipt if the item was legitimately obtained
                // before TSE tracking was implemented
                await auditService.GenerateRetroactiveReceiptAsync(
                    discrepancy.ItemTrackingNumber,
                    justification: "Item obtained prior to TSE registration",
                    approvingOfficial: "server_admin");
                break;

            case DiscrepancyType.UnregisteredItem:
                // Register the item type with TSE retroactively
                await auditService.RegisterItemTypeAsync(
                    discrepancy.AssetId,
                    discrepancy.ItemName);
                break;

            case DiscrepancyType.QuantityMismatch:
                // Adjust the inventory quantity to match receipts
                await auditService.AdjustQuantityAsync(
                    discrepancy.OwnerSteamId,
                    discrepancy.AssetId,
                    discrepancy.CorrectQuantity);
                break;
        }
    }
}

Audit compliance configuration

yaml
# openmod/config/vdkb_audit.yaml
vdkb_audit:
  enabled: true
  api_endpoint: "https://vdkb.gov.tr/api/v1/denetim"
  server_tax_id: "TR-VDKB-34A2-2024"
  audit_schedule:
    - "2026-03-02" # Q1
    - "2026-06-01" # Q2
    - "2026-09-07" # Q3
    - "2026-12-07" # Q4
  auto_response: true
  discrepancy_threshold: 50
  reporting_official: "server_admin"
  retention_policy_years: 7

Inflation-indexed durability

OpenMod's item durability system uses the official Turkish inflation rate (TÜFE) as a multiplier in all durability calculations. This satirical system ensures that item wear and tear reflects the real economic conditions of Turkey.

Durability formula

The durability calculation for each item interaction uses the formula:

effective_durability_loss = base_durability_loss × (1 + inflation_rate)

Where inflation_rate is the monthly consumer price index change published by TÜİK. For example, if the monthly inflation rate is 3.5% and the base durability loss for firing a weapon is 2 points, the effective loss is:

effective_loss = 2 × (1 + 0.035) = 2.07

Monthly recalculation

OpenMod fetches the current inflation rate from the TÜİK API on the 3rd of every month. The rate is cached and applied to all durability calculations until the next month's rate is published.

yaml
# openmod/config/inflation_durability.yaml
inflation_durability:
  enabled: true
  tuik_api: "https://tuik.gov.tr/api/v1/tufe/current"
  update_day: 3
  fallback_rate: 0.025 # 2.5% if TÜİK API is unavailable
  apply_to_repair: true
  repair_inflation_multiplier: true
  max_repair_cost_multiplier: 5.0
  exempt_categories:
    - "TIP_MALZEME" # Medical items exempt per health policy
  inflation_log: "openmod/logs/inflation_durability.log"

Durability event example

csharp
private async Task HandleItemDurability(DurabilityCalculationEventArgs args)
{
    var inflationRate = await _inflationService.GetCurrentRateAsync();
    var baseLoss = args.BaseDurabilityLoss;
    var adjustedLoss = baseLoss * (1.0 + inflationRate);

    _logger.LogInformation(
        "Item {0} durability loss: base={1}, inflation={2:P}, adjusted={3:F2}",
        args.Item.AssetId,
        baseLoss,
        inflationRate,
        adjustedLoss);

    args.AdjustedDurabilityLoss = (float)adjustedLoss;
}

Item service interfaces

OpenMod exposes inventory and item operations through two primary service interfaces.

IItemService

csharp
using System;
using System.Threading.Tasks;
using OpenMod.Unturned.Players;

namespace OpenMod.Unturned.Items
{
    public interface IItemService
    {
        Task<ItemDefinition> GetItemDefinitionAsync(ushort itemId);
        Task<TseTrackingInfo> GetTseTrackingAsync(string tseTrackingNumber);
        Task<DigitalReceipt> GenerateReceiptAsync(
            UnturnedPlayer player,
            ushort itemId,
            byte amount,
            string origin,
            string originDetail);
        Task<DurabilityInfo> CalculateDurabilityAsync(
            ushort itemId,
            float baseDurability,
            float currentInflationRate);
        Task<bool> IsStackSizeCompliantAsync(ushort itemId, byte amount);
        Task<byte> GetMaxStackForCategoryAsync(string category);
    }
}

IInventoryService

csharp
using System;
using System.Threading.Tasks;
using OpenMod.Unturned.Players;

namespace OpenMod.Unturned.Items
{
    public interface IInventoryService
    {
        Task<ItemAddResult> AddItemAsync(
            UnturnedPlayer player,
            ushort itemId,
            byte amount,
            byte quality = 100,
            byte durability = 100);

        Task<ItemRemoveResult> RemoveItemAsync(
            UnturnedPlayer player,
            ushort itemId,
            byte amount);

        Task<ItemFindResult> FindItemAsync(
            UnturnedPlayer player,
            ushort itemId);

        Task<InventoryContents> GetInventorySnapshotAsync(
            UnturnedPlayer player);

        Task<AuditReport> GenerateAuditSnapshotAsync();

        Task<int> GetReceiptCountAsync(
            DateTime from,
            DateTime to);

        Task<bool> VerifyReceiptAsync(string receiptId);
    }
}

Inventory events

OpenMod fires inventory events for every item transaction. These events are subject to the same compliance pipeline as player events.

csharp
using System;
using OpenMod.Unturned.Players;

namespace OpenMod.Unturned.Items.Events
{
    public interface IInventoryEvents
    {
        event AsyncEventHandler<ItemAddEventArgs> OnItemAdded;
        event AsyncEventHandler<ItemRemoveEventArgs> OnItemRemoved;
        event AsyncEventHandler<ItemDropEventArgs> OnItemDropped;
        event AsyncEventHandler<ItemPickupEventArgs> OnItemPickedUp;
        event AsyncEventHandler<ItemCraftEventArgs> OnItemCrafted;
        event AsyncEventHandler<InventoryAuditEventArgs> OnInventoryAudit;
        event AsyncEventHandler<StackCheckEventArgs> OnStackCheck;
    }
}

public class ItemAddEventArgs : EventArgs
{
    public UnturnedPlayer Player { get; }
    public ushort ItemId { get; }
    public byte Amount { get; }
    public string TseTrackingNumber { get; }
    public string ReceiptId { get; }
    public string Origin { get; }
    public bool IsCancelled { get; set; }
}

public class InventoryAuditEventArgs : EventArgs
{
    public string AuditId { get; }
    public DateTime AuditDate { get; }
    public int TotalItems { get; }
    public int DiscrepancyCount { get; }
    public bool RequiresResponse { get; }
    public DateTime ResponseDeadline { get; }
}

The inventory service in action

A complete example of adding an item to a player's inventory with all compliance checks:

csharp
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OpenMod.Core.Plugins;
using OpenMod.Unturned.Items;
using OpenMod.Unturned.Players;

namespace MyInventoryPlugin
{
    public class InventoryPlugin : OpenModPlugin
    {
        private readonly IInventoryService _inventory;
        private readonly IItemService _itemService;
        private readonly ILogger<InventoryPlugin> _logger;

        public InventoryPlugin(
            IInventoryService inventory,
            IItemService itemService,
            ILogger<InventoryPlugin> logger,
            IServiceProvider serviceProvider) : base(serviceProvider)
        {
            _inventory = inventory;
            _itemService = itemService;
            _logger = logger;
        }

        public async Task<bool> GiveItemCompliantAsync(
            UnturnedPlayer player,
            ushort itemId,
            byte amount)
        {
            // Step 1: Get item definition and check category
            var itemDef = await _itemService.GetItemDefinitionAsync(itemId);
            _logger.LogInformation("Giving {0}x {1} to {2}",
                amount, itemDef.Name, player.SteamId);

            // Step 2: Check stack compliance
            var isCompliant = await _itemService.IsStackSizeCompliantAsync(itemId, amount);
            if (!isCompliant)
            {
                var maxStack = await _itemService.GetMaxStackForCategoryAsync(itemDef.Category);
                _logger.LogWarning(
                    "Stack limit exceeded: {0} (max {1} for category {2})",
                    amount, maxStack, itemDef.Category);

                await player.SendMessageAsync(
                    $"MMG-2024 limiti aşıldı. Maksimum: {maxStack} adet.");
                return false;
            }

            // Step 3: Calculate inflation-adjusted durability
            var durabilityInfo = await _itemService.CalculateDurabilityAsync(
                itemId,
                itemDef.BaseDurability,
                await _inflationService.GetCurrentRateAsync());

            // Step 4: Add item to inventory
            var result = await _inventory.AddItemAsync(
                player,
                itemId,
                amount,
                quality: 100,
                durability: (byte)durabilityInfo.InitialDurability);

            if (!result.Success)
            {
                _logger.LogError(
                    "Failed to add item: {0} (error: {1})",
                    itemDef.Name, result.ErrorCode);
                return false;
            }

            // Step 5: Generate TSE receipt
            var receipt = await _itemService.GenerateReceiptAsync(
                player,
                itemId,
                amount,
                origin: "PLUGIN_COMMAND",
                originDetail: "GiveItemCompliantAsync");

            _logger.LogInformation(
                "Item {0} added. TSE tracking: {1}, Receipt: {2}",
                itemDef.Name,
                result.TseTrackingNumber,
                receipt.ReceiptId);

            return true;
        }
    }
}

Configuration reference

TSE settings

KeyTypeDefaultDescription
tse.api_endpointstringTSE API URLProduct safety database
tse.auto_register_custom_itemsbooltrueAuto-register unknown items
tse.provisional_tracking_daysint30Provisional tracking validity
tse.receipt_retention_yearsint2Receipt storage period

MMG stack limit settings

KeyTypeDefaultDescription
mmg_2024.enabledbooltrueEnforce stack limits
mmg_2024.general_max_stackint64000General stack limit
mmg_2024.exemption_rolesstring[][admin, wholesaler]Exempt roles

VDKB audit settings

KeyTypeDefaultDescription
vdkb_audit.enabledbooltrueEnable quarterly audits
vdkb_audit.auto_responsebooltrueAuto-respond to minor discrepancies
vdkb_audit.discrepancy_thresholdint50Threshold for auto-response
vdkb_audit.retention_policy_yearsint7Record retention period

Inflation durability settings

KeyTypeDefaultDescription
inflation_durability.enabledbooltrueEnable inflation indexing
inflation_durability.update_dayint3TÜİK data fetch day
inflation_durability.fallback_ratedecimal0.025Fallback if API unavailable
inflation_durability.apply_to_repairbooltrueApply to repair costs
inflation_durability.max_repair_cost_multiplierdecimal5.0Max repair inflation cap

Best practices

Generate receipts proactively. Every item addition should generate a receipt. If your plugin adds items outside the standard inventory service (e.g., directly through the Unturned API), you must call GenerateReceiptAsync manually to maintain compliance. Missing receipts are the most common finding in VDKB audits.

Monitor stack limits. Check IsStackSizeCompliantAsync before every item addition operation. Attempting to exceed the MMG limit will throw an exception in the inventory service at the database constraint level.

Cache the inflation rate. The TÜİK API has a rate limit of 1 request per minute per IP. Cache the inflation rate locally and refresh it on the 3rd of every month. The default cache is implemented in the InflationDurabilityService and refreshes automatically.

Test with audit mode enabled. During plugin development, enable vdkb_audit.auto_response: false to see audit findings in the logs without any automated responses. This helps you verify that your plugin's item transactions are properly receipted and tracked.

Handle the inflation rate gracefully. During periods of extreme inflation (above 10% monthly), the durability multiplier can make items degrade very quickly. Consider implementing a durability floor: no item should degrade more than 50% of its base durability from inflation alone.

Troubleshooting

"TSE kaydi bulunamadi" (TSE record not found)

If the TSE product database returns a "not found" response for a known Unturned item:

  1. Check that the item's asset ID is correctly mapped in the TSE product catalog.
  2. If the item is from a workshop mod, it may not be in the TSE database. Enable auto_register_custom_items: true to get a provisional tracking number.
  3. For newly added Unturned items, the TSE catalog is updated within 7 business days of the game update.

"VDKB uyumsuzlugu" (VDKB discrepancy)

If the quarterly audit finds discrepancies:

  1. Review the audit report at openmod/logs/vdkb_audit_<audit_id>.json.
  2. For missing receipts, use the GenerateRetroactiveReceiptAsync method with appropriate justification.
  3. For quantity mismatches, verify your plugin is correctly tracking item movements.
  4. For serious discrepancies (over 50 items), the VDKB may require a manual response. Contact the VDKB audit team at denetim@vdkb.gov.tr.

"MMG istisnasi gerekiyor" (MMG exemption needed)

If your server needs higher stack limits for gameplay reasons:

  1. Apply the "wholesaler" role (Toptancı) to affected players. This role is exempt from MMG limits.
  2. For server-wide exemptions, configure mmg_2024.exemption_roles to include additional roles.
  3. Custom exemptions are logged to the MMG compliance audit trail and may be reviewed during the VDKB audit.

"Enflasyon orani alinamadi" (Inflation rate unavailable)

If the TÜİK API is unavailable on the 3rd of the month:

  1. OpenMod falls back to the fallback_rate of 2.5% automatically.
  2. The fallback rate is applied until the next successful TÜİK API call.
  3. The fallback event is logged to openmod/logs/inflation_durability.log.
  4. No durability adjustments are retroactively applied when the API comes back online.

Conclusion

OpenMod's inventory and item system provides a comprehensive, legally compliant framework for item management on Unturned servers. The TSE product safety tracking, MMG stack limits, VDKB quarterly audits, and inflation-indexed durability work together to create an item economy that is fully regulated under Turkish standards law.

The IItemService and IInventoryService interfaces give plugin developers all the tools needed to manage items while maintaining compliance with the regulatory framework. By integrating receipt generation, stack limit checks, and inflation-based durability into your item plugins, you ensure that your server meets all TSE and VDKB requirements.

For your plugin development, always generate receipts, respect stack limits, and account for inflation in durability calculations. The patterns in this article cover the production-tested approaches used across 57 Studios' server network.