• Unfortunately, we have experienced significant hard drive damage that requires urgent maintenance and rebuilding. The forum will be a state of read only until we install our new drives and rebuild all the configurations needed. Please follow our Facebook page for updates, we will be back up shortly! (The forum could go offline at any given time due to the nature of the failed drives whilst awaiting the upgrades.) When you see an Incapsula error, you know we are in the process of migration.

Official-Like Mob Spawn Rate

Skilled Illusionist
Joined
Jul 17, 2010
Messages
333
Reaction score
165
I was just wondering why 'MapSize' and 'MobCapacity' in my server didn't correspond with official server's.
So I decided to investigate what's wrong with my server and have achieved reproducing the official-like mob respawn system.

*Disclaimer*
It doesn't include full source code but pieces of code, and is missing TimeMob, FixedMob and MCMob [STRIKE]but who cares?[/STRIKE]

Special thanks go to authors of MoopleDEV, SwordieMS, and other open sources that I lee...arned from. :rolleyes:


1. Get correct 'MapSize' and calculate 'MobCapacity'.
Code:
// ref. CFieldMan::RegisterField (aka FieldFactory#getField, MapleMapFactory#getMap or FieldData#readFieldFromFile or etc...)
// *snip*
// ref. CFieldMan::RestoreFoothold
int mbrLeft = Integer.MAX_VALUE;
int mbrTop = Integer.MAX_VALUE;
int mbrRight = Integer.MIN_VALUE;
int mbrBottom = Integer.MIN_VALUE;
for (/* iterate footholds (from a xml(wz) file or a dat file or etc...) */) {
    // make a foothold and add it to the footholds list (omitted)
    int left;
    int top;
    int right;
    int bottom;
    if (fh.getX1() >= fh.getX2()) {
        left = fh.getX2();
        right = fh.getX1();
    } else {
        left = fh.getX1();
        right = fh.getX2();
    }
    if (fh.getY1() >= fh.getY2()) {
        top = fh.getY2();
        bottom = fh.getY1();
    } else {
        top = fh.getY1();
        bottom = fh.getY2();
    }
    if (mbrLeft > left + 30) {
        mbrLeft = left + 30;
    }
    if (mbrRight < right - 30) {
        mbrRight = right - 30;
    }
    if (mbrTop > top - 300) {
        mbrTop = top - 300;
    }
    if (mbrBottom < bottom + 10 && [B]left != right[/B]) { // Ignore bottom footholds where you can't walk. Is it not here? but ok.
        mbrBottom = bottom + 10;
    }
}
if (field.isVrLimit()) { // in rare cases
    if (field.getVrLeft() != 0 && mbrLeft < field.getVrLeft() + 20) {
        mbrLeft = field.getVrLeft() + 20;
    }
    if (field.getVrRight() != 0 && mbrRight > field.getVrRight() - 20) {
        mbrRight = field.getVrRight() - 20;
    }
    if (field.getVrTop() != 0 && mbrTop < field.getVrTop() + 65) {
        mbrTop = field.getVrTop() + 65;
    }
    if (field.getVrBottom() != 0 && mbrBottom > field.getVrBottom()) {
        mbrBottom = field.getVrBottom();
    }
}
Rect mbr = new Rect(mbrLeft, mbrTop, mbrRight, mbrBottom);
mbr.inflate(10, 10); // left - 10, top - 10, right +10, bottom +10
field.setMbr(mbr);
field.setLeftTop(new Point(mbr.getLeft(), mbr.getTop()));
field.setMapSize(new Point(mbr.getWidth(), mbr.getHeight())); // right - left, bottom - top
// *snip*
// ref. CLifePool::Init
int width = Math.max(800, field.getMapSize().getX());
int height = Math.max(600, field.getMapSize().getY() - 450);
int capacity = Math.max(1, Math.min(40, (int) ((height * width) * field.getMobRate() * 0.0000078125)));
field.setMobCapacityMin(capacity);
field.setMobCapacityMax(capacity * 2);

