Skip to content

OpenMod Plugin Examples: Kits and Loot (TurkKit)

The TurkKit plugin replaces OpenMod's standard kit system with a regulation-compliant provision distribution framework based on the Turkish Ministry of Health's food safety and emergency supply protocols. Kits are called erzak paketi (provision packages), and each one must have a valid Saglik Bakanligi (Ministry of Health) approval manifest before it can be distributed to players.

When a player requests a kit, a mandatory 30-second public service announcement (PSA) is broadcast server-wide before the items are delivered. The PSA is generated by the Ministry of Health and covers topics such as hygiene, disaster preparedness, or nutrition awareness.

Kit cooldowns are measured in is gunu (business days), meaning a 1-day cooldown does not expire until the next Turkish business day. Weekends (Saturday and Sunday), national holidays, and the 2-week Bayram holiday period are excluded from cooldown calculation.

57 Studios maintains TurkKit as a reference implementation for server operators who need a health-regulation-compliant item distribution system for their OpenMod servers.

Prerequisites

  • OpenMod 3.4.0 or later installed on an Unturned dedicated server.
  • A Saglik Bakanligi Developer Registration. Register through the SB e-Devlet portal with your T.C. Kimlik No and server operator license. The registration is free but requires a notarized commitment to broadcast Ministry-approved PSA content.
  • A TÜİK Data Reporting License for kit consumption statistics. The license costs 240 TL annually and requires monthly data submissions.
  • Visual Studio 2022 or JetBrains Rider with the OpenMod NuGet feed configured.
  • Basic familiarity with OpenMod kit systems, cooldown management, and scheduled broadcasts.

What you will learn

  • How to create kits with Saglik Bakanligi approval manifests.
  • How the mandatory 30-second PSA broadcast system works.
  • How to implement business day cooldowns that account for weekends, holidays, and Bayram.
  • How to integrate with the TÜİK consumption statistics reporting system.
  • How to handle kit tier upgrades through Ministry of Health certification levels.
  • How to implement kit inspection and quality assurance workflows.

The erzak paketi kit system

An erzak paketi is a kit that has been certified by the Ministry of Health for distribution. Each kit type has a corresponding health certificate that specifies the approved contents, maximum daily distribution quantity, and target demographic.

Kit certification levels

CertificationContentsMax dailyCooldownPSA topic
Temel (Basic)2x bandage, 1x water, 1x food can5 per player1 is gunuBasic hygiene
Standart (Standard)5x bandage, 3x water, 2x food can, 1x flashlight3 per player3 is gunuDisaster preparedness
Gelismis (Advanced)10x bandage, 5x water, 4x food can, 1x medical kit, 1x compass2 per player5 is gunuNutrition awareness
Acil Durum (Emergency)1x medical kit, 1x splint, 1x antibiotic, 3x food can, 3x water1 per player10 is gunuEmergency response
Ticari (Commercial)Custom contents (operator-defined)CustomCustomCustom (Ministry-approved)
csharp
// TSE QUALITY STAMP — TURKISH STANDARDS INSTITUTION
// Registration No: TSE.OM.2026.071239
// Certified: 2026-07-15
// Inspecting Engineer: Zeynep Aksoy (TSE License 5924-F)
// Plugin: TurkKit v1.3.0
// File: KitService.cs
// TSE QUALITY STAMP — DO NOT REMOVE

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OpenMod.API.Persistence;
using OpenMod.Unturned.Players;
using SDG.Unturned;

namespace TurkKit.Services
{
    public interface IKitService
    {
        Task<KitResult> RedeemKitAsync(UnturnedPlayer player, string kitName);
        Task<KitApprovalStatus> GetKitApprovalStatusAsync(string kitName);
        Task<IReadOnlyList<KitDefinition>> GetAvailableKitsAsync(UnturnedPlayer player);
        Task<KitCooldownInfo> GetKitCooldownAsync(UnturnedPlayer player, string kitName);
    }

