Posts for btl

btl
He/Him
Experienced Forum User
Joined: 3/20/2018
Posts: 6
So I did a run of mission 2, which unfortunately desynced at the start of mission 2. Can the sound issues be the cause of the desync? I made a video after the desync occurs and music/voices are gone. However, military victory results in 2 minutes and 24 seconds from start to victory, which is a 1 minute and 40 seconds improvement over the "refinery capture"-method. Forgot to make the troops attack the barracks at the end, so there might be some time to save there. Here's the video from the remaining part of mission 2: Link to video
btl
He/Him
Experienced Forum User
Joined: 3/20/2018
Posts: 6
Did a test TAS of the beginning of mission 1 and noticed that the voices suddenly disappeared after the announcer says "Warning enemy unit approaching from ...", it actually silences right after "Warning". Found this thread describing the problem, and tried a run using the "fixed" PAK-files, but with those there was no voices at all. I think this is against the "tampering" rule anyway. Verified that it didn't happen in DOSBox, so I'm not sure if it can be fixed by using some other settings, will have to try different setups. I did also discover a reliable way to trigger the "1000 credits" bug, which is usable for RTA too. Immediately after starting the mission, you press F3 to open the construction menu, and press B to start building a concrete slab. After the building has used 1 credit you press F3 and B again, and when the screen is closed you press O to put the building on hold. If you did it correctly the credits should now stay at 1000, and is faster than restarting the mission. Link to video
btl
He/Him
Experienced Forum User
Joined: 3/20/2018
Posts: 6
I got it fixed by using "I5" instead of "I7" in the BLASTER-variable, so the voices are there now when I click on units! A subtle detail that's easy to miss :) I double checked mission 1, with Harkonnen, and it didn't finish the level when I killed all enemy units, so it's spice only. I only replicated the RTA run, and at the time only the construction version was used, and I guess it's the fastest one if the game still counts ticks when inside the construction menu. If you search through the source code for the RNG-function, you'll find it being used for randomizing animation and other things. It doesn't seem to be any other way than through animation it can be manipulated actively. So as it stands now it's "impossible" to control it consistently; so I guess harvester micromanagement will be the way to go.
btl
He/Him
Experienced Forum User
Joined: 3/20/2018
Posts: 6
Truncated wrote:
[...] So, perhaps it's already abandoned, but to give some feedback on this: - The sound card is not properly set up and Dune 2 falls back to midi soundeffects. This probably needs a redo to fix. All the other stuff is minor timesavers: - I think you can move your units, at least quick ones like quads, between their attacks. This means they have to move less distance to attack their next target when the current one dies. - It doesn't seem like the positioning tricks are used, to deal 100% damage and receive 50% damage. For example at 3:30, two Harkonnen Quads are dealing half damage and one is dealing full damage. I think positioning the enemy unit outside the screen contributes a lot less. - The troopers seem to be positioned at a distance on purpose, to get the missile attack rather than the bullet attack. But the bullet attack is actually 25% stronger, so this gives less damage rather than more. - Attacking head-on means you reach the enemy base a bit faster, but you cannot draw advantage of tricking the enemy to attack his own buildings. - The stage with less starting and enemy units is picked, to take over the refinery and win by passing the spice quota, like the WR speedrun. I wouldn't rule out that that complete military victory on one of the other stages is faster under TAS conditions.
This was my first try for a TAS and JPC-RR, so I just jumped off the deep end here ... - I followed the instructions for JPC-RR, http://tasvideos.org/EmulatorResources/JPC.html, for setup of the sound card. It did work, so I thought nothing more of it - what would be the correct settings here? - Played around with that in my first WIP, although I just did it to kill time, but if I remember correctly I made a mental note of possibly using it to avoid damage from enemies. - I discovered the positioning tricks after publishing that video, since I did some more thorough research after not beating the RTA record for mission 2 :) - I don't remember why I put the troopers there, but probably because I thought the rockets did more damage. - Here I just mimicked the RTA WR, but in later tries I noticed the trike fired at the building when I was positioned correctly with my unit. - I wasn't aware that it could be defeated with military victory. That might be worth a try in the map were Harkonnen starts on the top, which gives easy positioning for extra damage. I also noticed in the "insider's guide" that mission 1 has both credits and MV listed as conditions - is MV possible there also? Have done a handful of runs up to capturing the harvester, and when it didn't improve my time I just restarted. Also tried figuring out the RNG for harvesting, which would be super helpful in later levels, but after looking at the source code for OpenDune it doesn't seem to be an easy way to do it. I started on a run to maximize the credits to be stolen when taking over the construction yard, but I think I'll just try doing a MV-run to see how that turns out.
Post subject: Finding the RNG memory address in Dune 2 for JPC-RR
btl
He/Him
Experienced Forum User
Joined: 3/20/2018
Posts: 6
I was doing a test TAS of Dune 2 and in level 2 you need to harvest spice to beat the level, and harvesting is based on RNG and bruteforcing just didn't seem viable after several failed attempts. I've read some information about RNG prediction and various guides on luck manipulation scattered on tasvideos.org, but none of this information applied directly to JPC-RR. There's also an open source reverse engineered version of Dune 2 available, which made it easy to see how RNG is done in Dune 2! When a harvester harvests it makes two decisions using RNG, in the function Script_Unit_Harvest. The first check is if it will be adding 1 spice to its total harvested amount, and as you see from the code here it only harvests when the RNG gives it an even number.
Language: c

