Scripting and Tooling

Scripting and Tooling

Scripting and Tooling

Optimizing Workflow with Tools

Optimizing Workflow with Tools

Optimizing Workflow with Tools

The Brief

The League Audio Implementation Checker, designed for the Riot Audio team, significantly simplifies the sound design review process. Built using web technologies such as Electron, Node.js, React and TailwindCSS. This essential tool was created to ensure adherence to a detailed set of checklist requirements, a crucial but time-consuming task that often took over three hours for each check. This step, necessary for finalizing each skin/champion product, involves meticulous verification of aspects like naming conventions, HDR, in-game positioning, conversion settings, engine hooks, bussing, and the elimination of unused files. The tool adeptly scans for discrepancies inside Wwise and the game engine, then suggests appropriate fixes, offering users the flexibility to accept, modify, or skip. Once the user select apply fixes, the tool efficiently executes the necessary adjustments and check-out modified files in Perforce.

Workflow Scripts

import json
import os
import tkinter as tk
from tkinter import filedialog
import subprocess
import fnmatch
import time

def convert_to_asset_path(filepath):
    # Get the index of the 'ASSETS' directory
    assets_index = filepath.index('ASSETS')
    # Get the substring starting from the 'ASSETS' directory
    new_filepath = filepath[assets_index:]
    # Replace backslashes with forward slashes
    new_filepath = new_filepath.replace('\\', '/')
    # Return the modified filepath
    return new_filepath

def find_ogg_files(directory):
    """Recursively find all .ogg files in the given directory."""
    ogg_files = []
    for root, dirs, files in os.walk(directory):
        for filename in fnmatch.filter(files, '*.ogg'):
            ogg_files.append(os.path.join(root, filename))
            
    
    return ogg_files


def get_filename(filepath):
    # Split the filepath into directory names and the filename
    _, filename = os.path.split(filepath)
    # Return just the filename
    return filename

def main():
    # get the contents of the text boxes
    GENERIC_ASSET_LIST_FILE = json_path_textbox.get("1.0", "end-1c")
    ASSET_DIRECTORY = asset_textbox.get("1.0", "end-1c")
    
    settings = {
        "GENERIC_ASSET_LIST_PATH": GENERIC_ASSET_LIST_FILE,
        "ASSET_PATH": ASSET_DIRECTORY
    }
    
    command =  "p4 edit " + GENERIC_ASSET_LIST_FILE
    subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    
    with open("settings.json", 'w') as f:
         json.dump(settings, f, indent=4)
         

    if not os.path.exists(GENERIC_ASSET_LIST_FILE):
        button.configure(text="Failed, try again", bg='#f04747', fg='#ffffff')
        return
    
    if not os.path.exists(ASSET_DIRECTORY):
        button.configure(text="Failed, try again", bg='#f04747', fg='#ffffff')
        return   
    
    ogg_files = find_ogg_files(ASSET_DIRECTORY)
    asset_id = [get_filename(ogg_files) for ogg_files in ogg_files]
    asset_file_path = [convert_to_asset_path(fp) for fp in ogg_files]
    assets = list(zip(asset_id, asset_file_path))


    # open the JSON file and load its contents into a variable
    with open(GENERIC_ASSET_LIST_FILE, 'r') as f:
        data = json.load(f)

    new_data = []
    for i in range(len(assets)):
        new_data.append({
            "GenericAssetListItem": {
                "mId": assets[i][0],
                "mPath": assets[i][1]
            }
        })

    # TODO fix this, as it needs to thceck if path is "mId" or "mAssets"
    for section in data["streamDeltas"]:
        if section["path"] == "mAssets":
            section["current"] = new_data
            break
    
    #data["streamDeltas"][0]["current"] = new_data
    os.remove(GENERIC_ASSET_LIST_FILE)
    # write the modified object back to the file
    with open(GENERIC_ASSET_LIST_FILE, 'w') as f:
        json.dump(data, f, indent=4)
        
    button.configure(text="Done!", bg='#43b581', fg='#ffffff', state=tk.DISABLED)
    root.after(2000, lambda: button.configure(text="Update", bg='#7289da', fg='#ffffff', state=tk.NORMAL))
    
    

def browse_file(text_box, title, default_dir=".", select_dir=False):
    if select_dir == True:
        filename = filedialog.askdirectory(initialdir=default_dir, title=title)
    else:
        filename = filedialog.askopenfilename(initialdir=default_dir, title=title, filetypes=(("JSON files", "*.json"),))

    if filename == "":
        return
    
    text_box.delete("1.0", tk.END) # TODO make it not delete when you cancle or the file
    text_box.insert("1.0", filename)