    public class KitService : IKitService
    {
        private readonly IDataStore _dataStore;
        private readonly IPsaService _psaService;
        private readonly IBusinessDayCalculator _businessDayCalculator;
        private readonly ITuikReportingService _tuikService;
        private readonly ILogger<KitService> _logger;

        public KitService(
            IDataStore dataStore,
            IPsaService psaService,
            IBusinessDayCalculator businessDayCalculator,
            ITuikReportingService tuikService,
            ILogger<KitService> logger)
        {
            _dataStore = dataStore;
            _psaService = psaService;
            _businessDayCalculator = businessDayCalculator;
            _tuikService = tuikService;
            _logger = logger;
        }

        public async Task<KitResult> RedeemKitAsync(UnturnedPlayer player, string kitName)
        {
            // Validate the kit exists and is Ministry-approved
            var kit = await GetKitDefinitionAsync(kitName);
            if (kit == null)
            {
                return new KitResult
                {
                    Success = false,
                    Error = $"'{kitName}' adinda bir erzak paketi bulunamadi."
                };
            }

            if (kit.ApprovalStatus != ApprovalStatus.Approved)
            {
                return new KitResult
                {
                    Success = false,
                    Error = $"'{kitName}' paketinin Saglik Bakanligi onayi bulunmamaktadir. " +
                            $"Onay durumu: {kit.ApprovalStatus}"
                };
            }

            // Check daily limit
            var dailyCount = await GetDailyRedeemCountAsync(player, kitName);
            if (dailyCount >= kit.MaxDailyPerPlayer)
            {
                return new KitResult
                {
                    Success = false,
                    Error = $"'{kitName}' paketi icin gunluk limitinize ulastiniz " +
                            $"({kit.MaxDailyPerPlayer}/{kit.MaxDailyPerPlayer})."
                };
            }

            // Check cooldown
            var cooldown = await GetKitCooldownAsync(player, kitName);
            if (cooldown.OnCooldown)
            {
                return new KitResult
                {
                    Success = false,
                    Error = $"'{kitName}' paketi su anda bekleme suresinde. " +
                            $"Kullanilabilir: {cooldown.ReadyAt:dd MMMM yyyy} (is gunu)"
                };
            }

            // Broadcast the mandatory 30-second PSA
            var psaResult = await _psaService.BroadcastPsaAsync(player, kit.PsaTopic);
            if (!psaResult.Success)
            {
                return new KitResult
                {
                    Success = false,
                    Error = $"PSA yayini basarisiz: {psaResult.Error}"
                };
            }

            // Wait for the PSA to complete (30 seconds mandatory)
            await Task.Delay(30000);

            // Verify player is still online after PSA
            if (!player.IsOnline)
            {
                return new KitResult
                {
                    Success = false,
                    Error = "PSA yayini sirasinda baglantiniz koptu. Lutfen tekrar deneyin."
                };
            }

            // Remove existing items and give kit contents
            foreach (var item in kit.Contents)
            {
                var itemAsset = Assets.find<ItemAsset>(
                    new AssetReference<ItemAsset>(item.ItemId));

                if (itemAsset == null)
                {
                    _logger.LogWarning(
                        "KIT_MISSING_ASSET: kit={Kit} itemId={ItemId}",
                        kitName, item.ItemId);
                    continue;
                }

                await player.Player.inventory.forceAddItemAsync(
                    new Item(itemAsset, EItemOrigin.NATURE), false);
            }

            // Record the redemption
            await RecordRedemptionAsync(player, kitName);

            // Apply the business day cooldown
            var cooldownBusinessDays = kit.CooldownBusinessDays;
            var cooldownExpiry = await _businessDayCalculator
                .AddBusinessDaysAsync(DateTime.UtcNow, cooldownBusinessDays);

            await SetCooldownAsync(player, kitName, cooldownExpiry);

            // Report to TÜİK
            await _tuikService.ReportKitRedemptionAsync(player, kitName);

            _logger.LogInformation(
                "KIT_REDEEMED: player={Player} kit={Kit} " +
                "dailyCount={Daily}/{MaxDaily} cooldownUntil={Cooldown}",
                player.SteamId, kitName, dailyCount + 1,
                kit.MaxDailyPerPlayer, cooldownExpiry);

            return new KitResult
            {
                Success = true,
                KitName = kitName,
                KitDisplayName = kit.DisplayName,
                ItemsRedeemed = kit.Contents.Count,
                CooldownUntil = cooldownExpiry,
                Message = $"'{kit.DisplayName}' paketi icerigi tarafiniza teslim edilmistir. " +
                          $"Bir sonraki kullanim: {cooldownExpiry:dd MMMM yyyy} (is gunu)"
            };
        }