u->amount += Tools_Random_256() & 1;
After that, though I'm not 100% sure here, it does another check where it's decided if the amount of spice on the ground is decreased by 1:
Language: c

if ((Tools_Random_256() & 0x1F) != 0) return 1; // If the RNG-value is divisible by 32 it returns from the function which skips the next line where the spice on the ground is decreased. Map_ChangeSpiceAmount(packed, -1); // Decrease the spice on the ground by 1 spice.
I couldn't figure out what the return values 0 and 1 means, but locking the RNG-seed resulted in the harvester eating through a patch of spice in record time. The RNG-method is in the function Tools_Random_256, where you can see that the RNG-value is stored in a static variable. JPC-RR doesn't have any means of debugging that I've found, however DOSBox got a built-in debugger, that can be used. So to follow along here you need to install DOSBox 0.74, download the precompiled debug version and put it in the folder with the regular executable. After DOSBox is ready you'll need to get Dune 2 running, start a new game and get to the point where a harvester arrives and starts harvesting. Other assumptions of knowledge is, but no limited to, assembler, working with hexadecimal values and memory addressing. When the harvester gets at least 1 percent full, you press Alt+Pause to break in the debugger, and leave the DOSBox window with Ctrl+F10. Activate the debug window and press ENTER to show the text. To dump the memory you need to use the command MEMDUMPBIN. Run the command "DOS MCBS" to find which segment and size you need to dump. Output from the "DOS MCBS"-command:
 210972115: MISC:MCB Seg  Size (bytes)  PSP Seg (notes)  Filename
 210972115: MISC:Conventional memory:
 210972115: MISC:   016F            16     0008 (DOS)
 210972115: MISC:   0171            64     0000 (free)
 210972115: MISC:   0176           256     0040
 210972115: MISC:   0187           144     0192
 210972115: MISC:   0191        580608     0192          DUNE2
 210972115: MISC:   (data addr 34EA:76A2 is 240674 bytes past this MCB)
 210972115: MISC:   8F52         12272     0000 (free)   ^L^L^L^L^L^L^L^L
 210972115: MISC:   9252         56000     0000 (free)
 210972115: MISC:Upper memory:
 210972115: MISC:   9FFF        196608     0008 (DOS)    SC
 210972115: MISC:   D000         65520     0000 (free)
In this case I had to do a memory dump for the segment "0191" with a size of "580608" which translates to "MEMDUMPBIN 0191:0 580608". The zero after "0191" is the start address. The dump is stored as MEMDUMP.BIN in %AppData%\..\Local\VirtualStore\Program Files (x86)\DOSBox-0.74, and you got make a copy of that file in another folder; I named them "memdump<times>-<percentage>.bin". After you've done that, press F5 to resume Dune 2 again, wait for the harvested amount to increase and break again by pressing Alt+Pause, and take another memory dump. Repeat this at least 5 times. I couldn't find any programs that allowed me to search for changes in memory dumps, so I hacked together a Python-script that could compare memory dumps and compare to a value.
Language: python

