public StoredCustomModel(string name, bool overrideFileName = false)
            {
                this.modelName = ModelInfo.GetRawModel(name);
                this.scale     = ModelInfo.GetRawScale(name);

                var split = this.modelName.Split(new char[] { '(' }, StringSplitOptions.RemoveEmptyEntries);

                // "player+named", "aaa,bbbb)"
                if (split.Length == 2 && split[1].EndsWith(")"))
                {
                    if (overrideFileName || this.Exists())
                    {
                        // if "player+ (sit)" was a file, use that as override
                        this.fileName = this.modelName;
                    }
                    this.modelName = split[0];

                    // remove ")"
                    var attrs = split[1].Substring(0, split[1].Length - 1);
                    foreach (var attr in attrs.SplitComma())
                    {
                        if (attr.Trim() == "")
                        {
                            continue;
                        }
                        this.modifiers.Add(attr);
                    }
                }
            }
        // sends all missing models in level to player,
        // and removes all unused models from player
        static void CheckAddRemove(Player p, Level level)
        {
            Debug("CheckAddRemove {0}", p.name);

            var visibleModels = new HashSet <string>(StringComparer.OrdinalIgnoreCase)
            {
                ModelInfo.GetRawModel(p.Model)
            };

            if (!level.IsMuseum)
            {
                foreach (Player e in level.getPlayers())
                {
                    visibleModels.Add(ModelInfo.GetRawModel(e.Model));
                }
            }
            else
            {
                visibleModels.Add(ModelInfo.GetRawModel(p.Model));
            }
            foreach (PlayerBot e in level.Bots.Items)
            {
                visibleModels.Add(ModelInfo.GetRawModel(e.Model));
            }

            if (p.Extras.TryGet("TempBot_BotList", out object obj))
            {
                if (obj != null)
                {
                    List <PlayerBot> botList = (List <PlayerBot>)obj;
                    foreach (var bot in botList)
                    {
                        visibleModels.Add(ModelInfo.GetRawModel(bot.Model));
                    }
                }
            }


            // we must remove old models first before new models so that we have enough CM id's,
            // but removing first will cause a couple ms of humanoid to be shown before the new model arrives
            // TODO maybe check if we're about to overflow, and flip order?

            lock (SentCustomModels) {
                var sentModels = SentCustomModels[p.name];
                // clone so we can modify while we iterate
                foreach (var modelName in sentModels.ToArray())
                {
                    // remove models not found in this level
                    if (!visibleModels.Contains(modelName))
                    {
                        CheckRemoveModel(p, modelName);
                    }
                }
            }

            // send new models not yet in player's list
            foreach (var modelName in visibleModels)
            {
                CheckSendModel(p, modelName);
            }
        }