I am the Village Idiot and I’m a bit a newbie so please bear with me. I wanted to open the new Realm areas in 1.11 so I had to create a custom red portal to a new area with a cube recipe. I really could not find anything really documented on how to do it. I searched the Phrozen Keep forums and saw Kingpin mention it a couple of times, but there really was not any tutorial or really any posts describing how to do it. So, being a relatively resourceful person, I decided to try it myself. When I started I knew nothing about the internals of D2 and the Phrozen Keep forums and knowledge base have been indispensable in answering my questions even though I’m pretty much a lurker. Kingpin’s Red Portal was also a great help. I chose D2 1.10 because there was the most information on that version. Anyway, let’s get started shall we?
Adding your text strings to the parsing code
You should start with the extended levels plug-in modified D2Common and D2Client files. It's also a really good idea to use a rebased D2Common file. The first thing you have to do is get the game to recognize your specific output code which will spawn your portal. Of course you add the specific recipe to cubemain.txt and in the output code column; you insert a custom string such as ‘Pandemonium Portal’. In my case, I used the v1.11 cubemain which adds the following:
description enabled version op numinputs input 1 input 2 input 3 Output
Pandemonium key 1 100 28 3 pk1 pk2 pk3 Pandemonium Portal
Pandemonium Finale key 1 100 28 3 dhn bey mbr Pandemonium Finale Portal
Once that’s done, you need to edit the parser for cubemain to recognize your text string. This function is located in D2Common.dll. The parser basically checks all of the output codes looking for item codes or ‘special’ cube recipe code and then tokenizes it in the cube recipe structure stored in memory.
From what I can tell, each cube recipe has a structure in memory which is read in by parsing the cubemain text file. It looks something like this.
Code: Select all
CubeRecipeStruct
+0x14 1st Cube input code
+0x00 Item Properties (this is a bit field)
+0x02 Item code from Master list (0xFFFF = any)
+0x06 Item quality
+0x1C 2nd Cube input code
+0x24 3rd Cube input code
+0x2C 4th Cube input code
+0x34 5th Cube input code
+0x3C 6th Cube input code
+0x44 7th Cube input code
+0x4C 1st Cube Output Code
+0x00 Properties (this is a bit field)
+0x02 Item Code from Master List
+0x06 Item Quality
+0x08 Type
+0x12 Prefix Code
+0x18 Suffix Code
+0xA0 2nd Cube Output Code
+0xF4 3rd Cube Output Code
11 10 9 8 7 6 5 4 3 2 1 0
NRU ELI EXC BAS UPG ITEM NOE ETH SOCK NOS CODE
For output codes
10 9 8 7 6 5 4 3 2 1 0
RCH RED ELI EXC REG REM UNS ETH SOCK MOD
For the output types, we have the following values:
1 Cow Portal
2 Pandemonium Portal
3 Pandemonium Finale Portal
4 Soft Portal
FF Use Type
FE Use Item
FC Item Code Found
For item quality, use the same values that you would find in the items saved in a character file.
I added the actual text strings to the data section of the dll at offset 0x0A6CF0 which loads at 0x6F6A6CF0. So, it looks like this in Ollydebug once you enter the strings:
Code: Select all
000A6CF0 50 61 6E 64 65 6D 6F 6E 69 75 6D 20 50 6F 72 74 Pandemonium Port
000A6D00 61 6C 00 50 61 6E 64 65 6D 6F 6E 69 75 6D 20 46 al.Pandemonium F
000A6D10 69 6E 61 6C 65 20 50 6F 72 74 61 6C 00 00 00 00 inale Portal....
Code: Select all
6F612962 68 8486696F PUSH D2Common.6F698684 ; ASCII "Cow Portal"
6F612967 55 PUSH EBP
6F612968 E8 D31E0700 CALL D2Common.6F684840
6F61296D 83C4 08 ADD ESP,8
6F612970 85C0 TEST EAX,EAX
6F612972 . 75 42 JNZ SHORT D2Common.6F6129B6
Code: Select all
6F612962 68 8486696F PUSH D2Common.6F698684 ; ASCII "Cow Portal"
6F612967 55 PUSH EBP
6F612968 E8 D31E0700 CALL D2Common.6F684840
6F61296D 83C4 08 ADD ESP,8
6F612970 85C0 TEST EAX,EAX
[color=red]6F612972 ^75 92 JNZ SHORT D2Common.6F612906[/color]
Code: Select all
6F612906 90 NOP
Code: Select all
[color=red]6F612906 E9 71AC0700 JMP D2Common.6F68D57C[/color]
Edit: The code below depends on D2Common relocating to 0x03DF0000 otherwise you will get an access violation when you load a character. If you are customizing your own DLL you will need to change the addresses accordingly. I am looking into modifying the relocation table so these values get fixed up when the D2Common relocates.
Edit: Changed the addresses to the base address of the DLL and not the relocation address for my system. Updated the code below to NOT rely on the fixup table and to use the known locations of the strings left on the stack from the previous call.
Edit: Changed location so edits will fit with Extended Levels Plug-In
Edit: Changed locations to match load locations for rebased D2Common.
Code: Select all
6F68D57C 814424 FC 6CE60000 ADD DWORD PTR SS:[ESP-4],0E66C ; Get Cow Portal String location from stack
6F68D584 83EC 04 SUB ESP,4 ; And add offset to our strings 0xE66C and emulate push.
6F68D587 55 PUSH EBP ; String to compare to
6F68D588 E8 B372FFFF CALL D2Common.6F684840 ; Case insensitive string compare
6F68D58D 83C4 08 ADD ESP,8 ; Clean up Stack
6F68D590 85C0 TEST EAX,EAX ; See if we have a match
6F68D592 75 0E JNZ SHORT D2Common.6F68D5A2 ; If not goto next test
6F68D594 C646 08 02 MOV BYTE PTR DS:[ESI+8],2 ; Set Output to type 2 (Pandemonium Portal)
6F68D598 A3 F0FFD46F MOV DWORD PTR DS:[6FD4FFF0],EAX ; Clear Pandemonium Quest Status
6F68D59D ^E9 D653F8FF JMP D2Common.6F612978 ; Jump back to original code for more processing
6F68D5A2 834424 FC 13 ADD DWORD PTR SS:[ESP-4],13 ; Add offset for next custom string (pand finale)
6F68D5A7 83EC 04 SUB ESP,4 ; Emulate push of string location
6F68D5AA 55 PUSH EBP ; String to compare to
6F68D5AB E8 9072FFFF CALL D2Common.6F684840 ; Case insensitive string compare
6F68D5B0 83C4 08 ADD ESP,8 ; Clean up stack
6F68D5B3 85C0 TEST EAX,EAX ; See if we have a match
6F68D5B5 ^0F85 0054F8FF JNZ D2Common.6FD529B6 ; If not then goto next test in original code
6F68D5BB C646 08 03 MOV BYTE PTR DS:[ESI+8],3 ; Set output to type 3 (Pand Finale Portal)
6F68D5BF A3 F0FFD46F MOV DWORD PTR DS:[6FD4FFF0],EAX ; Clear Pandemonium Quest Status
6F68D5C4 ^E9 AF53F8FF JMP D2Common.6F612978 ; Jump back to original code for more processing
Now D2Common will recognize your custom portal codes in the text file and set the structures properly in memory, but you still will not be able use them in game until you modify the cube code in D2Game to recognize the new output types. Also, you need somewhere to go, so I used the levels.txt file from v1.11 since it has the additional entries for the pandemonium areas.
Basically D2Game uses a lookup table to a jump vector with the output type as an index into the table. It also does a bit of boundary checking to make sure the index is within range. Since in the original code, the only tokenized value is 1, the only index that works is 1. We need to change this. First of all, we need to add a jump table to the rdata section of D2Game. Our custom code will go in 2 locations 0x6FC90005 and 0x6FC9000A. The new jump table looks like this at 0x6FD2BC20 or offset 0x0FBC20.
Code: Select all
6FD2BC20 00 00 00 00 00 00 C9 6F 05 00 C9 6F 0A 00 C9 6F ......Éo .Éo..Éo
Code: Select all
6FC90000 E9 ABC50000 JMP D2Game.6FC9C5B0 ; Original Cow Portal Code
6FC90005 90 NOP
6FC90006 90 NOP
6FC90007 90 NOP
6FC90008 90 NOP
6FC90009 90 NOP
6FC9000A 90 NOP
Code: Select all
6FC90000 E9 ABC50000 JMP D2Game.6FC9C5B0 ; Original Cow Portal Code
[color=red]6FC90005 E9 966A0900 JMP D2Game.6FD26B9A
6FC9000A E9 916A0900 JMP D2Game.6FD26AA0
[/color]
Code: Select all
6FC90224 84C0 TEST AL,AL ; AL has the type code
6FC90226 74 22 JE SHORT D2Game.6FC9024A
6FC90228 3C 02 CMP AL,2 ; 0 < AL < 2 else leave.
6FC9022A 73 1E JNB SHORT D2Game.6FC9024A
6FC9022C 25 FF000000 AND EAX,0FF
6FC90231 8B0485 3C93D26F MOV EAX,DWORD PTR DS:[EAX*4+6FD2933C] ; Lookup in jump table
6FC90238 85C0 TEST EAX,EAX ; Make sure an address is returned
6FC9023A 74 0E JE SHORT D2Game.6FC9024A
6FC9023C 8B5424 18 MOV EDX,DWORD PTR SS:[ESP+18]
6FC90240 8B4C24 10 MOV ECX,DWORD PTR SS:[ESP+10]
6FC90244 FFD0 CALL EAX ; Jump to portal code.
Code: Select all
6FC90224 84C0 TEST AL,AL
6FC90226 74 22 JE SHORT D2Game.6FC9024A
[color=red]6FC90228 3C 04 CMP AL,4[/color]
6FC9022A 73 1E JNB SHORT D2Game.6FC9024A
6FC9022C 25 FF000000 AND EAX,0FF
[color=red]6FC90231 8B0485 20BCD26F MOV EAX,DWORD PTR DS:[EAX*4+6FD2BC20]
[/color]
Edit: Updated this routine for Pandemonium Finale Portal. Removed Cow Portal Specific Items.
Code: Select all
6FD26AA0 83EC 0C SUB ESP,0C ; Pandemonium Finale Code
6FD26AA3 53 PUSH EBX
6FD26AA4 56 PUSH ESI
6FD26AA5 57 PUSH EDI
6FD26AA6 8BF2 MOV ESI,EDX
6FD26AA8 8BF9 MOV EDI,ECX
6FD26AAA 56 PUSH ESI
6FD26AAB E8 464BFFFF CALL <JMP.&D2Common.#10424>
6FD26AB0 33C9 XOR ECX,ECX
6FD26AB2 8B4F 70 MOV ECX,DWORD PTR DS:[EDI+70] ; Game Version?
6FD26AB5 85C9 TEST ECX,ECX ; See if 0 which is Classic D2
6FD26AB7 0F84 9A000000 JE D2Game.6FD26B57 ; If Classic D2 then exit
6FD26ABD 33C9 XOR ECX,ECX
6FD26ABF 8A4F 6D MOV CL,BYTE PTR DS:[EDI+6D] ; Difficulty Level
6FD26AC2 80F9 02 CMP CL,2 ; See if Hell
6FD26AC5 0F85 8C000000 JNZ D2Game.6FD26B57 ; If not Hell then exit
6FD26ACB 6A 00 PUSH 0 ; Reward Granted / Quest Complete
6FD26ACD 6A 28 PUSH 28 ; Eve of Destruction Quest
6FD26ACF 8B4488 10 MOV EAX,DWORD PTR DS:[EAX+ECX*4+10]
6FD26AD3 50 PUSH EAX ; Quest History ptr (Quest and difficulty based)
6FD26AD4 E8 3B4EFFFF CALL <JMP.&D2Common.#11107> ; Check Quest State
6FD26AD9 85C0 TEST EAX,EAX
6FD26ADB 74 7A JE SHORT D2Game.6FD26B57 ; If not complete then exit
6FD26ADD 56 PUSH ESI ; ptUnit
6FD26ADE E8 C74BFFFF CALL <JMP.&D2Common.#10342> ; Get hRoom from ptUnit
6FD26AE3 8BD8 MOV EBX,EAX
6FD26AE5 85DB TEST EBX,EBX ; Check if 0. (NULL pointer?)
6FD26AE7 74 6E JE SHORT D2Game.6FD26B57 ; If 0 then exit
6FD26AE9 53 PUSH EBX ; hRoom
6FD26AEA E8 6D4BFFFF CALL <JMP.&D2Common.#10057> ; Get level from hRoom
6FD26AEF 83F8 6D CMP EAX,6D
6FD26AF2 75 63 JNZ SHORT D2Game.6FD26B57 ; If not Act V - Harrogath then exit
6FD26AF4 8D5424 10 LEA EDX,DWORD PTR SS:[ESP+10]
6FD26AF8 C74424 0C 00000000 MOV DWORD PTR SS:[ESP+C],0
6FD26B00 52 PUSH EDX
6FD26B01 56 PUSH ESI
6FD26B02 E8 034FFFFF CALL <JMP.&D2Common.#10332>
6FD26B07 8D4424 0C LEA EAX,DWORD PTR SS:[ESP+C]
6FD26B0B 8D5424 10 LEA EDX,DWORD PTR SS:[ESP+10]
6FD26B0F 6A 04 PUSH 4
6FD26B11 50 PUSH EAX
6FD26B12 68 00040000 PUSH 400
6FD26B17 6A 03 PUSH 3
6FD26B19 8BCB MOV ECX,EBX
6FD26B1B E8 20FEF6FF CALL D2Game.6FC96940
6FD26B20 8B4424 0C MOV EAX,DWORD PTR SS:[ESP+C]
6FD26B24 85C0 TEST EAX,EAX
6FD26B26 74 2F JE SHORT D2Game.6FD26B57
6FD26B28 8B4C24 14 MOV ECX,DWORD PTR SS:[ESP+14]
6FD26B2C 8B5424 10 MOV EDX,DWORD PTR SS:[ESP+10]
6FD26B30 6A 00 PUSH 0
6FD26B32 6A 3C PUSH 3C
6FD26B34 6A 00 PUSH 0
6FD26B36 68 88000000 PUSH 88
6FD26B3B 51 PUSH ECX
6FD26B3C 52 PUSH EDX
6FD26B3D 50 PUSH EAX
6FD26B3E 8BD6 MOV EDX,ESI
6FD26B40 8BCF MOV ECX,EDI
6FD26B42 E8 A9D2FEFF CALL D2Game.6FD13DF0 ; Create Portal Object
6FD26B47 85C0 TEST EAX,EAX ; Check if portal failed
6FD26B49 74 0C JE SHORT D2Game.6FD26B57 ; If so, then exit
6FD26B4B B8 01000000 MOV EAX,1 ; Return 1
6FD26B50 5F POP EDI
6FD26B51 5E POP ESI
6FD26B52 5B POP EBX
6FD26B53 83C4 0C ADD ESP,0C
6FD26B56 C3 RETN
6FD26B57 56 PUSH ESI ; Exit without success code.
6FD26B58 BA 14000000 MOV EDX,14
6FD26B5D 8BCE MOV ECX,ESI
6FD26B5F E8 1C59F9FF CALL D2Game.6FCBC480
6FD26B64 33C0 XOR EAX,EAX ; Return 0
6FD26B66 5F POP EDI
6FD26B67 5E POP ESI
6FD26B68 5B POP EBX
6FD26B69 83C4 0C ADD ESP,0C
6FD26B6C C3 RETN
Edit: This code has been added. Before both recipes pointed to the same portal generation code.
Code: Select all
6FD26B9A 83EC 0C SUB ESP,0C ; Pandemonium Portal Code
6FD26B9D 53 PUSH EBX
6FD26B9E 56 PUSH ESI
6FD26B9F 57 PUSH EDI
6FD26BA0 8BF2 MOV ESI,EDX
6FD26BA2 8BF9 MOV EDI,ECX
6FD26BA4 56 PUSH ESI ; ptUnit
6FD26BA5 E8 4C4AFFFF CALL <JMP.&D2Common.#10424> ; Get ptPlayerInfo from ptUnit
6FD26BAA 33C9 XOR ECX,ECX
6FD26BAC 8B4F 70 MOV ECX,DWORD PTR DS:[EDI+70] ; Game Version?
6FD26BAF 85C9 TEST ECX,ECX ; See if 0 which is Classic D2
6FD26BB1 0F84 37010000 JE D2Game.6FD26CEE ; Exit if Classic D2
6FD26BB7 33C9 XOR ECX,ECX
6FD26BB9 8A4F 6D MOV CL,BYTE PTR DS:[EDI+6D] ; Difficulty Level
6FD26BBC 80F9 02 CMP CL,2
6FD26BBF 0F85 29010000 JNZ D2Game.6FD26CEE ; If not Hell then Exit
6FD26BC5 6A 00 PUSH 0 ; Reward Granted / Quest Complete
6FD26BC7 6A 28 PUSH 28 ; Eve of Destruction Quest
6FD26BC9 8B4488 10 MOV EAX,DWORD PTR DS:[EAX+ECX*4+10]
6FD26BCD 50 PUSH EAX ; Quest History ptr (Quest and Difficulty based)
6FD26BCE E8 414DFFFF CALL <JMP.&D2Common.#11107> ; Check Quest State
6FD26BD3 85C0 TEST EAX,EAX
6FD26BD5 0F84 13010000 JE D2Game.6FD26CEE ; If not Complete then Exit
6FD26BDB 56 PUSH ESI ; ptUnit
6FD26BDC E8 C94AFFFF CALL <JMP.&D2Common.#10342> ; Get hRoom from ptUnit
6FD26BE1 8BD8 MOV EBX,EAX
6FD26BE3 85DB TEST EBX,EBX ; Check if 0 (NULL pointer?)
6FD26BE5 0F84 03010000 JE D2Game.6FD26CEE ; If so then exit
6FD26BEB 53 PUSH EBX ; hRoom
6FD26BEC E8 6B4AFFFF CALL <JMP.&D2Common.#10057> ; Get level from hRoom
6FD26BF1 83F8 6D CMP EAX,6D ; See if Act V - Harrogath
6FD26BF4 0F85 F4000000 JNZ D2Game.6FD26CEE ; If not then exit
6FD26BFA 8B5424 18 MOV EDX,DWORD PTR SS:[ESP+18] ; Pull caller address from stack
6FD26BFE 8B8A AAFD0B00 MOV ECX,DWORD PTR DS:[EDX+BFDAA] ; Add offset to point to our status location
6FD26C04 83F9 07 CMP ECX,7 ; See if all 3 portals open
6FD26C07 0F84 E1000000 JE D2Game.6FD26CEE ; If so exit
6FD26C0D 33C0 XOR EAX,EAX ; Clear number of unopened portals counter
6FD26C0F 0FBAE1 00 BT ECX,0 ; Test first portal
6FD26C13 72 06 JB SHORT D2Game.6FD26C1B ; If set the goto next test
6FD26C15 40 INC EAX ; Increment number of unopened portals
6FD26C16 68 85010000 PUSH 185 ; Destination + status bitmask
6FD26C1B 0FBAE1 01 BT ECX,1 ; Test Second Portal
6FD26C1F 72 06 JB SHORT D2Game.6FD26C27 ; If set the goto next test
6FD26C21 40 INC EAX ; Increment number of unopened portals
6FD26C22 68 86020000 PUSH 286 ; Destination + status bitmask
6FD26C27 0FBAE1 02 BT ECX,2 ; Test Third Portal
6FD26C2B 72 06 JB SHORT D2Game.6FD26C33 ; If set the goto next test
6FD26C2D 40 INC EAX ; Increment number of unopened portals
6FD26C2E 68 87040000 PUSH 487 ; Destination + status bitmask
6FD26C33 85C0 TEST EAX,EAX ; Make sure there is at least 1 portal to open
6FD26C35 0F84 B3000000 JE D2Game.6FD26CEE ; If not then exit
6FD26C3B 83F8 01 CMP EAX,1 ; See if there is only 1 portal to open
6FD26C3E 75 12 JNZ SHORT D2Game.6FD26C52 ; If not goto random number code
6FD26C40 58 POP EAX ; Get parameter from stack
6FD26C41 33C9 XOR ECX,ECX
6FD26C43 8ACC MOV CL,AH ; Set status mask
6FD26C45 25 FF000000 AND EAX,0FF ; Clear status mask from destination
6FD26C4A 098A AAFD0B00 OR DWORD PTR DS:[EDX+BFDAA],ECX ; Update status
6FD26C50 EB 3B JMP SHORT D2Game.6FD26C8D ; Proceed to opening portal
6FD26C52 55 PUSH EBP ; Start of random number code
6FD26C53 53 PUSH EBX ; From Nefarius
6FD26C54 52 PUSH EDX
6FD26C55 50 PUSH EAX
6FD26C56 8BC8 MOV ECX,EAX ; Number of portals on stack
6FD26C58 8B46 24 MOV EAX,DWORD PTR DS:[ESI+24] ; get ptUnit+24 (highseed) -> EAX
6FD26C5B BA C590C66A MOV EDX,6AC690C5 ; set static seed for rand
6FD26C60 33ED XOR EBP,EBP ; flush EBP
6FD26C62 F7E2 MUL EDX ; 64bit multiplication of EDX (EDX&EAX)
6FD26C64 8B5E 20 MOV EBX,DWORD PTR DS:[ESI+20] ; get ptUnit+20 (lowseed) -> EBX
6FD26C67 03C3 ADD EAX,EBX ; EAX+EBX
6FD26C69 11EA ADC EDX,EBP ; EDX+0+overflow (i.e. carryover)
6FD26C6B 33D2 XOR EDX,EDX ; flush EDX
6FD26C6D F7F1 DIV ECX ; 64bit division of ECX (EDX&EAX, but EDX is 0, so it divides by 0.EAX!)
6FD26C6F 83C2 04 ADD EDX,4 ; EDX contains the main result - Add offset for push statements above
6FD26C72 8B0494 MOV EAX,DWORD PTR SS:[ESP+EDX*4] ; Pull Parameter from stack
6FD26C75 59 POP ECX ; Restore saved registers
6FD26C76 5A POP EDX
6FD26C77 5B POP EBX
6FD26C78 5D POP EBP
6FD26C79 C1E1 02 SHL ECX,2 ; 4 * number of portals on stack
6FD26C7C 03E1 ADD ESP,ECX ; Clean up stack
6FD26C7E 33C9 XOR ECX,ECX
6FD26C80 8ACC MOV CL,AH ; Get status mask
6FD26C82 098A AAFD0B00 OR DWORD PTR DS:[EDX+BFDAA],ECX ; Update Status
6FD26C88 25 FF000000 AND EAX,0FF ; Clear status mask from destination
6FD26C8D 50 PUSH EAX ; Save destination
6FD26C8E 8D5424 14 LEA EDX,DWORD PTR SS:[ESP+14]
6FD26C92 C74424 10 000000>MOV DWORD PTR SS:[ESP+10],0
6FD26C9A 52 PUSH EDX ; ptGame?
6FD26C9B 56 PUSH ESI ; ptUnit
6FD26C9C E8 694DFFFF CALL <JMP.&D2Common.#10332> ; Get hPath from ptUnit?
6FD26CA1 8D4424 10 LEA EAX,DWORD PTR SS:[ESP+10]
6FD26CA5 8D5424 14 LEA EDX,DWORD PTR SS:[ESP+14] ; Arg2 - X Position from hPath
6FD26CA9 6A 04 PUSH 4 ; Arg6 - 4
6FD26CAB 50 PUSH EAX ; Arg5 - Y position?
6FD26CAC 68 00040000 PUSH 400 ; Arg4 - 400
6FD26CB1 6A 03 PUSH 3 ; Arg3 - 3
6FD26CB3 8BCB MOV ECX,EBX ; Arg1 -
6FD26CB5 E8 86FCF6FF CALL D2Game.6FC96940
6FD26CBA 8B4424 10 MOV EAX,DWORD PTR SS:[ESP+10]
6FD26CBE 85C0 TEST EAX,EAX
6FD26CC0 74 2C JE SHORT D2Game.6FD26CEE
6FD26CC2 8B4C24 18 MOV ECX,DWORD PTR SS:[ESP+18] ; Y Position from hPath?
6FD26CC6 5A POP EDX ; Get Destination
6FD26CC7 6A 00 PUSH 0 ; Arg9 - 0 = non permanent
6FD26CC9 6A 3C PUSH 3C ; Arg8 - Object ID / 3C = Red Portal
6FD26CCB 6A 00 PUSH 0 ; Arg7 - unit / source pointer
6FD26CCD 52 PUSH EDX ; Arg6 - Level ID
6FD26CCE 8B5424 20 MOV EDX,DWORD PTR SS:[ESP+20] ; X Position from hPath?
6FD26CD2 51 PUSH ECX ; Arg5 - Y position from hPath
6FD26CD3 52 PUSH EDX ; Arg4 - X position from hPath
6FD26CD4 50 PUSH EAX ; Arg3 - ptRoom
6FD26CD5 8BD6 MOV EDX,ESI ; Arg2 - ptPlayer
6FD26CD7 8BCF MOV ECX,EDI ; Arg1 - ptGame
6FD26CD9 E8 12D1FEFF CALL D2Game.6FD13DF0 ; Create Object
6FD26CDE 85C0 TEST EAX,EAX
6FD26CE0 74 0C JE SHORT D2Game.6FD26CEE ; If not successful then exit
6FD26CE2 B8 01000000 MOV EAX,1 ; Return success
6FD26CE7 5F POP EDI
6FD26CE8 5E POP ESI
6FD26CE9 5B POP EBX
6FD26CEA 83C4 0C ADD ESP,0C
6FD26CED C3 RETN
6FD26CEE 56 PUSH ESI ; Begin non-success here
6FD26CEF BA 14000000 MOV EDX,14
6FD26CF4 8BCE MOV ECX,ESI
6FD26CF6 E8 8557F9FF CALL D2Game.6FCBC480 ; "I can't" message?
6FD26CFB 33C0 XOR EAX,EAX ; Return failure
6FD26CFD 5F POP EDI
6FD26CFE 5E POP ESI
6FD26CFF 5B POP EBX
6FD26D00 83C4 0C ADD ESP,0C
6FD26D03 C3 RETN
6FD26D04 90 NOP
Code: Select all
6FD13E3B 85C0 TEST EAX,EAX
6FD13E3D 74 27 JE SHORT D2Game.6FD13E66
6FD13E3F 837C24 30 27 CMP DWORD PTR SS:[ESP+30],27 ; Moo moo farm
6FD13E44 75 07 JNZ SHORT D2Game.6FD13E4D
6FD13E46 837C24 38 3C CMP DWORD PTR SS:[ESP+38],3C ; Red Portal
6FD13E4B 74 19 JE SHORT D2Game.6FD13E66
Code: Select all
6FD13E3B 85C0 TEST EAX,EAX
6FD13E3D 74 27 JE SHORT D2Game.6FD13E66
[color=red]6FD13E3F E9 0C2C0100 JMP D2Game.6FD26B6E[/color]
6FD13E44 75 07 JNZ SHORT D2Game.6FD13E4D
6FD13E46 837C24 38 3C CMP DWORD PTR SS:[ESP+38],3C
6FD13E4B 74 19 JE SHORT D2Game.6FD13E66
Edit: Moved this code to fit with additional portal creation functions.
Code: Select all
6FD26B6E 50 PUSH EAX
6FD26B6F 8B4424 34 MOV EAX,DWORD PTR SS:[ESP+34]
6FD26B73 83F8 27 CMP EAX,27
6FD26B76 74 0E JE SHORT D2Game.6FD26B86
6FD26B78 3D 85000000 CMP EAX,85
6FD26B7D 7C 14 JL SHORT D2Game.6FD26B93
6FD26B7F 3D 88000000 CMP EAX,88
6FD26B84 77 0D JA SHORT D2Game.6FD26B93
6FD26B86 837C24 3C 3C CMP DWORD PTR SS:[ESP+3C],3C
6FD26B8B 75 06 JNZ SHORT D2Game.6FD26B93
6FD26B8D 58 POP EAX
6FD26B8E ^E9 D3D2FEFF JMP D2Game.6FD13E66
6FD26B93 58 POP EAX
6FD26B94 ^E9 B4D2FEFF JMP D2Game.6FD13E4D
Code: Select all
6FD13FD0 8BF0 MOV ESI,EAX
6FD13FD2 85F6 TEST ESI,ESI
6FD13FD4 75 0A JNZ SHORT 6FD13FE0
6FD13FD6 5F POP EDI
Code: Select all
[color=red]6FD13FD0 E9 B12A0100 JMP D2Game.6FD26A86
6FD13FD5 90 NOP
[/color]
Code: Select all
6FD26A86 50 PUSH EAX
6FD26A87 51 PUSH ECX
6FD26A88 8B4E 14 MOV ECX,DWORD PTR DS:[ESI+14]
6FD26A8B 8B4424 38 MOV EAX,DWORD PTR SS:[ESP+38]
6FD26A8F 8841 04 MOV BYTE PTR DS:[ECX+4],AL
6FD26A92 59 POP ECX
6FD26A93 58 POP EAX
6FD26A94 ^E9 47D5FEFF JMP D2Game.6FD13FE0
To emulate V1.11 functionality one still needs to create the maps for the new levels since the areas in the txt file do not work as is. However, this should demonstrate how one could make a mod with cube recipes that open portals to custom areas.
Taking it a step further
After I originally posted this, Joel made a excellent suggestion to soft code the red portal and add a cube output code of lvl= so that a mod maker can specify a destination for the red portal without resorting to code editing.
The first step is to add another output type to the cube parser. I will assign it to type 4 and store the destination level in the item quality field of the cube output structure. I will store the Act in the item type field of the structure.
Code: Select all
[color=black]6F68D57C 814424 FC 6CE60000 ADD DWORD PTR SS:[ESP-4],0E66C ; Get Cow Portal String location from stack
6F68D584 83EC 04 SUB ESP,4 ; And add offset to our strings 0xE66C and emulate push.
6F68D587 55 PUSH EBP ; String to compare to
6F68D588 E8 B372FFFF CALL D2Common.6F684840 ; Case insensitive string compare
6F68D58D 83C4 08 ADD ESP,8 ; Clean up Stack
6F68D590 85C0 TEST EAX,EAX ; See if we have a match
6F68D592 75 0E JNZ SHORT D2Common.6F68D5A2 ; If not goto next test
6F68D594 C646 08 02 MOV BYTE PTR DS:[ESI+8],2 ; Set Output to type 2 (Pandemonium Portal)
6F68D598 A3 F0FFD46F MOV DWORD PTR DS:[6FD4FFF0],EAX ; Clear Pandemonium Quest Status
6F68D59D ^E9 D653F8FF JMP D2Common.6F612978 ; Jump back to original code for more processing
6F68D5A2 834424 FC 13 ADD DWORD PTR SS:[ESP-4],13 ; Add offset for next custom string (pand finale)
6F68D5A7 83EC 04 SUB ESP,4 ; Emulate push of string location
6F68D5AA 55 PUSH EBP ; String to compare to
6F68D5AB E8 9072FFFF CALL D2Common.6F684840 ; Case insensitive string compare
6F68D5B0 83C4 08 ADD ESP,8 ; Clean up stack
6F68D5B3 85C0 TEST EAX,EAX ; See if we have a match
[/color][color=red]6F68D5B5 75 0E JNZ SHORT D2Common.6F68D5C5 ; If not then goto next test[/color]
[color=black]6F68D5B7 C646 08 03 MOV BYTE PTR DS:[ESI+8],3 ; Set output to type 3 (Pand Finale Portal)
6F68D5BB A3 F0FFD46F MOV DWORD PTR DS:[6FD4FFF0],EAX ; Clear Pandemonium Quest Status
6F68D5C0 ^E9 B353F8FF JMP D2Common.6F612978 ; Jump back to original code for more processing
[/color][color=green]6F68D5C5 8B7C24 FC MOV EDI,DWORD PTR SS:[ESP-4]
6F68D5C9 83C7 1A ADD EDI,1A ; Add offset to next custom string (lvl=)
6F68D5CC 56 PUSH ESI ; Save ESI
6F68D5CD 8BF5 MOV ESI,EBP ; EBP - String to compare to
6F68D5CF B9 04000000 MOV ECX,4 ; Only check 4 characters
6F68D5D4 F3:A6 REPE CMPS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI] ; Compare strings
6F68D5D6 5E POP ESI ; Restore ESI
6F68D5D7 ^0F85 D953F8FF JNZ D2Common.6F6129B6 ; If we do not match goto next test in original code
6F68D5DD 3E:807D 04 00 CMP BYTE PTR DS:[EBP+4],0 ; Make sure we have another parameter
6F68D5E2 ^0F84 CE53F8FF JE D2Common.6F6129B6 ; If we do not match goto next test in original code
6F68D5E8 83C5 03 ADD EBP,3 ; Skip over already checked chars. Used 3 for loop to work
6F68D5EB B9 05000000 MOV ECX,5 ; Only check 4 characters
6F68D5F0 8BC5 MOV EAX,EBP
6F68D5F2 40 INC EAX ; Point to next character
6F68D5F3 8038 41 CMP BYTE PTR DS:[EAX],41 ; See if we found Capital A
6F68D5F6 ^E0 FA LOOPDNE SHORT D2Common.6F68D5F2 ; Loop until we find it or CX reaches 0
6F68D5F8 85C9 TEST ECX,ECX ; See if we dropped out of loop
6F68D5FA ^0F84 B653F8FF JE D2Common.6F6129B6 ; If we did not find (A) then go back to original code
6F68D600 8BD8 MOV EBX,EAX ; Save pointer to A
6F68D602 C603 00 MOV BYTE PTR DS:[EBX],0 ; Clear out A to separate parameters
6F68D605 807B 01 00 CMP BYTE PTR DS:[EBX+1],0 ; See if another parameter follows A
6F68D609 ^0F84 A753F8FF JE D2Common.6F6129B6 ; If not then go back to original code
6F68D60F 45 INC EBP
6F68D610 55 PUSH EBP
6F68D611 E8 8573FFFF CALL D2Common.6F68499B ; Convert ASCII to numeric
6F68D616 83C4 04 ADD ESP,4 ; Clean up stack
6F68D619 C646 08 04 MOV BYTE PTR DS:[ESI+8],4 ; Set to type 4 (Custom Portal)
6F68D61D 66:8946 06 MOV WORD PTR DS:[ESI+6],AX ; Save Destination ID in Quality field
6F68D621 43 INC EBX ; Adjust pointer to point to Act parameter
6F68D622 53 PUSH EBX
6F68D623 E8 7373FFFF CALL D2Common.6F68499B ; Convert ASCII to numeric
6F68D628 83C4 04 ADD ESP,4 ; Clean up stack
6F68D62B 66:8946 02 MOV WORD PTR DS:[ESI+2],AX ; Save Act in item type field.
6F68D62F 33DB XOR EBX,EBX ; Set remaining parameters to 0
6F68D631 ^E9 4253F8FF JMP D2Common.6F612978 ; Go back to original code for more checks[/color]
Code: Select all
[color=black]
000A6CF0 50 61 6E 64 65 6D 6F 6E 69 75 6D 20 50 6F 72 74 Pandemonium Port
000A6D00 61 6C 00 50 61 6E 64 65 6D 6F 6E 69 75 6D 20 46 al.Pandemonium F
000A6D10 69 6E 61 6C 65 20 50 6F 72 74 61 6C 00 [color=green]6C 76 6C[/color] inale Portal.[color=green]lvl[/color]
000A6D20 [color=green]3D[/color] 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [color=green]=[/color]...............
[/color]
Code: Select all
[color=black]
000FBC20 00 00 00 00 00 00 C9 6F 05 00 C9 6F 0A 00 C9 6F ......Éo .Éo..Éo
000FBC30 [color=green]D4 0A C9 6F[/color] 00 00 00 00 00 00 00 00 00 00 00 00 [color=green]Ô.Éo[/color]............
[/color]
Code: Select all
[color=red]
6FC90AD4 E9 2D620900 JMP D2Game.6FD26D06
[/color]
Code: Select all
[color=red]
6FC90228 3C 05 CMP AL,5
[/color]
Instead of replacing the object creation code destination level check at 0x6FD13E3F with NOPs, we will just expand the range and remove the specific check for the Cow Portal. Here is the code that we modified above which will remain unchanged:
Code: Select all
6FD13E3F E9 2A2D0100 JMP D2Game.6FD26B6E
6FD13E44 75 07 JNZ SHORT D2Game.6FD13E4D
Code: Select all
6FD26B6E 50 PUSH EAX
6FD26B6F 8B4424 34 MOV EAX,DWORD PTR SS:[ESP+34]
[color=red]6FD26B73 90 NOP
6FD26B74 90 NOP
6FD26B75 90 NOP
6FD26B76 90 NOP
6FD26B77 90 NOP
6FD26B78 83F8 01 CMP EAX,1
6FD26B7B 90 NOP
6FD26B7C 90 NOP[/color]
6FD26B7D 7C 14 JL SHORT D2Game.6FD26B93
[color=red]6FD26B7F 3D E7030000 CMP EAX,3E7[/color]
6FD26B84 77 0D JA SHORT D2Game.6FD26B93
6FD26B86 837C24 3C 3C CMP DWORD PTR SS:[ESP+3C],3C
6FD26B8B 75 06 JNZ SHORT D2Game.6FD26B93
6FD26B8D 58 POP EAX
6FD26B8E ^E9 D3D2FEFF JMP D2Game.6FD13E66
6FD26B93 58 POP EAX
6FD26B94 ^E9 B4D2FEFF JMP D2Game.6FD13E4D
Code: Select all
[color=green]
6FD26D06 > 83EC 0C SUB ESP,0C
6FD26D09 . 53 PUSH EBX
6FD26D0A . 56 PUSH ESI ; ESI - Pointer to cube output structure
6FD26D0B . 57 PUSH EDI
6FD26D0C . 8BF2 MOV ESI,EDX ; EDX - ptUnit
6FD26D0E . 8BF9 MOV EDI,ECX
6FD26D10 . 56 PUSH ESI ; ptUnit
6FD26D11 . E8 9449FFFF CALL <JMP.&D2Common.#10342> ; Get hRoom from ptUnit
6FD26D16 . 8BD8 MOV EBX,EAX ; Save hRoom
6FD26D18 . 85DB TEST EBX,EBX ; See if 0
6FD26D1A . 0F84 89000000 JE D2Game.6FD26DA9 ; If so then exit
6FD26D20 . 53 PUSH EBX ; hRoom
6FD26D21 . E8 3649FFFF CALL <JMP.&D2Common.#10057> ; Get Level from hRoom
6FD26D26 . 50 PUSH EAX ; Level
6FD26D27 . E8 384AFFFF CALL <JMP.&D2Common.#10001> ; Get Act from level
6FD26D2C . 8B4C24 04 MOV ECX,DWORD PTR SS:[ESP+4] ; Get cube output structure pointer
6FD26D30 . 33D2 XOR EDX,EDX
6FD26D32 . 66:36:8B51 02 MOV DX,WORD PTR SS:[ECX+2] ; Get Act from cube recipe
6FD26D37 . 3BC2 CMP EAX,EDX ; See if current Act matches cube recipe
6FD26D39 . 75 6E JNZ SHORT D2Game.6FD26DA9 ; If not then exit
6FD26D3B . 8D5424 10 LEA EDX,DWORD PTR SS:[ESP+10]
6FD26D3F . C74424 0C 00000000 MOV DWORD PTR SS:[ESP+C],0
6FD26D47 . 52 PUSH EDX ; ptGame?
6FD26D48 . 56 PUSH ESI ; ptUnit
6FD26D49 . E8 BC4CFFFF CALL <JMP.&D2Common.#10332> ; Get hPath from ptUnit
6FD26D4E . 8D4424 0C LEA EAX,DWORD PTR SS:[ESP+C]
6FD26D52 . 8D5424 10 LEA EDX,DWORD PTR SS:[ESP+10]
6FD26D56 . 6A 04 PUSH 4 ; /Arg4 = 00000004
6FD26D58 . 50 PUSH EAX ; |Arg3
6FD26D59 . 68 00040000 PUSH 400 ; |Arg2 = 00000400
6FD26D5E . 6A 03 PUSH 3 ; |Arg1 = 00000003
6FD26D60 . 8BCB MOV ECX,EBX ; |
6FD26D62 . E8 D9FBF6FF CALL D2Game.6FC96940 ; \D2Game.6FC96940
6FD26D67 . 8B4C24 04 MOV ECX,DWORD PTR SS:[ESP+4] ; Get cube output pointer
6FD26D6B . 33D2 XOR EDX,EDX
6FD26D6D . 66:36:8B51 06 MOV DX,WORD PTR SS:[ECX+6] ; Get destination from cube recipe
6FD26D72 . 85D2 TEST EDX,EDX ; See if 0
6FD26D74 . 74 33 JE SHORT D2Game.6FD26DA9 ; If so then exit
6FD26D76 . 8B4424 0C MOV EAX,DWORD PTR SS:[ESP+C] ; Get ptRoom
6FD26D7A . 85C0 TEST EAX,EAX ; See if 0
6FD26D7C . 74 2B JE SHORT D2Game.6FD26DA9 ; If so exit.
6FD26D7E . 8B4C24 14 MOV ECX,DWORD PTR SS:[ESP+14] ; Y Position from hPath
6FD26D82 . 6A 00 PUSH 0 ; Non Permanant portal
6FD26D84 . 6A 3C PUSH 3C ; Red Portal
6FD26D86 . 6A 00 PUSH 0 ; Unit / Source ptr
6FD26D88 . 52 PUSH EDX ; Destination ID
6FD26D89 . 8B5424 20 MOV EDX,DWORD PTR SS:[ESP+20] ; X Position from hPath
6FD26D8D . 51 PUSH ECX ; Y position
6FD26D8E . 52 PUSH EDX ; X Position
6FD26D8F . 50 PUSH EAX ; ptRoom
6FD26D90 . 8BD6 MOV EDX,ESI ; ptPlayer
6FD26D92 . 8BCF MOV ECX,EDI ; ptGame
6FD26D94 . E8 57D0FEFF CALL D2Game.6FD13DF0 ; Create portal Object
6FD26D99 . 85C0 TEST EAX,EAX ; See if successful
6FD26D9B . 74 0C JE SHORT D2Game.6FD26DA9 ; If not then exit
6FD26D9D . B8 01000000 MOV EAX,1 ; Success
6FD26DA2 . 5F POP EDI
6FD26DA3 . 5E POP ESI
6FD26DA4 . 5B POP EBX
6FD26DA5 . 83C4 0C ADD ESP,0C
6FD26DA8 . C3 RETN
6FD26DA9 > 56 PUSH ESI
6FD26DAA . BA 14000000 MOV EDX,14 ; Start of failure code
6FD26DAF . 8BCE MOV ECX,ESI
6FD26DB1 . E8 CA56F9FF CALL D2Game.6FCBC480 ; "I can't" statement
6FD26DB6 . 33C0 XOR EAX,EAX ; Failure
6FD26DB8 . 5F POP EDI
6FD26DB9 . 5E POP ESI
6FD26DBA . 5B POP EBX
6FD26DBB . 83C4 0C ADD ESP,0C
6FD26DBE . C3 RETN
[/color]
lvl=133A4
lvl= tells the parser that it is a custom portal.
133 is the destination level ID
A is the Act parameter marker
4 is the act for the destination level. This is 0 based so this would be Act 5.
The level ID is limited to 3 characters so your max level is 999. This can be changed to 4 characters by changing the line at 0x6FDCD5E1 in the parsing code and the upper limit at 0x6FD26B7F in the object creation code. You can also limit portal activation by difficulty level using the min diff field in the recipe. Also note that the syntax is exactly like I have stated above and the A marker must be capitalized or the recipe will not work. I tried putting enough checks in so that syntax errors in the output will not be catastrophic, but one cannot catch everything.
That’s about it to cubing red portals. Thanks to Kingpin for his really helpful tutorial, Nefarius for the random number code and everyone who contributes to the Phrozen Keep for their invaluable knowledge.
The files for this tutoral are here in the file center. Note that this post contains the most up to date information.
The VillageIdiot
Edit: Added note about relocations of strings for portal names in cube recipe. Also added version to title.
Edit: Cleaned up Pandemonium Finale function. Added Pandemonium Portal function. Relocated some code to fit with Extended Levels Plug-In.
Edit: Add 'soft' portal code so that destination can be specified in cube formula instead of hard coded.
Edit: Fixed a jmp that needed to be NOP'd out.
Edit: Fixed use of term relocate to be more proper.
Edit: Added destination level check back to soft portal section. Fixed some formatting. Added note about using D2Client and D2Common pre-edited with extended levels plug-in.
Edit: Added link to file center for tutorial files. I also have discovered a minor bug. The portal status does not reset when a character is reloaded and the whole game needs a restart to reset the portal status.
Edit: Forgot to add the search string for the custom portals. Added recommendation to use rebased D2Common.
Edit: Changed D2Common addresses to use rebased locations. Added code to reset the Pandemonium Quest status when parsing the Pandemonium recipes. This fixes the issue where you would need to close the game once you open the portals to get them to reopen.