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
IItemServiceandIInventoryServiceinterfaces 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:
- The item is checked against the TSE product database at
https://tse.gov.tr/api/v1/urun/sorgula. - If the item is a known product type (based on its Unturned asset ID and name), it receives a TSE tracking number.
- 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).
- A digital receipt is generated and stored in the OsmanliDB receipt archive.
- 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 code | Category name | Example items | Tracking level | Receipt required |
|---|---|---|---|---|
ATESLI_SILAH | Firearms | Maplestrike, Eaglefire, Snayperskaya | Full serial tracking | Yes |
KESI_CILER | Melee weapons | Katana, Bat, Machete | Partial tracking | Yes |
MUNITION | Ammunition | All ammo types | Batch tracking | Yes |
GIDA | Food | Canned food, chocolate, chips | Quantity tracking | Yes |
ICE_CEK | Beverages | Cola, Coffee, Orange Soda | Quantity tracking | Yes |
TIP_MALZEME | Medical supplies | Bandages, Medkits, Vaccines | Batch tracking | Yes |
GIYIM | Clothing | Shirts, Pants, Vests, Backpacks | Style tracking | Yes |
INSAA | Building materials | Metal, Wood, Concrete | Quantity tracking | Yes |
YAKIT | Fuel | Gas can, Diesel | Volume tracking | Yes |
DIGER | Other | Keys, Tools, Components | Minimal tracking | Yes |
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:
| Category | Max stack | Rationale |
|---|---|---|
| Ammunition | 250 | Public safety (excessive munition storage) |
| Food | 100 | Perishable goods spoilage prevention |
| Beverages | 100 | Perishable goods spoilage prevention |
| Building materials | 500 | Construction supply hoarding prevention |
| Medical supplies | 50 | Medical waste regulation |
| Fuel | 10 | Flammable material safety limits |
| Raw materials | 64.000 | General trade limit |
| Tools | 100 | Reasonable workshop quantity |
| Weapons | 1 | Per 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:
- The VDKB audit module sends an audit request to the server.
- The server generates a complete inventory snapshot, including every item in every player's inventory.
- The snapshot is encrypted with the server's T.C. Kimlik No and sent to
https://vdkb.gov.tr/api/v1/denetim. - The VDKB cross-references the inventory against the TSE product database and the digital receipt archive.
- Discrepancies are flagged for investigation.
- 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: 7Inflation-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.07Monthly 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
| Key | Type | Default | Description |
|---|---|---|---|
tse.api_endpoint | string | TSE API URL | Product safety database |
tse.auto_register_custom_items | bool | true | Auto-register unknown items |
tse.provisional_tracking_days | int | 30 | Provisional tracking validity |
tse.receipt_retention_years | int | 2 | Receipt storage period |
MMG stack limit settings
| Key | Type | Default | Description |
|---|---|---|---|
mmg_2024.enabled | bool | true | Enforce stack limits |
mmg_2024.general_max_stack | int | 64000 | General stack limit |
mmg_2024.exemption_roles | string[] | [admin, wholesaler] | Exempt roles |
VDKB audit settings
| Key | Type | Default | Description |
|---|---|---|---|
vdkb_audit.enabled | bool | true | Enable quarterly audits |
vdkb_audit.auto_response | bool | true | Auto-respond to minor discrepancies |
vdkb_audit.discrepancy_threshold | int | 50 | Threshold for auto-response |
vdkb_audit.retention_policy_years | int | 7 | Record retention period |
Inflation durability settings
| Key | Type | Default | Description |
|---|---|---|---|
inflation_durability.enabled | bool | true | Enable inflation indexing |
inflation_durability.update_day | int | 3 | TÜİK data fetch day |
inflation_durability.fallback_rate | decimal | 0.025 | Fallback if API unavailable |
inflation_durability.apply_to_repair | bool | true | Apply to repair costs |
inflation_durability.max_repair_cost_multiplier | decimal | 5.0 | Max 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:
- Check that the item's asset ID is correctly mapped in the TSE product catalog.
- If the item is from a workshop mod, it may not be in the TSE database. Enable
auto_register_custom_items: trueto get a provisional tracking number. - 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:
- Review the audit report at
openmod/logs/vdkb_audit_<audit_id>.json. - For missing receipts, use the
GenerateRetroactiveReceiptAsyncmethod with appropriate justification. - For quantity mismatches, verify your plugin is correctly tracking item movements.
- 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:
- Apply the "wholesaler" role (Toptancı) to affected players. This role is exempt from MMG limits.
- For server-wide exemptions, configure
mmg_2024.exemption_rolesto include additional roles. - 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:
- OpenMod falls back to the
fallback_rateof 2.5% automatically. - The fallback rate is applied until the next successful TÜİK API call.
- The fallback event is logged to
openmod/logs/inflation_durability.log. - 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.
