Miscellaneous Forums > Scripting and Reverse Engineering

[FF7] Chocobo Betting

<< < (3/5) > >>

ff7man:


https://anonfiles.com/z63aZ2Ifx0/chocotrainer_zip

I wrote a python trainer to quickly edit values. Because of the extreme similarity between the memory layouts between pc and psx, it works on both pc (ff7_en.exe) and on a playstation emulator (ePSXe.exe). You will need to manually update the process name and base address used by your computer. I recommend using cheat engine to search for the name of the first chocobo (ie: PAU = 0x352130, my string encoding script can generate this) and counting up 164 bytes in the memory viewer.

Updating the jockey does not update the color displayed.

Setting 2byte values to less than 256 does not work (how I wrote the trainer, easy to change if need be).
Setting run speed -> 256 has the chocobo run at a really slow speed
Setting top speed -> 256 has the chocobo sprint at a really slow speed
Bronze is the only jockey that has a variable run speed (run2) which will adjust well above or below run1.
If stamina is set really low, none of them will choose to run fully out of stamina
Whether or not a chocobo gets stuck behind another seems to be largely luck based. I think intel helps, but idk how much.
With save states, I've observed, sometimes increasing a chocobos run speed/top speed will cause it to rank lower.

Here's the best I've done so far at guessing the winners.

--- Code: ---#evaluator.py
from operator import attrgetter
import copy
count = 0
def merge(c1,c2):
    for i in range(len(c1)):
        c1[i].val = i
    for i in range(len(c2)):
        c2[i].val = i
    for i in range(len(c1)):
        for y in c2:
            if c1[i].name == y.name:
                c1[i].val += y.val
    return c1

def race(chocobos):
    #remove bronze/lowest jockey
    chocobos2 = []
    lowest = 0

    course = ""
    for c in chocobos:
        if c.place != 0:
            course = c.course

    for c in chocobos:
        if c.jockey != lowest:
            chocobos2.append(c)

    # we need to select 2 winners so,
    # if < 2 chocobos don't delete any
    if len(chocobos2) < 2:
        chocobos2 = chocobos

    chocobos3 = copy.deepcopy(chocobos2)
    # Sort by top speed, then jockey
    chocobos2.sort(key=attrgetter('topspeed','jockey'),reverse=True)
    # Sort by intel and run speed
    chocobos3.sort(key=attrgetter('runspeed'), reverse=True)
    #chocobos4 = copy.deepcopy(chocobos2)
    #chocobos4.sort(key=attrgetter('intel'), reverse=True)
    # check if we predicted the correct winners

    #chocobos2 = merge(chocobos2,chocobos3).copy()
    #chocobos2 = merge(chocobos2,chocobos4).copy()

    notchocobos2 = copy.deepcopy(chocobos2)
    for i in range(len(notchocobos2)):
        notchocobos2[i].val = float(i)
    nothing = 0
    for i in range(len(chocobos2)):
        chocobos2[i].val = float(i)
        # if we are a top runner bump it up
        for j in range(len(chocobos3)):
            if chocobos2[i].name == chocobos3[j].name:
                if j == 0:
                    chocobos2[i].val -= 3
                if j == 1:
                    chocobos2[i].val -= 2
                if j == 2:
                    chocobos2[i].val -= 0.5
                if j == 3:
                    chocobos2[i].val += 0
                if j == 4:
                    chocobos2[i].val += 1
                if j == 5:
                    chocobos2[i].val += 2

        # if we are smart bump it up
        if chocobos2[i].intel == 100:
            chocobos2[i].val -= 2
        # adjust for jockey course advantage/disadvantage
        if "2003" in course:
            if chocobos2[i].jockey == 3:
                chocobos2[i].val += 0.5
                if chocobos2[i].sprinting == 2:
                    chocobos2[i].val += .5
            elif chocobos2[i].jockey == 2:
                chocobos2[i].val -= 0.5
                if chocobos2[i].sprinting == 2:
                    chocobos2[i].val -= .5
            else:
                nothing +=1
        else:
            if chocobos2[i].jockey == 3:
                chocobos2[i].val -= 0.5
                if chocobos2[i].sprinting == 2:
                    chocobos2[i].val -= .5
            elif chocobos2[i].jockey == 2:
                chocobos2[i].val += 0.5
                if chocobos2[i].sprinting == 2:
                    chocobos2[i].val += .5
            else:
                nothing +=1
    chocobos2.sort(key=lambda x: (x.val,-x.topspeed, -x.jockey))
    success = 0
    if chocobos2[0].place <3:
        success +=1
    if chocobos2[1].place <3:
        success +=1
    if len(chocobos2)> 2:
        if chocobos2[2].place <3:
            success += 1
    #print("Predicted: "+str(chocobos2))
    #input("Hello")
    if success >1:
        return 1
    else:
        #print("og: "+str(notchocobos2))
        #print("predicted: "+str(chocobos2))
        chocobos2.sort(key=attrgetter('place'))
        #print("actual: "+str(chocobos2))
        #input("here")
        return 2