if __name__ == '__main__':
    if os.path.exists("settings.json"):
        with open("settings.json", 'r') as f:
            settings_json = json.load(f)
        
        if "GENERIC_ASSET_LIST_PATH" in settings_json:
            saved_json_path = settings_json["GENERIC_ASSET_LIST_PATH"]
            saved_asset_path = settings_json["ASSET_PATH"]
        else:
            settings_json = {}
            saved_json_path = ""
            saved_asset_path = ""
            saved_csv_path = ""
    else:
        settings_json = {}
        saved_json_path = ""
        saved_asset_path = ""
        saved_csv_path = ""
        
    
    # create the main window
    root = tk.Tk()

    # set the title of the window
    root.title("Asset List Updater")

    # set the background color to Discord's dark theme color
    root.configure(background='#2f3136')

    # create a label for the first text box
    json_label = tk.Label(root, text="GenericAssetList JSON file path", bg='#2f3136', fg='#ffffff', font=('Arial', 12, 'bold'))
    json_label.pack(side=tk.TOP, padx=10)

    # create a frame for the first text box and browse button
    json_frame = tk.Frame(root, bg='#2f3136')
    json_frame.pack(side=tk.TOP, padx=10, pady=5, fill=tk.X)

    # create the first text box with white text on a black background and white caret
    json_path_textbox = tk.Text(json_frame, height=2, width=45, bg='#36393f', fg='#ffffff', font=('Arial', 12), insertbackground="white")
    json_path_textbox.pack(side=tk.LEFT)
    json_path_textbox.insert("1.0", saved_json_path)

    # create a browse button for the first text box
    json_path_button = tk.Button(json_frame, text="Browse", bg='#7289da', fg='#ffffff',
                                borderwidth=0, relief='raised', highlightthickness=1,
                                highlightbackground='#7289da', height=2, width=10,
                                command=lambda: browse_file(json_path_textbox, "Select JSON file", default_dir="T:\\p4\\depot\\lol\\__MAIN__\\DevRoot\\PROPERTIES\\Data\\LCU\\EventAssets\\"))
    json_path_button.pack(side=tk.RIGHT, padx=5)
    
    # assets
    asset_label = tk.Label(root, text="Audio asset directory", bg='#2f3136', fg='#ffffff', font=('Arial', 12, 'bold'))
    asset_label.pack(side=tk.TOP, padx=10)
    
    asset_frame = tk.Frame(root, bg='#2f3136')
    asset_frame.pack(side=tk.TOP, padx=10, pady=5, fill=tk.X)

    asset_textbox = tk.Text(asset_frame, height=2, width=45, bg='#36393f', fg='#ffffff', font=('Arial', 12), insertbackground="white")
    asset_textbox.pack(side=tk.LEFT)
    asset_textbox.insert("1.0", saved_asset_path)

    # create a browse button for the second text box
    asset_browse_button = tk.Button(asset_frame, text="Browse", bg='#7289da', fg='#ffffff',
                                borderwidth=0, relief='raised', highlightthickness=1,
                                highlightbackground='#7289da', height=2, width=10,
                                command=lambda: browse_file(asset_textbox, "Select asset directory", 
                                                            default_dir="T:\\p4\\depot\\lol\\__MAIN__\\DevRoot\\ASSETS\\Events\\",
                                                            select_dir=True))
    asset_browse_button.pack(side=tk.LEFT, padx=5)


    # create a frame to hold the button
    button_frame = tk.Frame(root, bg='#2f3136')
    button_frame.pack(side=tk.BOTTOM, pady=10)

    # create the button with rounded corners and a blue background
    button = tk.Button(button_frame, text="Update", bg='#7289da', fg='#ffffff', 
                        borderwidth=0, relief='raised', highlightthickness=1,
                        highlightbackground='#7289da', height=2, width=20,
                        command=main)
    button.pack(side=tk.BOTTOM, pady=10)

    # set the window size to 500x250 pixels
    root.geometry("500x250")
    root.resizable(width=False, height=False)
    
    root.iconbitmap('myicon.ico')

    # run the main event loop
    root.mainloop()

The Process

Identify

Before developing a tool, identifying a key problem to solve is crucial. This often involves listening to team members' insights on the most challenging aspects of their workflow and inquiring about their ideal solution. My approach focuses on pinpointing issues that, while relatively straightforward to address, can significantly save time and effort for the entire team.

Development

Selecting the right tech stack for tool development largely depends on the project's scope and whether it's a team or solo effort. For minor workflow enhancements, I opt for Python, as it's compatible with most of our in-house tooling APIs and is pre-installed on all development workstations. For more complex projects that might be handed over to a team, I lean towards JavaScript with web technology for the UI, considering the widespread familiarity of our engineering team with these tools.