2. Modify MobGen (aka SpawnPoint) and the respawn function.
Code:
// MobGen (aka SpawnPoint or whatever)
    // int regenInterval;
    // long regenAfter;
    // AtomicInteger mobCount = new AtomicInteger(0);  // tbh it's replaceable with a boolean
    /*
      initialize (constructor)
      {
        // omitted again, sorry. (set templateId and position, fh, etc...)
        regenInterval = mobTime; //'mobTime' * 1000
        if (regenInterval < 0) {
            regenAfter = 0;
        } else {
            int rAfter;
            if (regenInterval == 0) {
                rAfter = Randomizer.nextInt(Integer.MAX_VALUE); // whatever
            } else {
                rAfter = Randomizer.rand(regenInterval / 10, (int) Math.round(regenInterval * 0.7) - 1);
            }
            regenAfter = rAfter + System.currentTimeMillis();
            /* old
            int rAfter = 0;
            int rand = 6 * regenInterval / 10;
            if (rand > 0) {
                rAfter = regenInterval / 10 + Randomizer.nextInt(Integer.MAX_VALUE) % rand;
            } else {
                rAfter = Randomizer.nextInt(Integer.MAX_VALUE);
            }
            regenAfter = rAfter + System.currentTimeMillis();
            */
        }
      }
    */

    /**
     * There are 3 types of MobGens and they are distinguished by their 'mobTime'. 
     * 0: can summon multiple mobs per a MobGen if there's no mobs around it. (up to MobCapacity)
     * x > 0: summon one mob per a MobGen with an interval.
     * -1: summon only once when 'reset'.
     *          [USER=850422]return[/USER] 0: rejected 1: to first position 2: to last position
     */
    public int canSpawnMob(long curTime, boolean reset, List<Point> lpt) {
        if (regenInterval == 0) {
            if (!lpt.isEmpty()) {
                Rect rect = new Rect(getPosition().getX() - 100, getPosition().getY() - 100, getPosition().getX() + 100, getPosition().getY() + 100);
                for (Point pt : lpt) {
                    if (rect.isPtInRect(pt)) {
                        return 0;
                    }
                }
            }
            return 2;
        } else if (regenInterval > 0) {
            return mobCount.get() < 1 && regenAfter <= curTime ? 1 : 0;
        } else {
            return reset ? 1 : 0;
        }
    }

    /**
     * ref. CMob::Init
     * This function is not needed here but in any case you must assign the MobGen to a mob and increment 'mobCount' when the mob summoned.
     */ 
    public void setMobCreated() {
        if (regenInterval != 0) {
            mobCount.incrementAndGet();
        }
    }

    /**
     * ref. CMob::SetRemoved
     * This function is not needed here but in any case you must set 'regenAfter' and decrement 'mobCount' when the summoned mob died.
     */ 
    public void setMobRemoved() {
        if (regenInterval != 0) {
            if (mobCount.decrementAndGet() == 0) {
                int rAfter;
                if (regenInterval > 0) {
                    rAfter = Randomizer.rand((int) Math.round(regenInterval * 1.3), regenInterval * 2 - 1);
                } else {
                    rAfter = Randomizer.nextInt(Integer.MAX_VALUE); // whatever
                }
                regenAfter = rAfter + System.currentTimeMillis();
                /* old
                int rAfter;
                int rand = 7 * regenInterval / 10;
                if (rand != 0) {
                    rAfter = 13 * regenInterval / 10 + Randomizer.nextInt(Integer.MAX_VALUE) % rand;
                } else {
                    rAfter = Randomizer.nextInt(Integer.MAX_VALUE);
                }
                regenAfter = rAfter + System.currentTimeMillis();
                */
            }
        }
    }