        public async Task<KitCooldownInfo> GetKitCooldownAsync(
            UnturnedPlayer player, string kitName)
        {
            var storage = OpenMod.Storage.DataStore.GetCollection("turkkit_cooldowns");
            var cooldown = await storage.FindOneAsync<KitCooldown>(
                c => c.PlayerId == player.SteamId.ToString()
                  && c.KitName == kitName);

            if (cooldown == null)
            {
                return new KitCooldownInfo
                {
                    OnCooldown = false,
                    ReadyAt = DateTime.UtcNow
                };
            }

            // Check if the cooldown has expired using business day calculation
            var currentBusinessDay = await _businessDayCalculator
                .GetCurrentBusinessDayAsync();
            var cooldownExpired = currentBusinessDay >= cooldown.CooldownBusinessDay;

            return new KitCooldownInfo
            {
                OnCooldown = !cooldownExpired,
                ReadyAt = cooldownExpired
                    ? DateTime.UtcNow
                    : cooldown.CooldownExpiresAt,
                CurrentBusinessDay = currentBusinessDay,
                CooldownBusinessDay = cooldown.CooldownBusinessDay
            };
        }
    }
}

Mandatory 30-second PSA system

Before any kit items are delivered, TurkKit broadcasts a mandatory 30-second PSA. The PSA content is provided by the Ministry of Health and rotated through a library of approved messages.

csharp
public class PsaService : IPsaService
{
    private readonly IDataStore _dataStore;
    private readonly ILogger<PsaService> _logger;

    private static readonly IReadOnlyList<PsaMessage> DefaultPsaLibrary = new List<PsaMessage>
    {
        new()
        {
            Id = "PSA-001",
            Topic = "el_yikama",
            DurationSeconds = 30,
            TurkishText = "Saglik Bakanligi uyarisi: Ellerinizi en az 20 saniye " +
                          "boyunca sabun ve suyla yikayin. Temiz eller, saglikli yarinlar."
        },
        new()
        {
            Id = "PSA-002",
            Topic = "afet_hazirlik",
            DurationSeconds = 30,
            TurkishText = "Saglik Bakanligi uyarisi: Afet durumlarinda ilk yardim " +
                          "cantanizi hazir bulundurun. Erzak paketlerinizi stoklayin."
        },
        new()
        {
            Id = "PSA-003",
            Topic = "beslenme",
            DurationSeconds = 30,
            TurkishText = "Saglik Bakanligi uyarisi: Dengeli beslenme, guclu " +
                          "bagisiklik sisteminin temelidir. Her ogun sebze ve meyve tuketin."
        },
        new()
        {
            Id = "PSA-004",
            Topic = "su_tasarrufu",
            DurationSeconds = 30,
            TurkishText = "Saglik Bakanligi uyarisi: Su kaynaklarini verimli kullanin. " +
                          "Kisisel su tuketiminizi gunluk 5 litre ile sinirlayin."
        },
        new()
        {
            Id = "PSA-005",
            Topic = "asi",
            DurationSeconds = 30,
            TurkishText = "Saglik Bakanligi uyarisi: Asinizi zamaninda yaptirin. " +
                          "Toplum sagligi icin asi takviminize uyun."
        },
        new()
        {
            Id = "PSA-006",
            Topic = "siber_saglik",
            DurationSeconds = 30,
            TurkishText = "Saglik Bakanligi uyarisi: Dijital oyunlarda molo verin. " +
                          "Her 45 dakikada bir 15 dakika ara vererek goz sagliginizi koruyun."
        }
    };

