Jump to content
Moopler Closing Read more... ×
Sign in to follow this  

Information Map Rusher Write Up (v83)

Recommended Posts

Part I: Structs


In the way I setup map rusher, there are three structs that I define to hold all the data I need: 

1. MapData - This struct holds the map ID value along with the island name/street name/map name. It also contains a list of the portals that the map contains

ref struct MapData {
	int mapID;
	System::String^ islandName;
	System::String^ streetName;
	System::String^ mapName;
	System::Collections::Generic::List<PortalData^>^ portals;

2. PortalData - This struct holds the info about each portal (portal name, type, x/y coords, and connecting map id)

ref struct PortalData {
	System::String^ portalName;
	int portalType;
	int xPos;
	int yPos;
	int toMapID;

3. MapPath - This struct holds the info about a single node in the path to the destination map. So it contains the map id along with the portal that is used to connect to the next map in the path

ref struct MapPath {
	int mapID;
	PortalData^ portal;

Part 2: Loading the data 


On form shown event, a global variable "GlobalRefs::maps" is allocated and filled with all the maps read from dumped file. A helper function getMap() retrieves maps from GlobalRefs::maps based on map id passed in.

GlobalRefs::maps = gcnew Generic::List<MapData^>(); 

void loadMaps(){
 	//Method parses through the file and creates a MapData struct, and attaches a PortalData struct to it. Too long to put here, but should be simple to understand

//Gets MapData^ of the passed in mapID. Callee function checks if nullptr is returned
static MapData^ getMap(int mapID) {
	for each(MapData^ map in GlobalRefs::maps)
		if (map->mapID == mapID)
			return map;
	return nullptr;

Part 3: Recursive DFS


When the user clicks Map Rush after putting in a Destination Map ID, the button click event calls Map Rush(), which in turn calls generatePath() to generate a path as a vector<MapPath>.

The generatePath() takes in the start/end Map IDs and creates 2 vectors of type MapPath. One is for a temporary list that is used for building the vector, and one is for the final list. Both of which are passed in by reference to the existsInNextMapDFS(). existsInNextMapDFS() modifies the passed in finalPath vector and returns the shortest path it could find.

//Uses recursive existsInNextMapDFS() to generate a path
cliext::vector<MapPath^>^ generatePath(int startMapID, int destMapID) {
	cliext::vector<MapPath^> ^searchList = gcnew cliext::vector<MapPath^>(), ^finalPath = gcnew cliext::vector<MapPath^>();
	existsInNextMapDFS(startMapID, startMapID, destMapID, 0, searchList, finalPath); //Gets shortest path and puts it into finalPath (if there exists a path)
	return finalPath;

If the current map matches the destination map, the path the recursive DFS has stored in searchList is placed into the finalPath that was passed in. But only if the finalPath's size is greater than the current searchList path, because the shorter path is used as the final path. 

The recursive DFS returns if the map it is currently searching has no portals (ie endpoint of graph) or if the number of recursions has exceeded 300 (path should've been found by then)

It loops through all the portals within the current map and for each, it recursively calls existsInNextMapDFS() for each of the portals (if the path is found in the next call, it is put into finalPath, and the search continues). 

The loop skips portals where the portal's linking map id does not exist in the loaded maps (GlobalRefs::maps), and it skips portals that have already been searched (if it is in the searchList already) to prevent loops.

//Recursive Depth First Search (DFS) to find path
void existsInNextMapDFS(int currMapID, int startMapID, int destMapID, int numRecursions, cliext::vector<MapPath^>^ searchList, cliext::vector<MapPath^>^ finalPath) {
	if (currMapID == destMapID) {
		if ((int)(finalPath->size()) == 0 || finalPath->size() > searchList->size()) 
			*finalPath = searchList; //Current path is the shortest path to destination map
		return; //Returning so that no further maps from this one are searched

	if (getMap(currMapID)->portals->Count == 0 || numRecursions > 300) 
		return; //If current map is an endpoint or if number of recursions are over 300, no further maps are searched

	for each(PortalData^ portalData in getMap(currMapID)->portals) {
		bool existsInSearchList = false;
		for each(MapPath^ mapData in searchList) 
			if (mapData->mapID == portalData->toMapID) existsInSearchList = true;

		if (getMap(portalData->toMapID) == nullptr) continue; //Skips portals where the portal's map is not found
		if (existsInSearchList) continue; //Skip portals where it goes to maps already in search path to prevent loop backs

		MapPath^ mapPath = gcnew MapPath(currMapID, portalData);
		existsInNextMapDFS(portalData->toMapID, startMapID, destMapID, numRecursions + 1, searchList, finalPath); //Recursive call


Part 4: Map Rushing Preparations


Now that the path has been generated for the MapRush(), it checks to see if the starting map's island is the same as the destination map's island. If not, see part 6 for map rushing between islands. Couple error checks are done at this point:  Starting Map can't be the same as Destination Map & a valid path has to be generated for map rush to continue

Now in order to prepare for map rushing, toggleFastMapRushHacks() is called. This basically sets the No Map Background/Tiles/Objects/Fade In/Fade Out hacks to true (and saves the old state to restore at the end of the function). 


//Enables hacks to make map rush faster
void toggleFastMapRushHacks(bool isChecked) {
    if(isChecked) {
        //A way to save the original state of the hacks to restore later without needing to create global vars
        if (MainForm::TheInstance->cbNoMapFadeEffect->Checked) MainForm::TheInstance->cbNoMapFadeEffect->ForeColor = Color::Green;
        if (MainForm::TheInstance->cbNoMapBackground->Checked) MainForm::TheInstance->cbNoMapBackground->ForeColor = Color::Green;
        if (MainForm::TheInstance->cbNoMapTiles->Checked) MainForm::TheInstance->cbNoMapTiles->ForeColor = Color::Green;
        if (MainForm::TheInstance->cbNoMapObjects->Checked) MainForm::TheInstance->cbNoMapObjects->ForeColor = Color::Green;
        MainForm::TheInstance->cbNoMapFadeEffect->Checked = true;
        MainForm::TheInstance->cbNoMapBackground->Checked = true;
        MainForm::TheInstance->cbNoMapTiles->Checked = true;
        MainForm::TheInstance->cbNoMapObjects->Checked = true;
    else {
        if(MainForm::TheInstance->cbNoMapFadeEffect->ForeColor != Color::Green) MainForm::TheInstance->cbNoMapFadeEffect->Checked = false;
        if(MainForm::TheInstance->cbNoMapBackground->ForeColor != Color::Green) MainForm::TheInstance->cbNoMapBackground->Checked = false;
        if(MainForm::TheInstance->cbNoMapTiles->ForeColor != Color::Green) MainForm::TheInstance->cbNoMapTiles->Checked = false;
        if(MainForm::TheInstance->cbNoMapObjects->ForeColor != Color::Green) MainForm::TheInstance->cbNoMapObjects->Checked = false;

        MainForm::TheInstance->cbNoMapFadeEffect->ForeColor = Color::White;
        MainForm::TheInstance->cbNoMapBackground->ForeColor = Color::White;
        MainForm::TheInstance->cbNoMapTiles->ForeColor = Color::White;
        MainForm::TheInstance->cbNoMapObjects->ForeColor = Color::White;


Next, the current channel is saved in a variable to go back to when the map rush is finished. 

For this map rush, spawn point control is used instead of teleport so because it is quicker. Teleport/Spawn Point Control is necessary because certain v83 private servers checks to see if player is too far away from the portal (lookin at you HeavenMS -_-). So the spawn point codecave is enabled, and the old spawn control list is saved (so that it could be restored at the end). 


toggleFastMapRushHacks(true); //Enables No Map Background, Fade, Tiles, & Objects for quicker map rush
int oldChannel = ReadPointer(ServerBase, OFS_Channel);

std::vector<SpawnControlData*> *oldSpawnControl = CodeCaves::spawnControl; //Save old spawn control list
CodeCaves::spawnControl = new std::vector<SpawnControlData*>(); //Create a new spawn control list for map rushing
Jump(spawnPointAddr, CodeCaves::SpawnPointHook, 0); //Enable spawn control 


Next, all the maps in MapPath (generated by generatePath()) are looped through.

If it is the first map in path, then AutoCC is called to switch to a new channel and enable the hacks (toggled true by toggleFastMapRushHacks()). Also the current map's corresponding portal coordinates are saved in the spawnControl list so that when CC occurs, you spawn at the portal to the next map. 

For every map, the next map's portal coordinates are saved in the spawnControl list as well, so that when SendPacket is called, Spawn Control spawns the character at the portal of the next map that the character has to go through. 


//If first map, add spawn point to spawnControl & CC to new channel to enable hacks
if (i == mapPath->begin()) {
    CodeCaves::spawnControl->push_back(new SpawnControlData((*i)->mapID, (*i)->portal->xPos, (*i)->portal->yPos - 10));
    if (oldChannel == 1) AutoCC(2); 
    else AutoCC(1);

//Add next map's spawn point to spawnControl
if((i+1) != mapPath->end()) CodeCaves::spawnControl->push_back(new SpawnControlData((*(i+1))->mapID, (*(i+1))->portal->xPos, (*(i+1))->portal->yPos - 10));


For every map, the corresponding portal is checked in memory (by looping through the CPortalList data struct) to see if the stored values matches the ones the MapleStory client loaded from the wz files. I only added this in, because private servers often have different wz files. 


PortalData^ foundPortal = findPortal(mapData->portal->toMapID); //Find portal in mem in case wz files are different in private server
if (foundPortal != nullptr) mapData->portal = foundPortal;

//Returns correct portal data (reading client's mem) in the case the stored values are incorrect
PortalData^ findPortal(int toMapID) {
	short portalZRef = 0x4; //First portal
	int portalIndex = ReadMultiPointerSigned(PortalListBase, 3, 0x4, portalZRef, 0x0);
	if (portalIndex != 0) return nullptr; //Check if First Portal Exists
	bool nextPortalExists = true;

	while(nextPortalExists) {
		int currMap = ReadMultiPointerSigned(PortalListBase, 3, 0x4, portalZRef, 0x1C);
		if(currMap == toMapID) {
			PortalData^ newPortalData = gcnew PortalData();
			char* portalName = ReadMultiPointerString(PortalListBase, 3, 0x4, portalZRef, 0x4);
			newPortalData->portalName = gcnew System::String(portalName);
			newPortalData->portalType = ReadMultiPointerSigned(PortalListBase, 3, 0x4, portalZRef, 0x8);
			newPortalData->xPos = ReadMultiPointerSigned(PortalListBase, 3, 0x4, portalZRef, 0xC);
			newPortalData->yPos = ReadMultiPointerSigned(PortalListBase, 3, 0x4, portalZRef, 0x10);
			return newPortalData;
		//Check next portal
		portalZRef += 0x8;
		int prevIndex = portalIndex;
		portalIndex = ReadMultiPointerSigned(PortalListBase, 3, 0x4, portalZRef, 0x0);
		if (portalIndex != (prevIndex + 1)) nextPortalExists = false;

	return nullptr;



Part 5: Constructing & Sending the Map Rush Packet


Now this next part is simple. If the portal type is 2 (normal portals) or 7 (visible scripted portals), the packet is constructed and then sent to the server.

While the current map is not equivalent to the map that should've been rushed to, the function sleeps for 25 ms and re-sends the packet ever 75 ms, and re-teleports to the portal every 500 ms until a max of 6.25 seconds have passed (after which map rush breaks out and returns as a failure). This might be modified in the future, but this is the fastest it works on my computer without crashing. 

//Construct Packet
String^ packet = "";
if(mapData->portal->portalType == 2) {
    writeBytes(packet, gcnew array<BYTE>{0x26, 0x00}); //Change Map OpCode
    writeByte(packet, 0); // 0 = Change Map through Regular Portals, 1 = Change Map From Dying
    writeInt(packet, -1); // Target Map ID, only not -1 when character is dead, a GM, or for certain maps like Aran Introduction, Intro Map, Adventurer   Intro, etc.
    writeString(packet, mapData->portal->portalName); // Portal Name
    writeShort(packet, (short)mapData->portal->xPos); //Portal x Position
    writeShort(packet, (short)mapData->portal->yPos); //Portal y Position
    writeByte(packet, 0); //Unknown
    writeShort(packet, 0); //Wheel of Destiny (item that revtestives char in same map)
else if(mapData->portal->portalType == 7) {
    writeBytes(packet, gcnew array<BYTE>{0x64, 0x00}); //Change Map Special OpCode
    writeByte(packet, 0); // 0 = Change Map through Regular Portals, 1 = Change Map From Dying
    writeString(packet, mapData->portal->portalName); // Portal Name
    writeShort(packet, (short)mapData->portal->xPos); //Portal x Position
    writeShort(packet, (short)mapData->portal->yPos); //Portal y Position
else {
    break; //Only deal with visible portals for now

//Spawn in next map

//Check to see if next map is loaded, try max 50 attempts
for(int i = 0; i < 50; i++) {
    if (ReadPointer(UIMiniMapBase, OFS_MapID) != mapData->mapID) break;
    if (i % 3 == 0) SendPacket(packet);
    if (i == 20) Teleport(mapData->portal->xPos, mapData->portal->yPos - 20);

After the loop through the maps are done, the hacks/channel/spawn control is restored to its original state and the function exits

Part 6: Map Rushing Between Islands


To find the island of a map, the mapID is divided by 10000000 to get the first 2 digits (which seems to identify each island). 


// 0=Maple, 10=Victoria, 11=Florina Beach, 13=Ereve, 14=Rien, 20=Orbis, 21=El Nath, 22=Ludus Lake, 23=Aquarium, 24=Minar Forest, 25=Mu Lung Garden, 26=Nihal Desert, 27=Temple of Time, 30=Elin, 54=Singapore, 55=Malaysia, 60=Masteria, 68=Amoria, 80=Zipangu TODO: Find out if more exists

//Classifies each island by the first 2 digits of the Map Ids within the island
int getIsland(int mapID) {
	if (mapID < 100000000) return 0; //Maple
	return mapID / 10000000; //Returns first 2 digits of mapID as the island


So in mapRush(), the islands of the starting and destination maps are checked to see if they are different. A function existsInterIslandPath() checks if a path exists between the two islands. If it doesn't exist, map rush returns as a failure.

Next rushNextIsland() is called to actually rush to the next island in route to final island. The return value of rushNextIsland() is the new starting map.

This process is repeated 5 times (in a loop), and if the proper island is not reached after 5 times, then the map rush is a failure. After the loop sucessfully finishes, the character is in the same island as the destination map's island. So the map rush continues as normal


if (getIsland(startMapID) != getIsland(destMapID)) {
    for(int i = 0; i < 5; i++) { //Max islands to travel to has to be at max 5 islands
        if (!existsInterIslandPath(startMapID, destMapID)) break; //Check if path between islands exists
        startMapID = rushNextIsland(startMapID, destMapID); //Rushes to next island to dest map's island, return val is new starting map id
        if (startMapID == -1) return; //Error ocurred, rushNextIsland() handles error message
        if (startMapID == 0) break; //End of rushNextIsland() reached, shouldn't happen because of existsInterIslandPath()

        //If first map on new island is the destination, finish
        if (startMapID == destMapID) {
         	MainForm::TheInstance->lbMapRusherStatus->Text = "Status: Map Rushing Complete";
          	GlobalRefs::isMapRushing = false;

    //Couldn't rush to same island as Destination Map
    if (getIsland(startMapID) != getIsland(destMapID)) {
      	MainForm::TheInstance->lbMapRusherStatus->Text = "Status: Cannot find a path to Destination Map ID";
      	GlobalRefs::isMapRushing = false;


existsInterIslandPath() is basically hardcoded links between the islands (I'll have to come up with a better way later). Returns true if there exists a link between the island


//Checks if path exists to Destination Map's island
bool existsInterIslandPath(int startMapID, int destMapID) {
	int startIsland = getIsland(startMapID), destIsland = getIsland(destMapID);
	if (destIsland == 0) return false; //Cannot travel to Maple island from anywhere

	switch(startIsland) { 
        case 0:
            if (destIsland == 10 || destIsland == 11) return true;
        case 10:
            if (destIsland == 11) return true;
        case 11:
            if (destIsland == 10) return true;

	return false;

rushNextIsland() uses the same hardcoded links between islands. It first map rushes to the end of the current island by calling map rush(), and then sends an npc packet to open up the NPC Dialog to go to the next island. Then (for now) PostMessage is used to send the right arrow key (to click yes) and enter key (to click next



//Constructs and sends the NPC packets
void SendNPCPacket(int npcID, int xPos, int yPos) {
	String^ packet = "";
	writeBytes(packet, gcnew array<BYTE>{0x3A, 0x00}); //NPC Talk Packet Header
	writeInt(packet, npcID);
	writeShort(packet, xPos); //Char x pos when npc is clicked, not really important
	writeShort(packet, yPos); //Char y pos when npc is clicked, not really important

//Rushes to next island to route to Destination Map's island
int rushNextIsland(int startMapID, int destMapID) {
	int startIsland = getIsland(startMapID), destIsland = getIsland(destMapID);
	switch (startIsland) {
		case 0: //Rush to Victoria
			if (PointerFuncs::getCharMesos() < 150) {
				MainForm::TheInstance->lbMapRusherStatus->Text = "Status: You need 150 mesos to rush out of Maple Island";
				GlobalRefs::isMapRushing = false;
				return -1;
			if (startMapID != 2000000) mapRush(2000000); //Rush to the map that links to Victoria
			SendNPCPacket(1000000003, 3366, -112); Sleep(500); //NPC Shanks 
			SendKey(VK_RIGHT); Sleep(500); //Send Right Arrow to select yes
			SendKey(VK_RETURN); Sleep(500); //Send Enter to press yes
			SendKey(VK_RETURN); Sleep(500); //Send Enter to press next
			SendKey(VK_RETURN); Sleep(500); //Send Enter to press next
			return 104000000;
		case 10:
			if (destIsland == 11) {
				if (PointerFuncs::getCharMesos() < 1500) {
					MainForm::TheInstance->lbMapRusherStatus->Text = "Status: You need 1500 mesos to rush to Florina Beach";
					GlobalRefs::isMapRushing = false;
					return -1;
				if (startMapID != 104000000) mapRush(104000000); //Rush to the map that links to Florina Beach
				SendNPCPacket(1000000008, 1746, 647); Sleep(500); //NPC Pason
				SendKey(VK_RETURN); Sleep(500); //Send Enter to press yes
				return 110000000;
		case 11:
			if (startMapID != 110000000) mapRush(110000000); //Rush to the map that links to Victoria
			SendNPCPacket(1000000004, -273, 151); Sleep(500);
			SendKey(VK_RETURN); Sleep(500); //Send Enter to press next
			SendKey(VK_RIGHT); Sleep(500); //Send Right Arrow to select yes
			SendKey(VK_RETURN); Sleep(500); //Send Enter to press yes
			return 104000000;

	return 0;





Part 7: Map Rusher Unhandled Exceptions


There are 3 exceptions that I have identified that wouldn't be able to be map rushed to with the code I currently have: 

1. Timed Maps (where you stay in for 30 mins and then you get kicked out):

  • 100020000: links to Mini Dungeon: Henesys Pig Farm, map id: 100020100, with duplicate maps 100020101-100020199 if someone is in 100020100
  • 105040304: links to Mini Dungeon: Golem's Castle Ruins, map id: 105040320, with duplicate maps 105040321-105040369 if someone is in 105040320
  • 105050100: links to Mini Dungeon: Cave Where Mushrooms Grow, map id: 105050101, with duplicate maps 105050102-105050199 if someone is in 105050101
  • 221023400: links to Mini Dungeon: Drummer Bunny's Lair, map id: 221023401, with duplicate maps
  • 240020500: links to Mini Dungeon: The Round Table of Kentaurus, map id: 240020512, with duplicate maps
  • 240040511: links to Mini Dungeon: The Dragon Nest Left Behind, map id: 240040511, with duplicate maps
  • 240040520: links to Mini Dungeon: Newt Secured Zone, map id: 240040900, with duplicate maps
  • 260020600: links to Mini Dungeon: Hill of Sandstorms, map id: 260020630, with duplicate maps
  • 261020300: links to Mini Dungeon: Critical Error, map id: 261020301, with duplicate maps

2. Maple Doors in towns, these are special portals with portal type 1, that are not handled yet (think Kerning City Weapons/Pharmacy)

3. Timed boats (like waiting for the boat to orbis from ellinia), going to work on this soon tho

Part 8: TODO List: 


1. Figure out if there is a way to close the Yes/No NPC Dialog so that I can just use packets instead of PostMessage.

Writing a jump inside of CScriptMan::OnScriptMessage allows me to send multiple dialogs (preventing dc from another OnScriptMessage Packet). This in turn allows me to send the packets for clicking yes, and next. This works, but it leaves the yes/no dialog open (since the packets create new dialogs). The other dialogs can be closed using CUtilDlgEx__ForcedRet();


typedef int(__stdcall *pfnCUtilDlgEx__ForcedRet)(); //Close Dialogs (not the yes/no dialogue :sadface:)
auto CUtilDlgEx__ForcedRet = (pfnCUtilDlgEx__ForcedRet)0x009A3C2C;

WriteMemory(0x0074661D, 1, 0xEB); //Enables multiple open dialogs (in CScriptMan::OnScriptMessage)
SendPacket("3C 00 01 01"); Sleep(200); //Click Yes
SendPacket("3C 00 00 01 "); Sleep(200); //Click Next
CUtilDlgEx__ForcedRet(); //Closes the new "yes" and "next" dialogs that pop up over the original yes/no dialog


2. Figure out a way to reduce time between map change packets, @NewSprux2.0? & @Darter mentioned blocking the CStage::OnSetField packet so that you change maps server side and re-enable when you reach the final map. I'll test that out later today or this week


3. Figure out a way to get out of timed ships/boats quickly (should be pretty easy to figure out)

4. Manually add in every single scripted portal that is needed to travel between maps (since you can't scrape that info from wz or client's memory). Scripted portal's tm property is always 999999999 :(

5. Code in the rest of the islands

6. See if there is a better way than manually linking islands (I guess I could just add a fake node or something)


Anyways just typed this all out so I could understand my own code better and because @Razz asked. This is not meant to be anything good, just a write up of the map rusher that is in my trainer. Still a work in progress (Github: github.com/jnpl95/Timelapse). Lemme know if you guys have any tips or suggestions

Edited by JP.
  • Like 5
  • Thanks 2

Share this post

Link to post
1 hour ago, OuterHaven said:

You could patch out CUtilDlg::YesNo to always return 6 / yes

E8 ? ? ? ? 83 C4 1C 83 F8 06


Hmm ok, I'll look into that. I looked into that function before to see if I could do something like that, I think I got stuck because it calls CDialog::DoModel() which calls a vm'ed func CWvsApp::Run where it runs until yes/no/end chat is clicked (or so I thought). Anyways thanks, I'll check it out again. 

Share this post

Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this