import sys import os EXIT_CODE_DIFFERENT_SIZE = -1 def main(initial_filename, changed_filename, diff_filename, compare_method): initial_dump = open(initial_filename, 'rb') changed_dump = SingleValueFileImpostor(changed_filename) \ if is_int(changed_filename) else open(changed_filename, 'rb') diff_output = open(diff_filename, 'wb') compare = get_compare_method(compare_method.strip('\'')) if not is_diff_file(initial_filename) and not (is_diff_file(changed_filename) or is_int(changed_filename)) \ and not same_size(initial_dump, changed_dump): eprint('When comparing raw files they must be equal in size.') exit(EXIT_CODE_DIFFERENT_SIZE) elif is_diff_file(initial_filename) and not is_diff_file(changed_filename): changed_dump.seek(0, 2) changed_max_address = changed_dump.tell() while True: bytes_address_and_value = initial_dump.read(5) if at_end_of_file(bytes_address_and_value): break address = int.from_bytes(bytes_address_and_value[0:4], byteorder='big') initial_value = bytes_address_and_value[4:5] if changed_max_address <= address: continue changed_dump.seek(address) changed_value = changed_dump.read(1) if compare(initial_value, changed_value): diff_output.write(address.to_bytes(4, byteorder='big')) diff_output.write(changed_value if not is_int(changed_filename) else initial_value) else: while True: address = initial_dump.tell() initial_value = initial_dump.read(1) if at_end_of_file(initial_value): break changed_dump.seek(address, 0) changed_value = changed_dump.read(1) if compare(initial_value, changed_value): diff_output.write(address.to_bytes(4, byteorder='big')) diff_output.write(changed_value if not is_int(changed_filename) else initial_value) initial_dump.close() changed_dump.close() diff_output.close() def is_int(value): try: int(value) return True except ValueError: return False def get_compare_method(compare_method): comparisons = { '==': lambda x, y: x == y, '<': lambda x, y: x < y, '>': lambda x, y: x > y, '<=': lambda x, y: x <= y, '>=': lambda x, y: x >= y, '!=': lambda x, y: x != y } return comparisons.get(compare_method, lambda x, y: False) def at_end_of_file(byte): return byte == b'' def same_size(file1, file2): initial_size = get_file_size(file1) changed_size = get_file_size(file2) return initial_size == changed_size def is_diff_file(filename): return filename[-4:].lower() == '.mdd' def get_file_size(file): current_position = file.tell() file.seek(0, 2) position = file.tell() file.seek(current_position, 0) return position def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) class SingleValueFileImpostor: def __init__(self, value): self.value = int(value) def seek(self, address, position=0): pass def tell(self): return sys.maxsize def read(self, size): return bytes([self.value]) def close(self): pass if len(sys.argv) != 5: eprint("Usage: " + os.path.basename( sys.argv[0]) + " <initial dump> '<method:==|<|>|<=|>=|!=)>' <changed dump|integer> <output diff[.mdd]>") else: main(sys.argv[1], sys.argv[3], sys.argv[4], sys.argv[2])
Using this script you can find the address for the harvester's current spice percentage by comparing them like this, but you'll have to change the compare method and values to match your harvester spice percentage:
.\memdumpdiff.py .\MEMDUMP1-1.BIN '<' .\MEMDUMP2-5.BIN mem12.mdd
.\memdumpdiff.py .\mem12.mdd '<=' 8 .\mem12le8.mdd
.\memdumpdiff.py .\mem12le8.mdd '<' .\MEMDUMP3-8.BIN mem23.mdd
.\memdumpdiff.py .\mem23.mdd '<' .\MEMDUMP4-11.BIN mem34.mdd
.\memdumpdiff.py .\mem34.mdd '>' 10 mem34b10.mdd
.\memdumpdiff.py .\mem34b10.mdd '<' 15 mem34l15.mdd
.\memdumpdiff.py .\mem34l15.mdd '<' MEMDUMP5-22.BIN mem45.mdd
.\memdumpdiff.py .\mem45.mdd '>=' 22 mem45b22.mdd
This left me with the file "mem45b22.mdd" which now contained the address and current harvester percentage value in hex. The mdd-file is a collection of 4+1 bytes, 4 bytes for the address and 1 byte for the value. To view its contents I used HxD that you can download from here: https://mh-nexus.de/en/hxd/ The memory address in my case was 0007986C, which isn't quite the way DOSBox addresses it, but you can set a memory change breakpoint on that address and it will work. The breakpoint command is as follows, "BPM 0191:7986C", and after you've added it you resume Dune 2 by pressing F5. After a little while it should break and you should be in the function I mentioned earlier - the assembly version of it. After a few seconds the debugger stops because the memory address value was changed (1), and from the address the breakpoint hit I scrolled up a bit and recognized the "harvest 1 percent spice" line (2):
1717:272E  9A04005F2B          call 2B5F:0004 <-- The address of the function where the RNG is calculated.
1717:2733  2401                and  al,01 <-- (2) The "& 1" part that adds spice to the harvester if the number is even.
1717:2735  C41E6862            les  bx,[6268]              ds:[6268]=0B84
1717:2739  268A5758            mov  dl,es:[bx+58]          es:[0BDC]=001D
1717:273D  02D0                add  dl,al
1717:273F  C41E6862            les  bx,[6268]              ds:[6268]=0B84
1717:2743  26885758            mov  es:[bx+58],dl          es:[0BDC]=001D <-- Actual instruction that changes the spice amount in the harvester.
1717:2747  C41E6862            les  bx,[6268]              ds:[6268]=0B84 <-- (1) Where the debugger stopped.
From here you can se that the proper address for the harvester spice amount is at "es:[bx+58]", the ES value is at the top in the debugger "ES=7A5A" and BX contains the value "0B84" as set on instruction "1717:2735". This means the "correct" address in memory can be viewed by adding BX to 58, which is 0B84+58=0BDC, and run the "view memory command" on "es:[bx+58]": D 7A5A:0BDC But in this case the value of the harvester is not that important, since you're trying to find the RNG-seed, and from the source code you know that it's a static value which resides on the same memory address every time. The call to the RNG-function is at instruction 1717:272E, which means the RNG-function is located at 2B5F:0004. By running the command "C 2B5F:0004" DOSBox will jump to that address in the instruction window, or if you want to step through the function you select the line at 1717:272E, press F9 to set a breakpoint there and press F5 to start Dune 2 again. When the breakpoint hits now, you press F11 to step into the call and press it five more times to get to 2B5F:000E. I won't go in depth on the actual implementation, but from this function you can extract the memory addresses for the RNG-seed:
2B5F:0006  B8EA34              mov  ax,34EA
2B5F:0009  8ED8                mov  ds,ax <-- Sets the DS-register to 34EA
2B5F:000B  BEA276              mov  si,76A2
2B5F:000E  8A04                mov  al,[si]                ds:[76A2]=8058 <-- This retrieves the byte "58" from the address ds:[si], which is at 34EA:76A2 in memory
2B5F:0010  D0E8                shr  al,1
2B5F:0012  D0E8                shr  al,1
2B5F:0014  D05402              rcl  byte [si+02],1         ds:[76A4]=0076 <-- \
2B5F:0017  D05401              rcl  byte [si+01],1         ds:[76A3]=7680 <-- These two instructions retrieves the next two bytes of the RNG-value.
This means that the RNG-value for Dune 2 is stored in memory in the addresses 34EA:76A2, 34EA:76A3 and 34EA:76A4. But that's segmented addresses and do you no good in JPC-RR, so you have to "convert" them to linear addresses; you can read more about that addressing here. The conversion is done by multiplying the segment address with 10 (hexadecimal) and adding the address to that value. Since the DOSBox address is in segment 0191, you have to "normalize" it by subracting that segment adress from the final address. So for my RNG-addresses the calculation of the "normalized" linear addresses are as follows: DOSBox segment address: 0191 * 10 = 1910 RNG segment address: 34EA * 10 = 34EA0 1st byte: 34EA0 + 76A2 - 1910 = 3AC32 2nd byte: 34EA0 + 76A3 - 1910 = 3AC33 3rd byte: 34EA0 + 76A4 - 1910 = 3AC34 Now all that remains to find those values in JPC-RR, which means you have to "translate" the DOSBox address to JPC-RR. To do this I made a memory dump from JPC-RR via Snapshot -> RAM Dump -> Binary and put it in the same folder as the DOSBox dumps. Then I opened the first DOSBox dump and the JPC-RR dump in HxD. I tiled the two windows side by side and searched for "dune2", an educated guess, and searched in both windows until I found a place where the memory matched for a big chunk of memory. I found that memory around the addresses 3B0D0 and 34D40 were equal and used that as an reference point. The difference between 3B0D0 and 34D40, 3B0D0-34D40, is 6390. This means you need to add 6390 to the RNG address from DOSBox to find it in JPC-RR. Adding this offset to your DOSBox RNG address gives the corresponding address in JPC-RR as follows: 1st byte: 3AC32 + 6390 = 40FC2 2nd byte: 3AC33 + 6390 = 40FC3 3rd byte: 3AC34 + 6390 = 40FC4 You can then read them with this simple lua-script while running Dune 2:
Language: lua