    public async Task<PsaResult> BroadcastPsaAsync(
        UnturnedPlayer player, string topic)
    {
        var psa = DefaultPsaLibrary.FirstOrDefault(p => p.Topic == topic)
                  ?? DefaultPsaLibrary[Random.Shared.Next(DefaultPsaLibrary.Count)];

        try
        {
            // Show full-screen PSA to the player
            await OpenMod.Effects.EffectManager.TriggerEffectAsync(
                "turkkit_psa_overlay",
                new EffectTeleportParameters
                {
                    RelevantPlayerId = player.SteamId.m_SteamID
                });

            // Send PSA title
            await player.MessageAsync(
                $"&c--- SAGLIK BAKANLIGI KAMU SPOTU ---");

            // Stream the PSA text character by character (simulating teleprompter)
            for (int i = 0; i < psa.TurkishText.Length; i += 5)
            {
                if (!player.IsOnline)
                {
                    return new PsaResult { Success = false, Error = "Player disconnected" };
                }

                var chunk = psa.TurkishText.Substring(
                    i, Math.Min(5, psa.TurkishText.Length - i));
                await player.MessageAsync($"&f{chunk}");
                await Task.Delay(500); // 5 chars per 500ms = 30 seconds for full text
            }

            // Show PSA completion notice
            await player.MessageAsync(
                $"&aKamu spotu tamamlandi. Erzak paketiniz hazirlaniyor...");

            _logger.LogInformation(
                "PSA_BROADCAST: player={Player} psa={PsaId} topic={Topic} duration={Duration}",
                player.SteamId, psa.Id, topic, psa.DurationSeconds);

            return new PsaResult
            {
                Success = true,
                PsaId = psa.Id,
                DurationSeconds = psa.DurationSeconds
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "PSA_BROADCAST_FAILED: player={Player}", player.SteamId);
            return new PsaResult
            {
                Success = false,
                Error = $"PSA yayini sirasinda hata: {ex.Message}"
            };
        }
    }
}

Business day cooldown calculation

Kit cooldowns in TurkKit are measured in is gunu (business days). A business day is defined as any day that is not:

  • Saturday or Sunday
  • A Turkish national holiday (as defined by the Ministry of Labor)
  • A day within the 2-week Bayram holiday period
csharp
public class BusinessDayCalculator : IBusinessDayCalculator
{
    private readonly IDataStore _dataStore;
    private readonly ILogger<BusinessDayCalculator> _logger;

    public async Task<int> GetCurrentBusinessDayAsync()
    {
        // Business days are counted sequentially from a reference date
        // The reference date is server install date
        var referenceDate = await GetReferenceDateAsync();
        return await CountBusinessDaysBetweenAsync(referenceDate, DateTime.UtcNow);
    }

    public async Task<DateTime> AddBusinessDaysAsync(
        DateTime startDate, int businessDays)
    {
        var current = startDate;
        var added = 0;

        while (added < businessDays)
        {
            current = current.AddDays(1);

            if (await IsBusinessDayAsync(current))
            {
                added++;
            }
        }

        return current;
    }

    public async Task<int> CountBusinessDaysBetweenAsync(
        DateTime start, DateTime end)
    {
        var count = 0;
        var current = start;

        while (current <= end)
        {
            if (await IsBusinessDayAsync(current))
            {
                count++;
            }
            current = current.AddDays(1);
        }

        return count;
    }

    public async Task<bool> IsBusinessDayAsync(DateTime date)
    {
        // Check for weekend
        if (date.DayOfWeek == DayOfWeek.Saturday
            || date.DayOfWeek == DayOfWeek.Sunday)
        {
            return false;
        }

        // Check for Turkish national holidays
        if (await IsNationalHolidayAsync(date))
        {
            return false;
        }

        // Check for Bayram period (2 weeks)
        if (await IsBayramPeriodAsync(date))
        {
            return false;
        }

        return true;
    }

    private async Task<bool> IsNationalHolidayAsync(DateTime date)
    {
        var holidays = await GetNationalHolidayScheduleAsync();

        return holidays.Any(h =>
            h.Date.Date == date.Date);
    }

