Skip to content
RustPluginGenerator
Runtime errorOxide + Carbon

Fix "KeyNotFoundException: The given key was not present in the dictionary" in Oxide plugins

A hook tried to read a dictionary entry with an indexer for a key that was never added. For player-tracking plugins this usually means the player connected before the plugin loaded. Use TryGetValue or add the key on join.

Server console
Failed to call hook 'OnPlayerDisconnected' on plugin 'PlaytimeTracker v1.0.0' (KeyNotFoundException: The given key was not present in the dictionary.)
  at System.Collections.Generic.Dictionary`2[TKey,TValue].get_Item (TKey key) [0x0001e] in <b0e1ad7573a24fd5a9f2af9595e677e7>:0
  at Oxide.Plugins.PlaytimeTracker.OnPlayerDisconnected (BasePlayer player, System.String reason) [0x00012] in <8f3c1a2b4d5e4f6a9b0c1d2e3f4a5b6c>:0

Why it happens

Dictionary<TKey, TValue> throws KeyNotFoundException when you access dict[key] and the key is absent. In OnPlayerDisconnected the key is typically a player ID string. Players who were already connected when you called oxide.reload (or when the server started with the plugin late) never fired OnPlayerConnected, so their ID was never written into the dictionary. The same applies to data loaded from disk that predates the current session, to keys removed earlier in a disconnect handler that ran twice, and to code that mixes player.UserIDString with a ulong key type so the lookup never matches. Oxide keeps the plugin loaded after this exception and keeps calling the hook, so you will see the error every time that player disconnects.

How to fix it

  1. Replace every dict[key] read on player-driven paths with TryGetValue and handle the missing case.
  2. Seed the entry in OnPlayerConnected (and in OnServerInitialized for already-connected players) with a zero default.
  3. Make the key type consistent: pick player.UserIDString or player.userID.Get(), not a mix.
  4. Keep removal idempotent with Remove(key), which returns false if the key is absent instead of throwing.
  5. Make sure no other dictionary write in another hook stores a different key format, because that creates the same mismatch later.
  6. Reload with oxide.reload PlaytimeTracker, join the server, then disconnect. The console should print Unloaded plugin PlaytimeTracker v1.0.1 and Loaded plugin PlaytimeTracker v1.0.1, and no Failed to call hook 'OnPlayerDisconnected' line should appear.

Before and after

BeforeFails
using System.Collections.Generic;
using Oxide.Core;
using Oxide.Core.Plugins;

namespace Oxide.Plugins
{
    [Info("PlaytimeTracker", "RustPluginGenerator", "1.0.0")]
    [Description("Tracks how long each player has been connected.")]
    public class PlaytimeTracker : RustPlugin
    {
        private Dictionary<string, double> joinTimes = new Dictionary<string, double>();

        private void OnServerInitialized()
        {
            joinTimes = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, double>>("PlaytimeTracker") ?? new Dictionary<string, double>();
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            if (player == null) return;
            joinTimes[player.UserIDString] = UnityEngine.Time.realtimeSinceStartup;
            Interface.Oxide.DataFileSystem.WriteObject("PlaytimeTracker", joinTimes);
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player == null) return;

            double joined = joinTimes[player.UserIDString];
            double seconds = UnityEngine.Time.realtimeSinceStartup - joined;
            Puts(player.displayName + " played " + (int)seconds + "s");

            joinTimes.Remove(player.UserIDString);
            Interface.Oxide.DataFileSystem.WriteObject("PlaytimeTracker", joinTimes);
        }
    }
}
AfterFixed
using UnityEngine;
using System.Collections.Generic;
using Oxide.Core;
using Oxide.Core.Plugins;

namespace Oxide.Plugins
{
    [Info("PlaytimeTracker", "RustPluginGenerator", "1.0.1")]
    [Description("Tracks how long each player has been connected.")]
    public class PlaytimeTracker : RustPlugin
    {
        private Dictionary<string, double> joinTimes = new Dictionary<string, double>();

        private void OnServerInitialized()
        {
            joinTimes = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, double>>("PlaytimeTracker") ?? new Dictionary<string, double>();

            // Players already online never fired OnPlayerConnected.
            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null) continue;
                joinTimes[player.UserIDString] = UnityEngine.Time.realtimeSinceStartup;
            }
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            if (player == null || string.IsNullOrEmpty(player.UserIDString)) return;
            joinTimes[player.UserIDString] = UnityEngine.Time.realtimeSinceStartup;
            Interface.Oxide.DataFileSystem.WriteObject("PlaytimeTracker", joinTimes);
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player == null || string.IsNullOrEmpty(player.UserIDString)) return;

            double joined;
            if (!joinTimes.TryGetValue(player.UserIDString, out joined))
            {
                // No start time recorded (connected before load). Start counting now and skip.
                joinTimes[player.UserIDString] = UnityEngine.Time.realtimeSinceStartup;
                return;
            }

            double seconds = UnityEngine.Time.realtimeSinceStartup - joined;
            Puts(player.displayName + " played " + (int)seconds + "s");

            joinTimes.Remove(player.UserIDString);
            Interface.Oxide.DataFileSystem.WriteObject("PlaytimeTracker", joinTimes);
        }
    }
}

FAQ

Why does the error only happen for some players?

Those players connected before the plugin was loaded or reloaded, so OnPlayerConnected never ran for them and their entry is missing from the dictionary. Players who join after the reload are tracked normally.

Does the exception unload the plugin?

No. Oxide logs Failed to call hook ... with a stack trace, keeps the plugin loaded and calls the hook again next time. That is why you see repeated lines instead of one compile or load failure.

Should I use UserIDString or the ulong userID as the key?

Either works as long as you are consistent. UserIDString is convenient because the permission and lang APIs take it; player.userID.Get() avoids string allocation. Never write with one and read with the other.

Different error?

Paste the full console output and get the cause and a fix in plain English.

Open the error explainer

Skip the debugging

Generated plugins are compiled before you get them, and compiler errors are fixed automatically.

Generate a plugin

2 free plugins a month, no card.

Last updated Oct 26, 2026.

Get plugins that compile first time

Generate