while true do a, b = jpcrr.wait_event(); if a == "lock" then if nowTick ~= jpcrr.clock_time() then nowTick=jpcrr.clock_time() print(jpcrr.read_byte(266178)) print(jpcrr.read_byte(266179)) print(jpcrr.read_byte(266180)) end jpcrr.release_vga(); end end
I also copypasted together this script that prints the next RNG-value that will be produced; it contains code that's borrowed from all over the place :):
Language: lua

addressList = {266178, 266179, 266180} function bitor(a,b)--Bitwise or p,c=1,0 while a+b>0 do ra,rb=a%2,b%2 if ra+rb>0 then c=c+p end a,b,p=(a-ra)/2,(b-rb)/2,p*2 end return c end function bitxor(a,b) r = 0 for i = 0, 31 do x = a / 2 + b / 2 if x ~= math.floor (x) then r = r + 2^i end a = math.floor (a / 2) b = math.floor (b / 2) end return math.floor(r) end function bitand(a, b) result = 0 bitval = 1 while a > 0 and b > 0 do if a % 2 == 1 and b % 2 == 1 then -- test the rightmost bits result = result + bitval -- set the current bit end bitval = bitval * 2 -- shift left a = math.floor(a/2) -- shift right b = math.floor(b/2) end return result end function lshift(x, by) return math.floor(x * 2 ^ by) end function rshift(x, by) return math.floor(x / 2 ^ by) end byteOld = {} if not searchList then searchList={} end if addressList then for i,v in ipairs(addressList) do table.insert(searchList, v) end end while true do a, b = jpcrr.wait_event(); if a == "lock" then if nowTick ~= jpcrr.clock_time() then nowTick=jpcrr.clock_time() s_randomSeed = {} currentSeed = 1 for i,v in ipairs(searchList) do byteValue = jpcrr.read_byte(v) if byteOld[v] == nil or byteOld[v] ~= byteValue then byteOld[v]=byteValue s_randomSeed[currentSeed] = byteValue currentSeed = currentSeed + 1 end end val16 = bitor(lshift(s_randomSeed[2],8),s_randomSeed[3]); val8 = bitand(rshift(bitxor(val16,32768),15), 1); val16 = bitor(lshift(val16, 1),bitand(rshift(s_randomSeed[1],1),1)) val8 = rshift(s_randomSeed[1],2) - s_randomSeed[1] - val8 -- Emulate overflow for a signed byte if val8 < 0 then val8 = 256 + val8 end s_randomSeed[1] = bitand(bitor(lshift(val8,7),rshift(s_randomSeed[1],1)), 255) s_randomSeed[2] = bitand(rshift(val16,8), 255) s_randomSeed[3] = bitand(val16,255) print("next random " .. bitxor(s_randomSeed[1], s_randomSeed[2])) end jpcrr.release_vga(); end end
That's it for this guide!
btl
He/Him
Experienced Forum User
Joined: 3/20/2018
Posts: 6
Dune 2 v1.0 Harkonnen mission 1 Link to video My first time doing a TAS! Did a couple of test runs mostly for learning JPC-RR, this is the video of the last run. It finishes the mission right over 3 minutes, but I've since discovered that version 1.07 seems to have a bug where you don't need to do any harvesting to win ... So I guess I'll have to check that version and do another run. Hunting down enemies lost me about 14 seconds I think, instead of just watching the harvester and doing nothing. I tried doing a little variation on killing the enemies with a main "quick kill" I used for the most part, with the variation so there wouldn't be so much waiting for the harvester, but I realized my numbers didn't add up near the end so I tried making the best of it. Am I on the right path for "publication quality" and "entertainment value"? Tips and pointers wanted! :) Should also note that I accidentally came across c-square's JPC-RR version with proper "mouse support", and making this TAS with it was a breeze compared to the "stock" version. If at all possible I vote for it to be listed as a download in the resource section for the emulators!