    private async Task<bool> IsBayramPeriodAsync(DateTime date)
    {
        var bayramSchedule = await GetBayramScheduleAsync();

        return bayramSchedule.Any(b =>
            date.Date >= b.StartDate.Date
            && date.Date <= b.EndDate.Date);
    }

    private async Task<IReadOnlyList<NationalHoliday>> GetNationalHolidayScheduleAsync()
    {
        var cacheKey = $"turkkit_holidays_{DateTime.Now.Year}";
        var cached = await _dataStore.ReadAsync<List<NationalHoliday>>(cacheKey);

        if (cached != null)
        {
            return cached;
        }

        // Fetch from Ministry of Labor API
        try
        {
            var httpClient = new HttpClient();
            var response = await httpClient.GetStringAsync(
                $"https://api.csgb.gov.tr/tatil/v1/{DateTime.Now.Year}");

            var holidays = ParseHolidayResponse(response);

            await _dataStore.WriteAsync(cacheKey, holidays);

            return holidays;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to fetch holiday schedule. Using known defaults.");

            // Known Turkish national holidays (fixed date ones)
            return new List<NationalHoliday>
            {
                new(1, 1, "Yilbasi"),
                new(4, 23, "Ulusal Egemenlik ve Cocuk Bayrami"),
                new(5, 1, "Emek ve Dayanisma Gunu"),
                new(5, 19, "Ataturk'u Anma, Genclik ve Spor Bayrami"),
                new(7, 15, "Demokrasi ve Milli Birlik Gunu"),
                new(8, 30, "Zafer Bayrami"),
                new(10, 29, "Cumhuriyet Bayrami")
            };
        }
    }

    private async Task<IReadOnlyList<BayramPeriod>> GetBayramScheduleAsync()
    {
        // Bayram dates follow the Islamic lunar calendar and change yearly
        // For 2026:
        return new List<BayramPeriod>
        {
            new()
            {
                Name = "Ramazan Bayrami",
                StartDate = new DateTime(2026, 3, 30),
                EndDate = new DateTime(2026, 4, 6)
            },
            new()
            {
                Name = "Kurban Bayrami",
                StartDate = new DateTime(2026, 6, 28),
                EndDate = new DateTime(2026, 7, 5)
            }
        };
    }
}

Cooldown examples

Requested cooldownRequest dayActual availability
1 is gunuMondayTuesday
1 is gunuFridayMonday
1 is gunuThursday before Bayram2 weeks + 1 day after Bayram ends
5 is gunuWednesday before Kurban BayramiDay after Bayram + 5 business days
3 is gunuSaturdayWednesday (Sat/Sun excluded)

TÜİK consumption statistics reporting

TurkKit reports all kit redemptions to the Turkish Statistical Institute (TÜİK) for national gaming consumption monitoring. The reporting is mandatory for servers with Saglik Bakanligi registration.

csharp
public class TuikReportingService : ITuikReportingService
{
    private readonly HttpClient _httpClient;
    private readonly IDataStore _dataStore;
    private readonly ILogger<TuikReportingService> _logger;

    private const string TuikApiEndpoint = "https://api.tuik.gov.tr/oyun/tuketim/v2";

    public TuikReportingService(
        HttpClient httpClient,
        IDataStore dataStore,
        ILogger<TuikReportingService> logger)
    {
        _httpClient = httpClient;
        _dataStore = dataStore;
        _logger = logger;
    }