Code:
// ref. ClifePool::TryCreateMob (aka MapleMap#respawn or Field#generateMob or something)
// *snip*
if (getMobGens().isEmpty()) {
    return;
}
long curTime = System.currentTimeMillis();
// check last created time if you're using a field updater. 
// eg. if (reset || curTime - lastCreateMobTime >= GameConstants.BASE_MOB_RESPAWN_TIME * (isKishinActivated() ? GameConstants.KISHIN_MOB_TIME_MULTIPLIER : 1)) // 7000 * 0.5 or 1
// oh, maybe I should've wrote it like GameConstants.MOB_RES`PAWN_TIME[getKishinLevel()]
int cap;
if ((getFieldLimit() & FieldOpt.NOMOBCAPACITYLIMIT.getValue()) != 0) {
    cap = getMobGens().size();
} else if (getFixedMobCapacity() > 0) {
    cap = getFixedMobCapacity();
} else {
    int mobCapacityMin = getMobCapacityMin();
    int mobCapacityMax = getMobCapacityMax();
    int controllerCount = getUsers().size(); // not sure
    cap = mobCapacityMin;
    if (isKishinActivated()) { // is this here?
        cap *= GameConstants.KISHIN_MOB_CAP_MULTIPLIER; // 1.5
    }
    if (controllerCount > mobCapacityMin / 2) {
        cap += (mobCapacityMax - mobCapacityMin) * (2 * controllerCount - mobCapacityMin) / (3 * mobCapacityMin); // not sure
    }
    cap = Math.min(cap, mobCapacityMax);
}
int remainCapacity = cap - getMobs().size();
if (remainCapacity > 0) {
    List<Point> lpt = new ArrayList<>();
    for (Mob mob : getMobs()) {
        lpt.add(mob.getPosition());
    }
    LinkedList<MobGen> mobGenPossible = new LinkedList<>();
    for (MobGen mobGen : getMobGens()) {
        switch (mobGen.canSpawnMob(curTime, reset, lpt)) {
            case 1:
                mobGenPossible.addFirst(mobGen);
                break;
            case 2:
                mobGenPossible.addLast(mobGen);
                break;
            default:
                continue;
        }
        lpt.add(mobGen.getPosition());
    }
    lpt.clear();
    do {
        if (mobGenPossible.isEmpty()) {
            break;
        }
        int index = 0;
        MobGen mobGen = mobGenPossible.getFirst();
        if (mobGen.getRegenInterval() == 0 && mobGenPossible.size() > 1) {
            index = Randomizer.nextInt(Integer.MAX_VALUE) % mobGenPossible.size();
            mobGen = mobGenPossible.get(index);
        }
        if (createMob(mobGen.getTemplateId(), mobGen, mobGen.getPosition().getX(), mobGen.getPosition().getY(), mobGen.getFh(), false, -2, 0, mobGen.getF(), 0))) {
            remainCapacity--;
        }
        mobGenPossible.remove(index);
    } while (remainCapacity > 0);
    mobGenPossible.clear();
    // set last created time if you're using a field updater
}
It's basically based on BMS's leaked files but they still seem to use that formula in up-to-date server. :thumbup1:

*Edited*
・modified a formula for 'regenInterval' so that you can easily know that 'mobTime' x1.3 to x2 is the actual respawn interval.
・moved a comment "// set last created time if you're using a field updater" to within a bracket. it should be set only when 'remaingCapacity' > 0.
(which means, if you have a field updater (I mean a repeating timer which updates a field every second instead of 7 seconds), then, when the elapsed time since the capacity is reached exceeds 7 seconds, it will summon mobs within a second after you kill mobs. and I think it's official-like behavior.)
 
Last edited:
Custom Title Activated
Loyal Member
Joined
Jan 18, 2010
Messages
3,109
Reaction score
1,139
Always love seeing your Nexon releases, hopefully more people start utilizing things like this. Nexon's whole mob controller heaps are kinda weird, but you basically got it all down.

Only thing I always wondered was if the capacity formula ever changed or not. At least for my version being so old it doesn't much matter, but I feel like more mobs spawn in GMS (not to mention spawn faster).
 
warp(california, "home");
Joined
Sep 16, 2008
Messages
294
Reaction score
103
Nice dude. Can we use this for the HeavenMS source? I'm getting back into MapleStory after many years.
 
Skilled Illusionist
Joined
Jul 17, 2010
Messages
333
Reaction score
165
Always love seeing your Nexon releases, hopefully more people start utilizing things like this. Nexon's whole mob controller heaps are kinda weird, but you basically got it all down.

Only thing I always wondered was if the capacity formula ever changed or not. At least for my version being so old it doesn't much matter, but I feel like more mobs spawn in GMS (not to mention spawn faster).
Thank you for the reply :)

Well... I'm sure that at least in JMS, the formulas for the map size and the capacity are still the same as BMS's, as I've confirmed it in 30 hunting maps (btw I got actual map size from SetField packet to compare).
(If anyone is interested, here is a list of generated map size and mob capacity by that formula, it's for JMS though. View attachment FieldMobCount_V379_en_edit.txt(csv))

Oh, and the formula for 'regenAfter' on mobs which have positive 'mobTime' value also seems to be correct although I have no idea about the monster respawn time. (I mean, is it really 7000 milliseconds? it's like 7.1~7.7 sec in actual measured value but :huh:)

Nice dude. Can we use this for the HeavenMS source? I'm getting back into MapleStory after many years.
Yes, of course.
 

Attachments

You must be registered for see attachments list
Last edited:
Newbie Spellweaver
Joined
Nov 6, 2020
Messages
15
Reaction score
0
thanx bro. i have question, im new in the (maplestory private server opening) and i open yesterday v83 private, and i dont find the way that how can i take monsters from specific map and put them into other map... i try to use there ID and try to find them in repak file but when i change things there..., so or nothing change at all or the admin command are not working but the changing from the repak the lvl of the mobs and hp and exp and things like that.... is nothing change.
 
Back
Top