class Chocobo:
    def __init__(self,name,ts,stamina,sprinting,jockey,jockey2,course,order,place,intel,runspeed):
        self.topspeed = ts
        self.stamina = stamina
        self.sprinting = sprinting
        self.jockey = jockey
        self.jockey2 = jockey2
        self.name = name
        self.course = course
        self.order = order
        self.place = place
        self.intel = intel
        self.runspeed = int(runspeed)
        self.rss = int(runspeed)+int(ts)
        self.val = float(0)
    def __str__(self):
        return "name=%s speed=%s stamina=%s sprinting=%s jockey=%s ogjockey=%s course=%s order=%s place=%s intel=%s rs=%s rss=%s val=%s\n" % (self.name, self.topspeed, self.stamina/10, self.sprinting, self.jockey, self.jockey2, self.course, self.order, self.place, self.intel, self.runspeed, self.rss, self.val)
    def __repr__(self):
        return str(self)

def parse_csv(theFile):
    wins = 0
    losses = 0
    with open(theFile) as f:
        data = f.read()
    sdata = data.split("win-order,")
    for x in sdata:
        chocobos = []
        if "order-b152" in x:
            lines = x.split("\n")
            chocobos = []
            for i in range(0,len(lines)):
                if i >0 and i <7:
                    sline = lines[i].split(",")
                    name = sline[6]
                    place = sline[0]
                    jockey = int(sline[5])
                    # bronze -> silver -> gold -> plat
                    if jockey == 0: #plat
                        jockey = 3
                    elif jockey == 1: #gold
                        jockey = 2
                    elif jockey == 2: #bronze
                        jockey = 0
                    elif jockey == 3: #silver
                        jockey = 1
                    else:
                        print("not a jockey")
                    speed = sline[7]
                    stamina = sline[10]
                    end_stamina = sline[11]
                    sprinting = sline[12]
                    runspeed = sline[13]
                    intel = sline[14]
                    course = sline[17]
                    order = i-1
                    c = Chocobo(name,int(speed),int(stamina)/2,int(sprinting),jockey,jockey,course,int(order),int(place),int(intel),int(runspeed))
                    chocobos.append(c)
        if len(chocobos) == 6:
            res = race(chocobos)
            if res == 1:
             wins +=1
            elif res == 2:
             losses +=1
            else:
             print("how")
    return wins,losses

def run_rank(rank,thefile):
    w,l = parse_csv(thefile)
    avg = round(((w / (w + l) * 100)),2)
    print(rank+"\t"+str(w)+"\t"+str(l)+"\t"+str(avg))
    return w,l

if __name__ == '__main__':
    tw,tl = 0,0
    print("Class\tWins\tLosses\tAvg")
    w,l = run_rank("C","c1000.csv")
    tw += w
    tl += l
    w,l = run_rank("B","b1000.csv")
    tw += w
    tl += l
    w,l = run_rank("A","a1000.csv")
    tw += w
    tl += l
    w,l = run_rank("S","s1000.csv")
    tw += w
    tl += l
    avg = round(((tw / (tw + tl)) * 100),2)
    print("total\t"+str(tw)+"\t"+str(tl)+"\t"+str(avg))


