Custom AI Championship Tutorial

Discussion in 'Modding' started by prescient, Dec 29, 2019.

  1. prescient

    prescient Registered

    Joined:
    Dec 20, 2019
    Messages:
    9
    Likes Received:
    12
    I've been wanting to create a custom GT3 AI championship for offline racing for a while now. However, it doesn't seem to be clearly documented and the information I needed was scattered across different threads in different forums. My aim here is to provide start to finish documentation for how I organized in-game assets, created a few scripts and finally got to a working AI championship within rfactor 2. This whole process is probably fragile so if studio 397 makes changes I have no doubt it will bork my process.

    Starting out I had a few requirements for the championship:
    1. I need consistent driver names between races for tracking in rf2 log analyzer (great tool!)
    2. I need to be able to set driver profiles and skills
    3. I need to be able to swap out cars and skins for a driver easily
    4. I need to be able to move drivers between teams easily
    5. I don't want to perform steps 1-4 manually
    Below is the process I figured out. Better processes may exist.

    Step 1: Creating an asset library.
    The rfactor 2 in-game assets are stored in your C:\Program Files (x86)\Steam\steamapps\common\rFactor 2\Installed\Vehicles folder. We need to create our own library of assets if we are going to be able to easily swap drivers, cars and skins within a season. For GT3 cars we can create our own library by going to the car folder (ex: C:\Program Files (x86)\Steam\steamapps\common\rFactor 2\Installed\Vehicles\Audi_R8LMS_GT3_2018\1.51) and extracting the contents of the car-upgrade.mas file using either
    • The command line with ModMgr.exe found in the C:\Program Files (x86)\Steam\steamapps\common\rFactor 2\Bin32 folder
    • Or the MAS2.exe extraction tool found in the C:\Program Files (x86)\Steam\steamapps\common\rFactor 2\Support\Tools folder
    I chose to use the command line app because it can be scripted to automate extraction, and we need to extract files for all of the vehicles and skins we want to use in our AI Championships. The command line call looked similar to the following for the Porsche GT3:
    Now that we know how to extract the data we need to create a location to extract it to. For this I created a folder called "vehicles" and the directory structure was as follows:
    [​IMG]

    The Porsche_911_GT3_R_2018 folder is the same as the folder name used by the game and this will come in handy later when we create our directories in the C:\Program Files (x86)\Steam\steamapps\common\rFactor 2\UserData\player\Settings folder.

    Within the Porsche_911_GT3_R_2018 we now have a bunch of veh, png, json, and dds files. For each skin I wanted to use I named a folder with the skin name and put the files in the folder. Now we have a directory structure that looks like the following:
    [​IMG]

    Within the Porsche_911_GT3_R_2018 the contents of the fanatec directory are:
    [​IMG]
    I renamed the DDS and JSON files to alt because that is what rFactor 2 does when you create a new AI through the main menu. I suspect you could have different names, but this is now consistent with what the devs did.

    Now that we have all of the files in the correct directory we need to create a directory listing with attributes that we will link to our drivers. This is simply a table that lists the:
    • car
    • skin name
    • vehicle file (xxxx.veh)
    • skin folder
    • unique id for look ups
    [​IMG]
    Most of the creation of this file was automated by simply pulling a listing of directories and files, and dumping them to tables.

    Step 2: Creating driver profiles

    Now that we have an asset library we need to create profiles for the drivers that we will be racing against. This is pretty simple as I store my profiles in an excel table much like the vehicle table above.

    [​IMG]

    In this table we have the following fields:
    • first_name: Driver's first name
    • last_name: Driver's last name
    • number: Driver's car number (this must be unique)
    • team: Driver's team (can be anything)
    • unique_id: This is the car/skin the driver will use. We will use this to link to the unique_id in the vehicle table
    • speed: Driver's speed on track
    • qualify_speed: Driver's qualifying speed
    • wet_speed: Driver's wet speed
    • aggression: Driver's aggresion
    • composure: Pretty sure this determines how well a driver stays focused after being passed
    • crash: not sure probability of crashing?
    • completed_laps: whether the driver will complete all of their qualifying laps?
    • min_racing_skill: min driving skill
    • start_skill: how well a driver starts off the line
    • recovery: not sure. maybe crash or spin recovery?
    • reputation: pretty sure this is deprecated based on other threads
    • courtesy: pretty sure this is deprecated based on other threads.
    My knowledge on these fields is based on this thread: https://forum.studio-397.com/index.php?threads/rcd-talent-file.52996/. If there is a more definitive source I'd appreciate being pointed in the right direction.

    Now that we have our table setup we only have to create rcd files and link them to vehicles.

    Step 3: Creating vehicles in the player/settings folder.
    Now that we have done all of the hard work the next part is easy. We simply have to create a script that:
    • Takes the driver profiles and creates an rcd for each profile
    • Creates a new directory in the C:\Program Files (x86)\Steam\steamapps\common\rFactor 2\UserData\player\Settings folder for each vehicle and AI skin
    • Copy the skins from our assets library to the AI skin directory
    First we need to create a folder for each vehicle we will use. For example, if we want to use the Audi R8 we need a folder in player/settings named "Audi_R8LMS_GT3_2018". Once we have done that we can create the RCD for the AI.

    To create the RCD file I've written a little script in R with an output that looks like this:
    //[[gMa1.002f (c)2016 ]] [[ ]]
    GT3
    {
    Brian O'Conner
    {
    Team = 2018 Audi R8 LMS
    Component = Audi_R8LMS_GT3_2018
    Skin = alt.dds
    VehFile = R8LMS_77FDF19309.VEH
    Description = #43 Audi_R8LMS_GT3_2018
    Number = 43
    Classes = GT3 fast_n_furious
    Category = fast_n_furious
    Aggression = 80
    Reputation = 80
    Courtesy = 80
    Composure = 80
    Speed = 100
    QualifySpeed = 100
    WetSpeed = 90
    StartSkill =100
    Crash = 0
    Recovery = 80
    CompletedLaps = 100
    MinRacingSkill = 90
    }
    }

    A few items to note on RCD file:
    1. GT3 has to be included in classes or the AI skill settings won't work
    2. The fast_n_furious in the classes field lets us filter on only the AI we have created in the opponents menu. Note that rFactor 2 organizes categories alphabetically with capital letters first and then lower case letters [A-Z][a-z].
    3. I'm pretty sure the category field is what you will see when you select vehicles in the car selection screen. So all of our custom skins would be listed under fast_n_furious.
    4. You should create a car for yourself that you select when racing in the league. This allows you to select that specific car from the menu and you won't overwrite any of your AI opponents
    5. You have to specify the vehicle file you are working with as this is linked to the car somehow. Not quite sure on the internals here.
    6. If you misspell anything in the RCD it simply doesn't work

    We name our RCD files 0.rcd through xx.rcd where xx is the number of drivers in the folder. Next we create a folder with the driver name. The driver name folder contains the car skin. From our assets library/vehicles directory we copy the car skin using the unique_id selected for our car in the drivers table. If we do this for all of our drivers the directory should look something like this:
    [​IMG]

    And the Brian O'Conner folder contains the alt.dds, alt_region.dds, and alt.json files from the Audi Olimp car skin directory:
    [​IMG]

    To test that it is working correctly you can assign all of the AI to a single car and have them qualify. The AI opponents with low qualifying speed should perform poorly and opponents with high qualifying speed should perform well. We can see that is the case in the following image (except Mia as she might have crashed/spun).
    [​IMG]

    If we want to change any of the names, skills, teams, or cars we can simply edit our drivers table and re-run the script we created. Note this will wipe out whatever we had assigned previously.

    And that is it. I'll leave my script below that sets this up but I imagine that it is only useful if you know R. Once you have your championship setup I highly recommend tracking results with rFactor 2 Log Analyzer.

    I hope this is helpful for people even if they don't know R as it took me a while to figure out the vehicles files, skins, rcd files, etc and might save some time for others. Additionally, it documents what I did for when I forget two months from now.
     
    Last edited: Dec 30, 2019
  2. prescient

    prescient Registered

    Joined:
    Dec 20, 2019
    Messages:
    9
    Likes Received:
    12
    Link to the spreadsheet I use for managing my AI drivers and attributes: https://docs.google.com/spreadsheet...o0JLQ04b-dQXFoWYuOat9CPdUo8No-0tY0VGw/pubhtml

    Script for setting up my AI:
    Code:
    require(stringr)
    require(tidyverse)
    require(readxl)
    
    # Params ------------------------------------------------------------------
    player_dir    <- "C:/Program Files (x86)/steam/steamapps/common/rFactor 2/UserData/player/Settings/"
    vehicles_dir  <- "C:/Users/[user_here]/Google Drive/rfactor2/Vehicles/"
    excel_file    <- "vehicle_directory.xlsx"
    car_class     <- "Fast_and_Furious"
    vehicle_sheet <- 'vehicle_directory'
    league_sheet  <- 'league'
    
    
    # RCD data ----------------------------------------------------------------
    
    header <- "//[[gMa1.002f (c)2016    ]] [[            ]]
    GT3
    {"
    
    # Load data ---------------------------------------------------------------
    
    setwd(vehicles_dir)
    cars <- read_xlsx(excel_file, sheet = vehicle_sheet)
    drivers <- read_xlsx(excel_file, sheet = league_sheet)
    drivers <- merge(drivers, cars, by = "unique_id")
    
    
    # Logic -------------------------------------------------------------------
    
    #create rcd idx
    drivers <- drivers %>%
      group_by(car) %>%
      mutate(
        rcd_num = row_number() - 1
      )
    
    #delete existing directories
    for(i in 1:nrow(cars)){
      if(dir.exists(paste0(player_dir, cars$car[i]))){
        unlink(paste0(player_dir, cars$car[i]), recursive = T)
      }
    }
    
    for(i in 1:nrow(drivers)){
      #create car directory
      if(!dir.exists(paste0(player_dir, drivers$car[i]))){
        dir.create(paste0(player_dir, drivers$car[i]))
      }
     
      #create ai directory
      ai_dir <- paste0(player_dir, drivers$car[i], "/", drivers$first_name[i], " ", drivers$last_name[i])
      dir.create(ai_dir)
     
      #copy vehicle files to ai dir
      car_files <- list.files(paste0(vehicles_dir, drivers$car[i], "/", drivers$folder[i]),
                              full.names = T) %>%
        tolower()
      file_idx <- c(grep("alt.json", car_files, fixed = T),
                    grep("alt.dds", car_files, fixed = T),
                    grep("alt_region.dds", car_files, fixed = T))
      car_files <- car_files[file_idx]
      flush.console()
      paste0("Copying ", drivers$first_name[i], " ", drivers$last_name[i], " files: ", paste(car_files, sep = ", ")) %>%
        print()
      file.copy(car_files, ai_dir)
     
      #create rcd file
      rcd <- paste(header,
                   paste0(drivers$first_name[i], " ", drivers$last_name[i]),
                   "{",
                   paste0("Team = ", drivers$team[i]),
                   paste0("Component = ", drivers$car[i]),
                   "Skin = alt.dds",
                   paste0("VehFile = ", drivers$vehicle_file[i]),
                   paste0("Description = ", paste0("#", drivers$number[i], " ", str_replace_all(drivers$car[i], "_", " "))),
                   paste0("Number = ", drivers$number[i]),
                   paste0("Classes = ", paste0("GT3 ", car_class)),
                   paste0("Category = ", str_replace_all(car_class, "_", " ")),
                   paste0("Aggression = ", drivers$aggression[i]),
                   paste0("Reputation = ", drivers$reputation[i]),
                   paste0("Courtesy = ", drivers$courtesy[i]),
                   paste0("Composure = ", drivers$composure[i]),
                   paste0("Speed = ", drivers$speed[i]),
                   paste0("QualifySpeed = ", drivers$qualify_speed[i]),
                   paste0("WetSpeed = ", drivers$wet_speed[i]),
                   paste0("StartSkill =", drivers$start_skill[i]),
                   paste0("Crash = ", drivers$crash[i]),
                   paste0("Recovery = ", drivers$recovery[i]),
                   paste0("CompletedLaps = ", drivers$completed_laps[i]),
                   paste0("MinRacingSkill = ", drivers$min_racing_skill[i]),
                   "}",
                   "}",
      sep = "\n")
      fname <- paste0(drivers$rcd_num[i], ".rcd")
      file_conn <- file(paste0(player_dir, drivers$car[i], "/", fname))
      writeLines(rcd, file_conn)
      close(file_conn)
    }
    
    
    Script for unpacking all the GT3 cars and creating an asset library:
    Code:
    require(stringdist)
    require(stringr)
    require(readtext)
    require(tidyverse)
    # Params ------------------------------------------------------------------
    
    vehicles_dir    <- "C:/Program Files (x86)/steam/steamapps/common/rFactor 2/Installed/Vehicles"
    output_dir      <- "C:/Users/[user_here]/Desktop/Vehicles_Test"
    mod_mgr_path    <- "C:/Program Files (x86)/Steam/steamapps/common/rFactor 2/Bin32/ModMgr.exe"
    car_category    <- "GT3_Season_Pack"
    vehicles_unpack <- c("AstonMartin_Vantage_GT3_2019",
                         "Audi_R8LMS_GT3_2019",
                         "Audi_R8LMS_GT3_2018",
                         "Bentley_Continental_GT3_2017",
                         "BMW_M6_GT3_2018",
                         "Callaway_Corvette_GT3_2017",
                         "McLaren_650S_GT3_2017",
                         "McLaren_720S_GT3_2018",
                         "Mercedes_AMG_GT3_2017",
                         "Porsche_Cup_GT3_2018",
                         "Radical_RXC_GT3_2017")
    mas_file_to_unpack <- "car-upgrade.mas"
    
    
    # Functions ---------------------------------------------------------------
    
    get_models <- function(vehicles_dir){
      #gets models of installed cars
      setwd(vehicles_dir)
      models <- list.dirs(full.names = T, recursive = F)
      models <- str_split_fixed(models, "/", n = 2)[,2]
      return(models)
    }
    
    get_latest_version <- function(vehicle_dir){
      versions_dir <- list.dirs(path = vehicle_dir, full.names = T, recursive = F)
      versions_dir <- gsub(pattern = vehicle_dir, x = versions_dir, replacement = "", fixed = T)
      if(length(versions_dir) > 1){
        versions_dir <- str_split_fixed(versions_dir, pattern = "/", n = 2)[,2] %>%
          as.numeric() %>%
          max()
      }
      return(versions_dir)
    }
    
    unpack_mas <- function(vehicle, vehicles_dir, temp_dir, mod_mgr_path, mas = "car-upgrade.mas"){
      unpack_file_types <- "*.veh *.dds *.json"
      latest_version <- get_latest_version(paste(vehicles_dir, vehicle, sep = "/"))
      extract_path <- paste(vehicles_dir, vehicle, latest_version, mas, sep = "/")
      system2(command = mod_mgr_path,
              args = paste(unpack_file_types,
                           paste0('-x"', extract_path, '"'),
                           paste0('-o"', temp_dir, '"'),
                           sep = " "))
    }
    
    extract_veh_info <- function(veh_contents, tag){
      veh_contents <- veh_contents[grep(tag, veh_contents, fixed = F)]
      if(length(veh_contents) > 0){
        veh_contents <- str_extract(veh_contents, '(?<=").*?(?=")')
      } else {
        veh_contents <- NA
      }
      return(veh_contents[1])
    }
    
    # Get Vehicles ------------------------------------------------------------
    
    #if temp dir doesn't exist create it
    temp_dir <- paste0(output_dir, "/temp")
    if(!dir.exists(temp_dir)){
      dir.create(temp_dir, recursive = T)
    }
    
    vehicle_inventory <- list()
    i <- vehicles_unpack[1] #for testing
    for(i in vehicles_unpack){
     
      #unpack vehicle
      unpack_mas(vehicle      = i,
                 vehicles_dir = vehicles_dir,
                 temp_dir     = temp_dir,
                 mod_mgr_path = mod_mgr_path,
                 mas          = mas_file_to_unpack)
     
      #get all vehicle files and catalog all extracted files
      all_files <- list.files(path = temp_dir, full.names = F) %>%
        tolower()
      veh_files <- all_files[grep('.veh', all_files, fixed = T)]
     
      #loop over vehicle files extracting info
      df <- data.frame(veh_file        = veh_files,
                       vehicle         = i,
                       dds_file        = NA,
                       dds_region_file = NA,
                       json_file       = NA,
                       team            = NA,
                       full_team_name  = NA,
                       veh_folder      = NA,
                       stringsAsFactors = F)
     
      for(x in 1:nrow(df)){
        #failures are the end of the world
        try({
          veh_contents <- readLines(paste0(temp_dir, "/", df$veh_file[x]), warn = F)
          df$dds_file[x]        <- extract_veh_info(veh_contents, 'DefaultLivery=.*dds\"')
          df$team[x]            <- extract_veh_info(veh_contents, 'Team=.*\"')
          df$full_team_name[x]  <- extract_veh_info(veh_contents, 'FullTeamName=.*\"')
          df$dds_region_file[x] <- if(!is.na(df$dds_file[x])) paste0(strsplit(df$dds_file[x], ".", fixed = T)[[1]][1], "_region.dds")
          df$json_file[x]       <- if(!is.na(df$dds_file[x])) paste0(strsplit(df$dds_file[x], ".", fixed = T)[[1]][1], ".json")
          df$veh_folder[x]      <- strsplit(df$veh_file[x], ".", fixed = T)[[1]][1]
        })
      }
     
      # start copy
      # if the directory exists clean it out
      if(dir.exists(paste(output_dir, i, sep = "/"))){
        unlink(paste(output_dir, i, sep = "/"), recursive = T)
      }
     
      #recreate the directory
      dir.create(paste(output_dir, i, sep = "/"))
     
      for(x in 1:nrow(df)){
        dir.create(paste(output_dir, i, df$veh_folder[x], sep = "/"))
        file.copy(paste0(temp_dir, "/", df$veh_file[x]),
                  paste(output_dir, i, df$veh_folder[x], df$veh_file[x], sep = "/"))
        file.copy(paste0(temp_dir, "/", df$dds_file[x]),
                  paste(output_dir, i, df$veh_folder[x], "alt.dds", sep = "/"))
        file.copy(paste0(temp_dir, "/", df$dds_region_file[x]),
                  paste(output_dir, i, df$veh_folder[x], "alt_region.dds", sep = "/"))
        file.copy(paste0(temp_dir, "/", df$json_file[x]),
                  paste(output_dir, i, df$veh_folder[x], "alt.json", sep = "/"))
      }
     
      #clean up temp folder
      if(dir.exists(temp_dir)) unlink(temp_dir, recursive = T)
      dir.create(temp_dir, recursive = T)
     
      #add the vehicles to our inventory
      vehicle_inventory[[i]] <- df
    }
    
    if(dir.exists(temp_dir)) unlink(temp_dir, recursive = T)
    
    vehicle_inventory_tbl <- do.call(rbind, vehicle_inventory)
    write_csv(vehicle_inventory_tbl, paste0(output_dir, "/vehicle_inventory.csv"))
    
     
    Last edited: Dec 31, 2019
  3. Marcel Offermans

    Marcel Offermans Administrator Staff Member

    Joined:
    Oct 4, 2010
    Messages:
    579
    Likes Received:
    2,550
    A very nice and detailed description. Thanks for sharing this!
     
    Gilles Benoit likes this.
  4. vernwozza

    vernwozza Registered

    Joined:
    Feb 9, 2017
    Messages:
    10
    Likes Received:
    3
    Awesome. I'm not the only one who plays like this then. Great stuff!
     
  5. Seven Smiles

    Seven Smiles Registered

    Joined:
    Oct 5, 2010
    Messages:
    721
    Likes Received:
    766
    <Copies it for next presentation to the team> :D
     
  6. prescient

    prescient Registered

    Joined:
    Dec 20, 2019
    Messages:
    9
    Likes Received:
    12
    A quick question to see if I can automate more of this process.

    1. The veh file contains the DDS file name, but does not have the xxxx_region.dds or xxxx.json file name.s Is there a file that maps these files together or is there a rule regarding the names?

    What I mean in terms of a rule is if the dds is named for example "xxxx.dds" will region always be appended as "xxxx_Region.dds" and the JSON always be "xxxx.JSON"?

    Edit: it turns out for the GT3 cars that you can just grab the "xxxx.dds" out of the vehicle file and the region file will always be "xxxx_region.dds" and json file will always be "xxxx.json".
     
    Last edited: Dec 31, 2019
  7. AustinIsCoolBeans

    AustinIsCoolBeans Registered

    Joined:
    Jan 20, 2019
    Messages:
    2
    Likes Received:
    0
    THANK YOU, you've just clearly explained what I've been trying to figure out for the past few months:D
     

Share This Page