static void OnPlayerCommand(Player p, string cmd, string args, CommandData data)
        {
            if (cmd.CaselessEq("skin") && Hacks.CanUseHacks(p))
            {
                var splitArgs = args.Trim().Length == 0 ? new string[] { } : args.SplitSpaces();
                if (splitArgs.Length == 0)
                {
                    // check if we should use default skin
                    var storedModel = new StoredCustomModel(p.Model);
                    if (storedModel.Exists())
                    {
                        storedModel.LoadFromFile();

                        if (
                            !storedModel.usesHumanSkin &&
                            storedModel.defaultSkin != null
                            )
                        {
                            Debug("Setting {0} to defaultSkin {1}", p.name, storedModel.defaultSkin);
                            Command.Find("Skin").Use(
                                p,
                                "-own " + storedModel.defaultSkin,
                                data
                                );
                            p.cancelcommand = true;
                        }
                    }
                    else if (splitArgs.Length > 0)
                    {
                        var last = splitArgs[splitArgs.Length - 1];
                        MemoizedGetSkinType.Invalidate(last);
                    }
                }
            }
        }
        static void CheckSendModel(Player p, string modelName)
        {
            lock (SentCustomModels) {
                var sentModels = SentCustomModels[p.name];

                if (!sentModels.Contains(modelName))
                {
                    var storedModel = new StoredCustomModel(modelName);
                    if (storedModel.Exists())
                    {
                        storedModel.LoadFromFile();
                        storedModel.Define(p);
                        sentModels.Add(modelName);
                    }
                }
            }
        }
            void Wear(Player p, string modelName, CommandData data)
            {
                // check if we should use default skin
                var storedCustomModel = new StoredCustomModel(modelName);

                if (!storedCustomModel.Exists())
                {
                    p.Message("%WCustom Model %S{0} %Wnot found!", modelName);
                    return;
                }
                p.HandleCommand("XModel", modelName, data);

                storedCustomModel.LoadFromFile();

                if (
                    !storedCustomModel.usesHumanSkin &&
                    storedCustomModel.defaultSkin != null
                    )
                {
                    p.HandleCommand("Skin", "-own " + storedCustomModel.defaultSkin, data);
                }
            }
            void Goto(Player p, string playerName = null, ushort page = 0)
            {
                bool all = false;

                if (playerName != null)
                {
                    playerName = playerName.ToLower();
                    if (playerName == "all")
                    {
                        all = true;
                    }
                    else if (playerName == "public")
                    {
                        playerName = null;
                    }
                    else
                    {
                        playerName = StoredCustomModel.GetPlayerName(playerName) ?? StoredCustomModel.GetPlayerName(playerName + "+");
                    }
                }

                List <string> modelNames;

                if (all)
                {
                    var dict = GetAllModels(p);
                    modelNames = dict
                                 // public ones first
                                 .OrderByDescending(pair => pair.Key == "Public")
                                 // then by player name A-Z
                                 .ThenBy(pair => pair.Key)
                                 .Select(pair => pair.Value)
                                 .SelectMany(x => x)
                                 .ToList();
                }
                else
                {
                    modelNames = GetModels(playerName, p);
                }
                if (modelNames == null)
                {
                    return;
                }

                // - 1 for our self player
                var partitionSize = Packet.MaxCustomModels - 1;
                var partitions    = modelNames.Partition(partitionSize).ToList();

                if (page >= partitions.Count)
                {
                    p.Message(
                        "%WPage doesn't exist"
                        );
                    return;
                }
                var total = modelNames.Count;

                modelNames = partitions[page];
                p.Message(
                    "%HViewing %T{0} %Hmodels{1}",
                    total,
                    partitions.Count > 1
                        ? string.Format(
                        " %S(page %T{0}%S/%T{1}%S)",
                        page + 1,
                        partitions.Count
                        )
                        : ""
                    );
                if (partitions.Count > 1 && page < (partitions.Count - 1))
                {
                    p.Message(
                        "%SUse \"%H/cm goto {0} {1}%S\" to go to the next page",
                        playerName,
                        page + 2
                        );
                }

                var mapName = string.Format(
                    "&f{0} Custom Models{1}",
                    all ? "All" : (
                        playerName == null
                            ? "Public"
                            : GetNameWithoutPlus(playerName) + "'s"
                        ),
                    page != 0 ? string.Format(" ({0})", page + 1) : ""
                    );

                ushort spacing = 4;
                ushort width   = (ushort)(
                    // edges
                    (spacing * 2) +
                    // grass blocks
                    modelNames.Count +
                    // inbetween blocks
                    ((modelNames.Count - 1) * (spacing - 1))
                    );
                ushort height = 1;
                ushort length = 16;

                byte[] blocks = new byte[width * height * length];

                Level lvl = new Level(mapName, width, height, length, blocks);

                for (int i = 0; i < blocks.Length; i++)
                {
                    blocks[i] = 1;
                }

                lvl.SaveChanges        = false;
                lvl.ChangedSinceBackup = false;

                lvl.IsMuseum        = true;
                lvl.BuildAccess.Min = LevelPermission.Nobody;
                lvl.Config.Physics  = 0;

                lvl.spawnx = spacing;
                lvl.spawny = 2;
                lvl.Config.HorizonBlock = 1;
                lvl.Config.EdgeLevel    = 1;
                lvl.Config.CloudsHeight = -0xFFFFFF;
                lvl.Config.SidesOffset  = 0;
                lvl.Config.Buildable    = false;
                lvl.Config.Deletable    = false;

                for (ushort i = 0; i < modelNames.Count; i++)
                {
                    ushort x = (ushort)(spacing + (i * spacing));
                    ushort y = 0;
                    ushort z = spacing;

                    blocks[lvl.PosToInt(x, y, z)] = 2;

                    var modelName = modelNames[i];

                    var storedModel = new StoredCustomModel(modelName);
                    if (storedModel.Exists())
                    {
                        storedModel.LoadFromFile();
                    }

                    var skinName = p.SkinName;
                    if (
                        !storedModel.usesHumanSkin &&
                        storedModel.defaultSkin != null
                        )
                    {
                        skinName = storedModel.defaultSkin;
                    }

                    // hack because clients strip + at the end
                    var botName = "&f" + (modelName.EndsWith("+") ? modelName + "&0+" : modelName);
                    var bot     = new PlayerBot(botName, lvl)
                    {
                        id       = (byte)i,
                        Model    = modelName,
                        SkinName = skinName,
                    };
                    bot.SetInitialPos(Position.FromFeetBlockCoords(x, y + 1, z));
                    bot.SetYawPitch(Orientation.DegreesToPacked(180), Orientation.DegreesToPacked(0));
                    bot.ClickedOnText = "/CustomModel wear " + modelName;

                    _ = lvl.Bots.Add(bot);
                }

                if (!PlayerActions.ChangeMap(p, lvl))
                {
                    return;
                }
            }
            void Config(Player p, string modelName, List <string> args)
            {
                StoredCustomModel storedCustomModel = new StoredCustomModel(modelName, true);

                if (!storedCustomModel.Exists())
                {
                    p.Message("%WCustom Model %S{0} %Wnot found!", modelName);
                    return;
                }

                storedCustomModel.LoadFromFile();

                if (args.Count == 0)
                {
                    // /CustomModel [name] config
                    foreach (var entry in ModifiableFields)
                    {
                        var fieldName  = entry.Key;
                        var modelField = entry.Value;
                        if (!modelField.CanEdit(p, modelName))
                        {
                            continue;
                        }

                        p.Message(
                            "{0} = %T{1}",
                            fieldName,
                            modelField.get.Invoke(storedCustomModel)
                            );
                    }
                    return;
                }

                if (args.Count >= 1)
                {
                    // /CustomModel [name] config [field]
                    // or
                    // /CustomModel [name] config [field] [value]
                    var fieldName = args.PopFront();
                    if (!ModifiableFields.ContainsKey(fieldName))
                    {
                        p.Message(
                            "%WNo such field %S{0}!",
                            fieldName
                            );
                        return;
                    }

                    var modelField = ModifiableFields[fieldName];
                    if (args.Count == 0)
                    {
                        // /CustomModel [name] config [field]
                        p.Message(
                            "{0} = %T{1}",
                            fieldName,
                            modelField.get.Invoke(storedCustomModel)
                            );
                        return;
                    }
                    else
                    {
                        // /CustomModel config [field] [value]...
                        var values = args.ToArray();
                        if (values.Length != modelField.types.Length)
                        {
                            p.Message(
                                "%WNot enough values for setting field %S{0}",
                                fieldName
                                );
                        }
                        else
                        {
                            if (!modelField.CanEdit(p, modelName))
                            {
                                p.Message("%WYou can't edit this field on a primary personal model!");
                                return;
                            }

                            if (modelField.set.Invoke(storedCustomModel, p, values))
                            {
                                // field was set, update file!
                                p.Message("%TField %S{0} %Tset!", fieldName);

                                storedCustomModel.WriteToFile();
                                CheckUpdateAll(storedCustomModel);
                            }
                        }
                    }
                }
            }