--- End code ---


--- Code: ---Class Wins Losses Avg
C 940 60 94.0
B 943 57 94.3
A 957 43 95.7
S 951 49 95.1
total 3791 209 94.77

--- End code ---

ff7man:
I took another look at this and wanted to consider possible RNG manipulation and I think I've found something.

With bizhawk on the psx version I found there is a sole single 4 byte memory address at 0x51568 that when frozen will generate the same chocobo races, winning items, and end result. That address starts at 0 and increments seemingly by one for every frame after the playstation logo, I stop watched it at ~33/s. We can power cycle the console and have it always start at 0. Using the bizhawk emulator we can set the seed, frame perfectly start the race, and pull out the results, then repeat for a window of time we care about. So if it takes 60s to run up to the chocobo counter from a save we would generate the races from 60s to 70s to make a 10s window of the races where we know the result. This is a much smaller task instead of needing to generate 4.3 billion races per class, now we only need to generate 300 races per class. The timer method seems like a very viable strategy, I just need to script up something to pull out the race data to search through and confirm.

ff7man:
Ok I wrote a macro to automate binhawk to edit the memory address, run the race, pull out the win order with opencv, dump the memory, pull the chocobo stats, and repeat.

The 10 second window is actually ~600 of the counter at 0x51568, I timed myself running from the save at the ropeway station which took about 70s, I then added 20s for padding.
At the 90 second mark you should be at frame 5400.
To make matters worse whether or not it is a short or long course is a 50/50 determined by 0x95DC8
So we'll have to guess two tickets and do 1200 automations ~1min/piece for each rank.

It's going to take a while to run this. About 20 hrs for each rank assuming the script doesn't break.

I'm a little concerned chocobo behavior might be linked to some other variable and and I'm gonna sink a lot of time to be able to only get a 95% or so level of accuracy.

I won't be able to test this until 20hrs of automation run without issue. 

ff7man:
Ok the first batch finished and the data set is confirmed working for class B short courses with PSX for the 90-100s window on actual hardware :)

https://pastebin.com/ChgYj9kD
 
You can also use a PS2 with fast load as well. I'll share once the script runs for all the classes.

Xbox/iOS/PS4/PC does not work. They must not count frames to seed the minigame. I'm not sure what it's doing, perhaps @nfitc1 could chime in.

The pointers you listed here don't contain values that change
https://forums.qhimm.com/index.php?topic=11514.0

ff7man:
Taking a look at the PC side of things. Random should be baked into kernel.bin/kernel2.bin so that makes me think it should share how it's generated with the psx.

With cheat engine I found the variable that determines course length is a single byte at FF7_EN.exe+8BF588 which can be frozen using a 1ms interval as a thread always creating long or short races. That's promising.

FF7_EN.exe+5A070C appears to be incrementing with each frame. I can't progress the game frame by frame to verify, but it does increase by 600/10s. This counter starts once you load a save. The names don't match and the speed and stamina values in our time window from PSX do not work for when this value gets to 5400. I tried freezing the value but it does not create the same chocobos and neither does nulling out what increments this value. You can find it in ghidra at FUN_0040ab81 which 0xDC08B8 stores the in game time and increments the counter. Cheat engine can null out the change by searching for what modifies that address. You can also change A1 0C 07 9A 00 83 C0 01 to A1 0C 07 9A 00 90 90 90 with hxd to have it not add one to the counter. Telling cheat engine to follow what accesses this value doesn't show anything extra on the betting menu load, although it doesn't show anything for the course byte either which we know impacts the result.

When I pause with the counter not nulled or patched then unpause the game the frame counter jumps by the time it was paused, kinda weird.

Navigation

[0] Message Index

[#] Next page

[*] Previous page

Go to full version