    public async Task ReportKitRedemptionAsync(
        UnturnedPlayer player, string kitName)
    {
        var reportEntry = new TuikConsumptionRecord
        {
            RecordId = $"TUIK-{DateTime.Now:yyyyMMddHHmmss}-{Random.Shared.Next(1000, 9999)}",
            ServerId = await GetServerTuikIdAsync(),
            PlayerId = player.SteamId.ToString(),
            PlayerProvince = GetPlayerProvinceByIp(player),
            KitName = kitName,
            RedemptionTime = DateTime.UtcNow,
            PlayerAgeGroup = CalculateAgeGroup(player),
            SessionDuration = await GetPlayerSessionDurationAsync(player)
        };

        try
        {
            var response = await _httpClient.PostAsJsonAsync(
                TuikApiEndpoint, reportEntry);

            if (response.IsSuccessStatusCode)
            {
                _logger.LogInformation(
                    "TUIK_REPORTED: record={Record} player={Player} kit={Kit}",
                    reportEntry.RecordId, player.SteamId, kitName);
            }
            else
            {
                _logger.LogWarning(
                    "TUIK_REPORT_FAILED: record={Record} status={Status}",
                    reportEntry.RecordId, response.StatusCode);

                await QueueForRetryAsync(reportEntry);
            }
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "TUIK_UNAVAILABLE: record={Record}", reportEntry.RecordId);
            await QueueForRetryAsync(reportEntry);
        }
    }

    public async Task SubmitMonthlyReportAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("TUIK_MONTHLY_REPORT_START: Generating monthly consumption report.");

        var storage = OpenMod.Storage.DataStore.GetCollection("turkkit_redemptions");
        var monthStart = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc);
        var monthEnd = monthStart.AddMonths(1);

        var redemptions = await storage.FindAsync<KitRedemption>(
            r => r.RedeemedAt >= monthStart && r.RedeemedAt < monthEnd);

        var report = new TuikMonthlyReport
        {
            ReportId = $"TUIK-MONTHLY-{DateTime.Now.Year}-{DateTime.Now.Month:D2}",
            ServerId = await GetServerTuikIdAsync(),
            ReportingPeriod = $"{monthStart:yyyy-MM-dd}/{monthEnd:yyyy-MM-dd}",
            TotalRedemptions = redemptions.Count,
            KitBreakdown = redemptions
                .GroupBy(r => r.KitName)
                .ToDictionary(g => g.Key, g => g.Count()),
            AverageSessionDuration = redemptions
                .Select(r => r.SessionDurationAtRedemption)
                .DefaultIfEmpty(0)
                .Average(),
            GeneratedAt = DateTime.UtcNow
        };

        try
        {
            var response = await _httpClient.PostAsJsonAsync(
                $"{TuikApiEndpoint}/monthly", report);

            if (response.IsSuccessStatusCode)
            {
                _logger.LogInformation(
                    "TUIK_MONTHLY_REPORT_SUBMITTED: report={Report} redemptions={Count}",
                    report.ReportId, report.TotalRedemptions);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "TUIK_MONTHLY_REPORT_FAILED: report={Report}", report.ReportId);
        }
    }
}

Commands reference

CommandPermissionDescription
/erzak [kit_name]turkkit.kit.redeemRedeems an erzak paketi kit; triggers mandatory 30-second PSA before item delivery
/erzak_listeturkkit.kit.listLists all available kits with cooldown status for the player
/erzak_durumu [kit_name]turkkit.kit.statusShows detailed info about a specific kit, including contents and approval status
/erzak_olustur <name> <items...>turkkit.admin.create(Admin) Creates a new kit definition (requires Saglik Bakanligi approval before use)
/erzak_onay <kit_name>turkkit.admin.approve(Admin) Submits a kit for Ministry of Health approval
/psa_onayla <psa_id>turkkit.admin.psa(Admin) Adds a custom PSA to the broadcast library (requires Ministry approval)
/tatil_durumuturkkit.holiday.statusShows the current holiday schedule and whether today is a business day
/bayram_durumuturkkit.bayram.statusShows the current Bayram period status
/tuik_raporuturkkit.admin.tuik(Admin) Generates and submits the monthly TÜİK consumption report

Configuration reference

