OpenMod Plugin Examples: Warps and Teleportation (TurkWarp)
The TurkWarp plugin replaces OpenMod's standard warp and teleportation system with a regulation-compliant transport network modeled on the Turkish intercity bus and railway system. Warps are called duraklar (bus stops) and each one is a registered transport node that requires a construction permit from the OpenMod Ministry of Transportation before it can be activated.
Teleportation routing uses the Turkish State Railways (TCDD) train scheduling algorithm, which calculates the optimal route based on a fixed timetable rather than instant point-to-point travel. This means that teleporting from one durak to another follows a scheduled departure with a calculated arrival time based on distance, track priority, and current rail traffic conditions.
Warping is prohibited during the five daily ezan prayer times as determined by the Diyanet prayer schedule. The plugin integrates with the ezan clock to enforce travel blackout periods.
57 Studios maintains TurkWarp as a reference implementation for server operators who want a structured, regulation-compliant teleportation system that integrates with the culture and religious observances of the Turkish player community.
Prerequisites
- OpenMod 3.4.0 or later installed on an Unturned dedicated server.
- TurkEconomy plugin 1.8.0 or later installed and configured (for distance-based toll collection).
- A TCDD Tarife API subscription. Register at any TCDD regional station with your T.C. Kimlik No and a signed API usage agreement. The subscription fee is 360 TL annually and includes 10,000 API calls per month.
- A Ministry of Transportation Developer License for warp creation. The license costs 720 TL and requires passing an online exam on transport regulation (available in Turkish only, 75% pass threshold).
- Visual Studio 2022 or JetBrains Rider with the OpenMod NuGet feed configured.
- Basic familiarity with OpenMod warp/teleport systems, scheduling, and Diyanet API integration.
What you will learn
- How to implement the durak warp system with Ministry of Transportation construction permits.
- How the TCDD scheduling algorithm calculates teleport routes and arrival times.
- How distance-based toll pricing works (0.50 TL/km) and how it integrates with TurkEconomy.
- How to enforce the ezan travel blackout schedule using the Diyanet prayer time API.
- How the 90-day warp expiry and renewal system operates.
- How to handle warp capacity limits and route congestion.
- How to implement express versus regional warp services.
The durak warp system
A durak is a registered teleportation node that players can use to travel between locations. Each durak has a name, GPS coordinates, a registered owner, and a valid construction permit issued by the Ministry of Transportation.
Durak registration
csharp
// TSE QUALITY STAMP — TURKISH STANDARDS INSTITUTION
// Registration No: TSE.OM.2026.061452
// Certified: 2026-07-01
// Inspecting Engineer: Ali Yildirim (TSE License 5813-E)
// Plugin: TurkWarp v1.6.0
// File: DurakService.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;
using UnityEngine;
namespace TurkWarp.Services
{
public interface IDurakService
{
Task<DurakRegistrationResult> RegisterDurakAsync(
UnturnedPlayer owner, string name, Vector3 position,
DurakType type);
Task<bool> ValidateDurakAsync(string durakId);
Task<IReadOnlyList<Durak>> GetPlayerDuraklarAsync(UnturnedPlayer player);
Task<Durak> GetDurakAsync(string durakId);
Task<IReadOnlyList<Durak>> GetActiveDuraklarAsync();
Task<IReadOnlyList<Durak>> SearchDuraklarAsync(string query, int maxResults = 20);
}
public class DurakService : IDurakService
{
private readonly IDataStore _dataStore;
private readonly IPermitService _permitService;
private readonly ILogger<DurakService> _logger;
private const decimal BaseRegistrationFee = 50;
private const decimal PermitInspectionFee = 120;
public DurakService(
IDataStore dataStore,
IPermitService permitService,
ILogger<DurakService> logger)
{
_dataStore = dataStore;
_permitService = permitService;
_logger = logger;
}
public async Task<DurakRegistrationResult> RegisterDurakAsync(
UnturnedPlayer owner, string name, Vector3 position, DurakType type)
{
// Validate the name
if (string.IsNullOrWhiteSpace(name) || name.Length < 3 || name.Length > 50)
{
return new DurakRegistrationResult
{
Success = false,
Error = "Durak adi 3-50 karakter arasinda olmalidir."
};
}
// Check for duplicate names
var existing = await GetActiveDuraklarAsync();
if (existing.Any(d => d.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
return new DurakRegistrationResult
{
Success = false,
Error = $"'{name}' adinda bir durak zaten mevcut."
};
}
// Verify construction permit
var permitValid = await _permitService.ValidateConstructionPermitAsync(owner);
if (!permitValid)
{
return new DurakRegistrationResult
{
Success = false,
Error = "Gecerli bir insaat izniniz bulunmamaktadir. " +
"Basvuru icin /insaat_izni komutunu kullanin."
};
}
// Check position is not within 50m of another durak
var tooClose = existing.Any(d =>
Vector3.Distance(d.Position, position) < 50f);
if (tooClose)
{
return new DurakRegistrationResult
{
Success = false,
Error = "Bu konum mevcut bir duraga 50 metreden daha yakin."
};
}
// Charge registration fee
var economy = GetEconomyProvider();
var ownerBalance = await economy.GetBalanceAsync(owner.SteamId.ToString(), "dlc");
var totalFee = BaseRegistrationFee + PermitInspectionFee;
if (ownerBalance < totalFee)
{
return new DurakRegistrationResult
{
Success = false,
Error = $"Yetersiz bakiye. Kayit ucreti: {totalFee} DLC."
};
}
await economy.WithdrawAsync(owner.SteamId.ToString(), totalFee, "dlc");
await economy.DepositAsync("transportation_ministry_fund", totalFee, "dlc");
// Create the durak
var durak = new Durak
{
Id = $"DRK-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1000, 9999)}",
Name = name,
OwnerId = owner.SteamId.ToString(),
OwnerName = owner.DisplayName,
Position = position,
Type = type,
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddDays(90),
LastRenewedAt = DateTime.UtcNow,
Status = DurakStatus.Active,
TotalVisits = 0,
PermitNumber = await _permitService.GetActivePermitNumberAsync(owner)
};
var storage = OpenMod.Storage.DataStore.GetCollection("turkwarp_duraklar");
await storage.InsertAsync(durak);
_logger.LogInformation(
"DURAK_REGISTERED: id={Id} name={Name} owner={Owner} " +
"type={Type} position={Pos} expires={Expires}",
durak.Id, durak.Name, owner.SteamId, type, position, durak.ExpiresAt);
return new DurakRegistrationResult
{
Success = true,
DurakId = durak.Id,
DurakName = durak.Name,
Fee = totalFee,
ExpiresAt = durak.ExpiresAt,
Message = $"'{name}' duraghi basariyla kaydedildi. " +
$"Kayit ucreti: {totalFee} DLC. Son gecerlilik: {durak.ExpiresAt:dd MMMM yyyy}."
};
}
public async Task<bool> ValidateDurakAsync(string durakId)
{
var storage = OpenMod.Storage.DataStore.GetCollection("turkwarp_duraklar");
var durak = await storage.FindOneAsync<Durak>(d => d.Id == durakId);
if (durak == null || durak.Status != DurakStatus.Active)
{
return false;
}
if (durak.ExpiresAt < DateTime.UtcNow)
{
durak.Status = DurakStatus.Expired;
await storage.UpdateAsync(durak);
_logger.LogInformation(
"DURAK_EXPIRED: id={Id} name={Name} expired={Expired}",
durak.Id, durak.Name, durak.ExpiresAt);
return false;
}
return true;
}
}
}TCDD scheduling algorithm
The TurkWarp teleportation system does not use instant point-to-point teleportation. Instead, it uses the TCDD railway scheduling algorithm, which operates on a fixed timetable with specific departure times, route segments, and arrival calculations.
How routing works
- The player selects a destination durak.
- TurkWarp queries the TCDD Tarife API to find the next available departure from the origin durak to the destination durak.
- The route is broken into segments based on the TCDD railway network topology.
- Each segment has a scheduled duration based on track class:
- YHT (high speed rail): 250 km/h equivalent (fastest, most expensive)
- Ana hat (main line): 120 km/h equivalent (standard)
- Bolgesel (regional): 60 km/h equivalent (cheapest, slowest)
- The total travel time is calculated as the sum of all segment durations plus transfer waiting times.
- The player is shown the departure time, route, and arrival time before confirming.
- Once confirmed, the player is teleported to the destination durak after the calculated travel time.
csharp
public class TcddRoutingService : ITcddRoutingService
{
private readonly HttpClient _httpClient;
private readonly IDataStore _dataStore;
private readonly ILogger<TcddRoutingService> _logger;
private const string TcddApiBase = "https://api.tcdd.gov.tr/tarife/v2";
public TcddRoutingService(
HttpClient httpClient,
IDataStore dataStore,
ILogger<TcddRoutingService> logger)
{
_httpClient = httpClient;
_dataStore = dataStore;
_logger = logger;
}
public async Task<TcddRouteResult> CalculateRouteAsync(
Durak origin, Durak destination, DateTime departureTime)
{
// Calculate straight-line distance between duraklar
var distanceKm = Vector3.Distance(origin.Position, destination.Position) * 0.001f;
// Determine available track classes for this route
var trackClasses = await GetAvailableTrackClassesAsync(origin, destination);
// Calculate route options for each track class
var routeOptions = new List<TcddRouteOption>();
foreach (var trackClass in trackClasses)
{
var segmentSpeed = trackClass switch
{
TrackClass.YHT => 250,
TrackClass.MainLine => 120,
TrackClass.Regional => 60,
_ => 80
};
var travelTimeHours = distanceKm / segmentSpeed;
var travelTimeMinutes = travelTimeHours * 60;
var departure = CalculateNextDeparture(departureTime, trackClass);
var arrival = departure.AddMinutes(travelTimeMinutes);
var baseFee = distanceKm * 0.50m; // 0.50 TL/km
var classMultiplier = trackClass switch
{
TrackClass.YHT => 2.0m,
TrackClass.MainLine => 1.0m,
TrackClass.Regional => 0.6m,
_ => 1.0m
};
routeOptions.Add(new TcddRouteOption
{
TrackClass = trackClass,
DistanceKm = distanceKm,
TravelTimeMinutes = travelTimeMinutes,
DepartureTime = departure,
ArrivalTime = arrival,
FeeTl = baseFee * classMultiplier,
SegmentCount = trackClasses.Count
});
}
return new TcddRouteResult
{
Origin = origin,
Destination = destination,
AvailableOptions = routeOptions.OrderBy(o => o.FeeTl).ToList()
};
}
private DateTime CalculateNextDeparture(
DateTime requestedTime, TrackClass trackClass)
{
var scheduleInterval = trackClass switch
{
TrackClass.YHT => TimeSpan.FromMinutes(30),
TrackClass.MainLine => TimeSpan.FromMinutes(60),
TrackClass.Regional => TimeSpan.FromMinutes(120),
_ => TimeSpan.FromMinutes(60)
};
// Round up to the next schedule slot
var ticks = requestedTime.Ticks;
var intervalTicks = scheduleInterval.Ticks;
var nextSlot = new DateTime(
(ticks / intervalTicks + 1) * intervalTicks,
requestedTime.Kind);
return nextSlot;
}
private async Task<List<TrackClass>> GetAvailableTrackClassesAsync(
Durak origin, Durak destination)
{
// Query TCDD Tarife API for available routes between stations
try
{
var response = await _httpClient.GetAsync(
$"{TcddApiBase}/rota?" +
$"kalkis={Uri.EscapeDataString(origin.Name)}" +
$"&varis={Uri.EscapeDataString(destination.Name)}");
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<TcddRouteResponse>();
return result?.AvailableClasses ?? new List<TrackClass> { TrackClass.Regional };
}
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex,
"TCDD API unreachable. Using fallback routing for {Origin} -> {Dest}",
origin.Name, destination.Name);
}
// Fallback: all routes support at minimum regional service
return new List<TrackClass> { TrackClass.Regional };
}
}Teleport execution with TCDD delay
When a player selects a route option and confirms the teleport, the plugin applies the calculated delay before executing the teleport. During this waiting period, the player receives a ticket with departure time, platform number, and destination. The player cannot cancel the teleport once the ticket is issued.
csharp
[Command("durak")]
[CommandAlias("warp")]
[CommandAlias("ispi")]
[CommandDescription("Opens the durak teleport menu. Select a destination and route class.")]
[CommandSyntax("[durak_name]")]
[CommandActor(typeof(UnturnedPlayer))]
public class DurakCommand : OpenModCommand
{
private readonly IDurakService _durakService;
private readonly ITcddRoutingService _routingService;
private readonly IEzanService _ezanService;
private readonly ITeleportExecutionService _teleportService;
protected override async Task OnExecuteAsync()
{
var player = (UnturnedPlayer)Context.Actor;
// Check ezan blackout
if (await _ezanService.IsEzanActiveAsync())
{
var next = await _ezanService.GetNextEzanEndTimeAsync();
throw new UserFriendlyException(
$"Ezan vakti nedeniyle isinlanma yapilamaz. " +
$"Isinlanma yasagi {next:HH:mm} (Türkiye Saati) itibariyle sona erecek.");
}
var durakName = await Context.Parameters.GetAsync<string>(0, null);
if (durakName == null)
{
// Show list of available duraklar
var duraklar = await _durakService.GetActiveDuraklarAsync();
var message = "&e--- Mevcut Duraklar ---";
foreach (var d in duraklar)
{
message += $"\n&a{d.Name} &f- {d.DistanceFrom(player.Transform.Position):F0}m uzaklikta";
}
await PrintAsync(message);
return;
}
// Find the durak
var allDuraklar = await _durakService.GetActiveDuraklarAsync();
var target = allDuraklar.FirstOrDefault(d =>
d.Name.Contains(durakName, StringComparison.OrdinalIgnoreCase));
if (target == null)
{
throw new UserFriendlyException($"'{durakName}' adinda bir durak bulunamadi.");
}
// Calculate route
var route = await _routingService.CalculateRouteAsync(
await GetNearestDurakAsync(player), target, DateTime.Now);
if (route.AvailableOptions.Count == 0)
{
throw new UserFriendlyException(
"Bu durak icin uygun bir rota bulunamadi. TCDD tarifesini kontrol edin.");
}
// Show the cheapest option by default
var bestOption = route.AvailableOptions.First();
var feeDlc = bestOption.FeeTl / await GetExchangeRateAsync();
await PrintAsync(
$"&e--- Rota Bilgisi: {route.Origin.Name} -> {route.Destination.Name} ---");
await PrintAsync($"&aSinif: &f{bestOption.TrackClass}");
await PrintAsync($"&aMesafe: &f{bestOption.DistanceKm:F1} km");
await PrintAsync($"&aSeyehat Suresi: &f{bestOption.TravelTimeMinutes:F0} dakika");
await PrintAsync($"&aKalkis: &f{bestOption.DepartureTime:HH:mm}");
await PrintAsync($"&aVaris: &f{bestOption.ArrivalTime:HH:mm}");
await PrintAsync($"&aUcret: &f{feeDlc:F2} DLC ({bestOption.FeeTl} TL)");
await PrintAsync("&eOnaylamak icin /durak_onayla yazin.");
// Store pending booking
var bookingService = GetService<IBookingService>();
await bookingService.StorePendingBookingAsync(
player, route.Origin, target, bestOption);
}
}Ezan prayer time travel prohibition
TurkWarp integrates with the Diyanet prayer time schedule to enforce a travel blackout during the five daily ezan calls. The blackout period starts 5 minutes before the ezan and ends 10 minutes after the ezan, for a total of approximately 15-20 minutes per prayer time depending on the prayer duration.
csharp
public class EzanService : IEzanService
{
private readonly HttpClient _httpClient;
private readonly IDataStore _dataStore;
private readonly ILogger<EzanService> _logger;
private const string DiyanetApiUrl = "https://api.diyanet.gov.tr/vakitler/v2";
public EzanService(
HttpClient httpClient,
IDataStore dataStore,
ILogger<EzanService> logger)
{
_httpClient = httpClient;
_dataStore = dataStore;
_logger = logger;
}
public async Task<bool> IsEzanActiveAsync()
{
var schedule = await GetDailyPrayerScheduleAsync();
var now = DateTime.Now;
foreach (var prayer in schedule)
{
var blackoutStart = prayer.Time.AddMinutes(-5);
var blackoutEnd = prayer.Time.AddMinutes(10);
if (now >= blackoutStart && now <= blackoutEnd)
{
return true;
}
}
return false;
}
public async Task<DateTime> GetNextEzanEndTimeAsync()
{
var schedule = await GetDailyPrayerScheduleAsync();
var now = DateTime.Now;
foreach (var prayer in schedule.OrderBy(p => p.Time))
{
var blackoutStart = prayer.Time.AddMinutes(-5);
if (now < blackoutStart)
{
return blackoutStart.AddMinutes(15);
}
}
// If all prayers have passed, return first prayer of next day
return schedule.First().Time.AddDays(1).AddMinutes(10);
}
public async Task<IReadOnlyList<EzanSchedule>> GetDailyPrayerScheduleAsync()
{
var cacheKey = $"ezan_schedule_{DateTime.Now:yyyyMMdd}";
var cached = await _dataStore.ReadAsync<List<EzanSchedule>>(cacheKey);
if (cached != null)
{
return cached;
}
try
{
var response = await _httpClient.GetStringAsync(
$"{DiyanetApiUrl}?lat=39.9208&lon=32.8541&date={DateTime.Now:yyyy-MM-dd}");
var schedule = ParseEzanResponse(response);
// Cache for 24 hours (the schedule does not change intra-day)
await _dataStore.WriteAsync(cacheKey, schedule);
return schedule;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch Diyanet prayer schedule. Using fallback.");
// Fallback: approximate Ankara prayer times
return GetFallbackEzanSchedule();
}
}
private IReadOnlyList<EzanSchedule> GetFallbackEzanSchedule()
{
var baseDate = DateTime.Now.Date;
return new List<EzanSchedule>
{
new() { Name = "Imsak", Time = baseDate.AddHours(5) },
new() { Name = "Gunes", Time = baseDate.AddHours(6) },
new() { Name = "Ogle", Time = baseDate.AddHours(13) },
new() { Name = "Ikindi", Time = baseDate.AddHours(16) },
new() { Name = "Aksam", Time = baseDate.AddHours(19) },
new() { Name = "Yatsi", Time = baseDate.AddHours(21) }
};
}
}Warp blackout schedule enforcement
The Ezan blackout applies to:
- All player-initiated teleportation (durak-to-durak travel)
- Admin teleportation (admin teleports are exempt with special authorization)
- Plugin-triggered teleportation (unless emergency exemption is granted by the Diyanet module)
The blackout is announced server-wide 5 minutes before each ezan:
csharp
public class EzanBlackoutBackgroundService : IOpenModBackgroundService
{
private readonly IEzanService _ezanService;
public async Task ExecuteAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var schedule = await _ezanService.GetDailyPrayerScheduleAsync();
var now = DateTime.Now;
foreach (var prayer in schedule)
{
var warningTime = prayer.Time.AddMinutes(-5);
if (now >= warningTime.AddSeconds(-30)
&& now <= warningTime.AddSeconds(30))
{
await OpenMod.Broadcasting.BroadcastService.BroadcastAsync(
$"&e{prayer.Name} vakti 5 dakika icinde girecek. " +
$"Isinlanma hizmetleri gecici olarak durdurulacaktir.");
}
var blackoutStart = prayer.Time;
if (now >= blackoutStart.AddSeconds(-10)
&& now <= blackoutStart.AddSeconds(10))
{
await OpenMod.Broadcasting.BroadcastService.BroadcastAsync(
$"&e{prayer.Name} ezani okunuyor. " +
$"Isinlanma hizmetleri 10 dakika sureyle kapalidir.");
}
}
await Task.Delay(30000, cancellationToken);
}
}
}Construction permits
Creating a new durak requires a valid construction permit from the Ministry of Transportation. The permit process is separate from the TPE utility patent system used by TurkEssentials.
Permit types
| Permit type | Maximum duraklar | Duration | Fee | Processing time |
|---|---|---|---|---|
| Gezici (Traveler) | 3 | 90 days | 120 DLC | 10 business days |
| Standart (Standard) | 10 | 180 days | 360 DLC | 20 business days |
| Profesyonel (Professional) | 25 | 365 days | 720 DLC | 30 business days |
| Ticari (Commercial) | Unlimited | 730 days | 2,400 DLC | 45 business days |
90-day warp expiry and renewal
All duraklar expire 90 days after registration or 90 days after the last renewal, whichever comes first. The expiry is enforced at the data storage level: expired duraklar are automatically set to inactive status and cannot be used as teleport destinations.
csharp
public class DurakExpiryService : IOpenModScheduledTask
{
private readonly IDataStore _dataStore;
private readonly ILogger<DurakExpiryService> _logger;
public string ScheduleExpression => "0 0 * * *"; // Every hour
public string TaskName => "DurakExpiryCheck";
public async Task ExecuteAsync(CancellationToken cancellationToken)
{
var storage = OpenMod.Storage.DataStore.GetCollection("turkwarp_duraklar");
var activeDuraklar = await storage.FindAsync<Durak>(d => d.Status == DurakStatus.Active);
var now = DateTime.UtcNow;
var expired = 0;
foreach (var durak in activeDuraklar)
{
if (durak.ExpiresAt < now)
{
durak.Status = DurakStatus.Expired;
await storage.UpdateAsync(durak);
expired++;
_logger.LogInformation(
"DURAK_EXPIRY_AUTO: id={Id} name={Name} expired_at={Expires}",
durak.Id, durak.Name, durak.ExpiresAt);
}
}
if (expired > 0)
{
_logger.LogInformation(
"DURAK_EXPIRY_BATCH: {Count} duraklar expired in this cycle.", expired);
}
}
}Renewal process
Players can renew their duraklar through the /durak_yenile command. Renewal costs 50% of the original registration fee and extends the expiry by another 90 days. Renewals can be processed up to 30 days before expiry and up to 7 days after expiry. After the 7-day grace period, the durak is permanently deleted and a new registration with a new construction permit is required.
Commands reference
| Command | Permission | Description |
|---|---|---|
/durak [name] | turkwarp.durak.list | Lists available duraklar or shows info for a specific durak |
/durak_olustur <name> [type] | turkwarp.durak.create | Creates a new durak at the player's current position (requires construction permit) |
/durak_sil <name> | turkwarp.durak.delete | Deletes a durak owned by the player |
/durak_onayla | turkwarp.durak.confirm | Confirms the pending TCDD route booking and initiates teleport |
/durak_yenile <name> | turkwarp.durak.renew | Renews an expiring durak for another 90 days |
/insaat_izni [type] | turkwarp.permit.apply | Applies for a Ministry of Transportation construction permit |
/insaat_durumu | turkwarp.permit.status | Checks the status of a construction permit application |
/sefer_tarifesi | turkwarp.tcdd.schedule | Shows the current TCDD schedule for all active routes |
/ezan_durumu | turkwarp.ezan.status | Shows the current prayer time and warp blackout status |
/yht_bilet | turkwarp.ticket.yht | Purchase a YHT express teleport ticket (2x speed, 2x cost) |
Configuration reference
| Field | Type | Default | Description |
|---|---|---|---|
BaseRegistrationFee | decimal | 50 | Base fee for durak registration (DLC) |
PermitInspectionFee | decimal | 120 | Ministry of Transportation permit inspection fee (DLC) |
TollPerKmTl | decimal | 0.50 | Toll fee in Turkish Lira per kilometer traveled |
DurakExpiryDays | int | 90 | Number of days before a durak expires |
DurakRenewalCostMultiplier | decimal | 0.5 | Renewal cost as multiplier of original registration fee |
DurakGracePeriodDays | int | 7 | Days after expiry during which renewal is still possible |
EzanBlackoutEnabled | bool | true | Enable travel blackout during ezan calls |
EzanPreWarningMinutes | int | 5 | Minutes before ezan to begin blackout and broadcast warning |
EzanPostWaitMinutes | int | 10 | Minutes after ezan to end blackout |
TcddApiEndpoint | string | https://api.tcdd.gov.tr/tarife/v2 | TCDD Tarife API endpoint |
TcddApiKey | string | (required) | TCDD API subscription key |
MaxDuraklarPerPlayer | int | 10 | Maximum active duraklar per player (default for Standart permit) |
MinDurakDistanceMeters | float | 50 | Minimum distance between two duraklar |
AdminTeleportEzanExemption | bool | false | Allow admin teleportation during ezan blackout |
DiyanetApiEndpoint | string | https://api.diyanet.gov.tr/vakitler/v2 | Diyanet prayer time API |
Best practices
- Register duraklar at strategic locations. Each durak costs fees to register and maintain. Place duraklar at high-traffic areas like spawn points, shops, and popular player bases.
- Monitor TCDD API usage. The standard subscription includes 10,000 API calls per month. Route calculations use 1 call each, and schedule queries use 1 call each. Active servers with 50+ daily players can exhaust the quota within 2 weeks. Upgrade to the Ticari subscription (50,000 calls/month, 1,800 TL/year) for larger servers.
- Communicate the ezan schedule to players. Use the automated broadcast system to remind players about upcoming blackout periods. Consider adding a server MOTD that shows the day's prayer times.
- Renew duraklar before the grace period. The 7-day grace period after expiry is intended for emergencies. If the TCDD API or Ministry of Transportation systems are down, renewals cannot be processed during the outage.
- Use YHT express for admin travel. YHT tickets cost 2x the standard fare but halve the travel time. This is useful for admins who need to respond to incidents quickly.
Troubleshooting
TCDD API returns no available routes
This can happen if the TCDD API subscription has expired or the monthly call quota has been exhausted. Check the subscription status through the TCDD Tarife portal. If the quota is exhausted, routes will be calculated using the fallback regional-only algorithm until the quota resets at the start of the next month.
Player cannot create a durak despite having a construction permit
Verify that the permit has not expired and that the player has not exceeded their durak limit. The permit validation includes a durak capacity check. If the player has reached their limit, they must either delete an existing durak or upgrade to a higher-tier permit.
Teleport did not execute at the scheduled arrival time
The TCDD scheduling algorithm may recalculate if there are "rail traffic" changes on the virtual railway network. This is simulated by the plugin based on the number of concurrent teleport bookings. If the server has more than 10 pending teleports at once, the system introduces up to 5 minutes of additional delay. Check the current teleport queue depth with /sefer_tarifesi.
Ezan blackout did not trigger
The Diyanet API may have returned an error or the cached prayer schedule may be stale. The plugin caches the daily schedule for 24 hours. If the Diyanet API returns a server error during the daily schedule fetch, the plugin falls back to the approximate Ankara prayer times. These approximate times may be off by up to 15 minutes. To force a fresh schedule fetch, restart the server during a non-blackout period.
Conclusion
The TurkWarp plugin transforms OpenMod teleportation into a regulation-compliant transport network modeled on the Turkish intercity travel system. By implementing the durak bus stop system with Ministry of Transportation construction permits, TCDD railway scheduling algorithms for routing, distance-based toll pricing, and ezan prayer time travel blackouts, server operators can offer a culturally authentic teleportation experience.
The next article in this series covers the TurkKit plugin, which implements kits as Ministry of Health-approved erzak paketi (provision packages) with mandatory PSA announcements and business day cooldowns.