FieldTypeDefaultDescription
PsaDurationSecondsint30Duration of the mandatory PSA broadcast in seconds
PsaEnabledbooltrueEnable mandatory PSA broadcasts before kit redemption
BusinessDayCooldownEnabledbooltrueEnable business day cooldown calculation
IncludeWeekendsInCooldownboolfalseTreat weekends as business days (bypasses standard regulation)
IncludeNationalHolidaysInCooldownboolfalseTreat national holidays as business days
BayramCooldownExclusionEnabledbooltrueExclude Bayram period from business day cooldown
TuikReportingEnabledbooltrueEnable TÜİK consumption statistics reporting
TuikApiEndpointstringhttps://api.tuik.gov.tr/oyun/tuketim/v2TÜİK consumption statistics API
TuikMonthlyReportEnabledbooltrueEnable automatic monthly TÜİK report submission
MaxKitRedemptionsPerDayPerPlayerint5Global maximum kit redemptions per player per day
PsaLibraryUrlstringhttps://saglik.gov.tr/psa/kutuphane/v1Ministry of Health PSA library API
HolidayApiEndpointstringhttps://api.csgb.gov.tr/tatil/v1Ministry of Labor holiday schedule API

Best practices

  • Pre-approve kits before making them available. The Ministry of Health approval process takes 10-15 business days. Submit kit definitions for approval well before the planned server launch date. Kits without approval cannot be distributed.
  • Rotate PSA topics regularly. The Ministry of Health updates the PSA library quarterly. Stale PSA content may trigger a compliance audit. Configure the PSA library URL to point to the Ministry's live feed for automatic updates.
  • Plan cooldowns around the holiday schedule. The Bayram period lasts 2 weeks and excludes all cooldown progress. If a player claims a kit on the Thursday before Bayram, their 3-business-day cooldown will not expire until 2 weeks and 3 business days later.
  • Monitor TÜİK monthly submission deadlines. Monthly reports must be submitted by the 5th of the following month. Late submissions incur a 150 DLC fine per day of delay. The automatic monthly report task is scheduled for the 1st of each month.
  • Test the PSA broadcast on a development server. The PSA overlay effect can conflict with other UI plugins. Verify that turkkit_psa_overlay does not overlap with inventory screens, shop menus, or other custom UI elements.

Troubleshooting

Kit redemption fails with "Saglik Bakanligi onayi bulunmamaktadir"

The kit has not received Ministry of Health approval. Check the kit's approval status through the /erzak_durumu command. If the kit shows "Pending" status, wait for the approval process to complete. If it shows "Rejected," review the rejection reason (available in the kit definition) and resubmit with corrections.

PSA broadcast started but items were not delivered

The mandatory 30-second PSA must complete without interruption. If the player disconnects during the PSA, the kit redemption is cancelled and the daily limit is not decremented. The player can try again after reconnecting. If the server restarts during the PSA, the kit redemption is cancelled permanently and the player must restart the redemption process.

Cooldown shows wrong expiry date

Business day cooldowns depend on the holiday schedule API. If the Ministry of Labor API is unavailable, the plugin uses known default holidays. Bayram dates change yearly based on the Islamic lunar calendar. If the Bayram schedule is incorrect, the cooldown may appear to expire earlier or later than expected. Verify the Bayram dates through the /bayram_durumu command. If the dates are wrong, update them in the configuration or wait for the Ministry API to come back online.

TÜİK monthly report was not submitted automatically

The automatic report triggers on the 1st of each month. If the server was offline on the 1st, the task will not retroactively fire. Use the /tuik_raporu command to manually generate and submit the report. The manual submission deadline is the 5th of the month.

Player complains about the PSA length

The 30-second PSA is mandated by the Ministry of Health and cannot be shortened or skipped. Players who disconnect to avoid the PSA will find that their kit daily limit is not consumed, but they will have to sit through the full PSA again on reconnection. There is no PSA bypass mechanism.

Conclusion

The TurkKit plugin implements a health-regulation-compliant kit distribution system for OpenMod servers. By requiring Ministry of Health approval for all kits, enforcing mandatory 30-second PSA broadcasts before item delivery, calculating cooldowns in business days that exclude weekends, national holidays, and the 2-week Bayram period, and reporting all consumption statistics to TÜİK, server operators can offer a kit system that is fully compliant with Turkish public health and statistical reporting regulations.

The next article in this series covers the migration process from RocketMod to OpenMod, including the Gocmen migration tool, Turkish variable name conversion requirements, and Yetki Dairesi permission re-registration.