Task 260: .GM9 File Format

Task 260: .GM9 File Format

1. List of All Properties of the .GM9 File Format Intrinsic to Its File System

The .GM9 file format is a plain-text script format used by GodMode9 (a file browser for the Nintendo 3DS console). It supports interaction with the 3DS file system (SD card, SysNAND, EmuNAND partitions) through commands and variables. The intrinsic properties related to the file system are runtime variables automatically set by GodMode9 before or during script execution. These describe key file system aspects like paths, sizes, and sectors. They cannot be directly modified by scripts in most cases and are referenced using $[VAR] syntax.

Here is the complete list:

  • CURRDIR: Path to the directory containing the script file (no trailing forward slash). Example: 0:/gm9/scripts.
  • GM9OUT: GodMode9's output directory path for dumps and logs. Example: 0:/gm9/out.
  • EMUBASE: Base sector number of the current EmuNAND partition (integer value, or empty if no EmuNAND).
  • SDSIZE: Total size of the SD card, formatted with units (bytes if <1 KiB, KiB/MiB/GiB otherwise, rounded down to nearest tenth). Example: 1273.0 GB.
  • SDFREE: Free space on the SD card, formatted like SDSIZE (never exceeds SDSIZE). Example: 500.2 GB.
  • NANDSIZE: Total size of SysNAND (including all partitions and bonus drive), formatted like SDSIZE. Known values: 943.0 MB or 954.0 MB (O3DS), 1.2 GB or 1.8 GB (N3DS/O2DS).

These properties are file-system-specific and enable scripts to perform operations like copying to output paths or checking storage availability.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .GM9 Property Dump

Drag and drop a .GM9 file here to dump its file system properties.

This HTML/JS snippet can be embedded in a Ghost blog post. It creates a drag-and-drop zone that parses the .GM9 text file for set <prop> <value> lines matching the properties list and dumps them to the screen (with descriptions if not explicitly set in the script).

4. Python Class for .GM9 Handling

import os

class GM9Decoder:
    def __init__(self, filename):
        self.filename = filename
        self.properties = {
            'CURRDIR': 'Path to the directory containing the script file (no trailing forward slash).',
            'GM9OUT': "GodMode9's output directory path for dumps and logs.",
            'EMUBASE': 'Base sector number of the current EmuNAND partition (integer value, or empty if no EmuNAND).',
            'SDSIZE': 'Total size of the SD card, formatted with units (bytes if <1 KiB, KiB/MiB/GiB otherwise, rounded down to nearest tenth).',
            'SDFREE': 'Free space on the SD card, formatted like SDSIZE (never exceeds SDSIZE).',
            'NANDSIZE': 'Total size of SysNAND (including all partitions and bonus drive), formatted like SDSIZE.'
        }
        self.found_values = {}

    def read(self):
        if not os.path.exists(self.filename):
            print(f"Error: {self.filename} not found.")
            return
        with open(self.filename, 'r', encoding='utf-8') as f:
            lines = f.readlines()
        for line in lines:
            trimmed = line.strip()
            if trimmed.startswith('set '):
                parts = trimmed.split(maxsplit=2)
                if len(parts) >= 3 and parts[1] in self.properties:
                    self.found_values[parts[1]] = parts[2]
        self.print_properties()

    def print_properties(self):
        print("File System Properties:")
        for name, desc in self.properties.items():
            value = self.found_values.get(name, 'Not set in script (runtime value)')
            print(f"{name}: {value}")
            print(f"Description: {desc}\n")

    def write(self, output_filename=None):
        if output_filename is None:
            output_filename = self.filename
        with open(output_filename, 'r', encoding='utf-8') as f:
            content = f.read()
        lines = content.split('\n')
        new_lines = []
        modified = False
        for line in lines:
            trimmed = line.strip()
            if trimmed.startswith('set '):
                parts = trimmed.split(maxsplit=2)
                if len(parts) >= 3 and parts[1] in self.properties:
                    # Example: Set a default if not present; in real use, pass values to set
                    if parts[1] not in self.found_values:
                        self.found_values[parts[1]] = 'default_value'  # Placeholder; customize
                    new_line = f"set {parts[1]} {self.found_values[parts[1]]}"
                    new_lines.append(new_line)
                    modified = True
                    continue
            new_lines.append(line)
        if not modified:
            # Append example sets if none found
            for name in self.properties:
                new_lines.append(f"set {name} default_value  # Example")
        with open(output_filename, 'w', encoding='utf-8') as f:
            f.write('\n'.join(new_lines) + '\n')
        print(f"Written to {output_filename}")

# Usage
if __name__ == "__main__":
    decoder = GM9Decoder("example.gm9")
    decoder.read()
    decoder.write()

This class reads the .GM9 file, parses for set commands matching properties, prints them (with descriptions), and writes modifications (e.g., adds/updates sets).

5. Java Class for .GM9 Handling

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class GM9Decoder {
    private String filename;
    private Map<String, String> properties = new HashMap<>();
    private Map<String, String> foundValues = new HashMap<>();

    public GM9Decoder(String filename) {
        this.filename = filename;
        properties.put("CURRDIR", "Path to the directory containing the script file (no trailing forward slash).");
        properties.put("GM9OUT", "GodMode9's output directory path for dumps and logs.");
        properties.put("EMUBASE", "Base sector number of the current EmuNAND partition (integer value, or empty if no EmuNAND).");
        properties.put("SDSIZE", "Total size of the SD card, formatted with units (bytes if <1 KiB, KiB/MiB/GiB otherwise, rounded down to nearest tenth).");
        properties.put("SDFREE", "Free space on the SD card, formatted like SDSIZE (never exceeds SDSIZE).");
        properties.put("NANDSIZE", "Total size of SysNAND (including all partitions and bonus drive), formatted like SDSIZE.");
    }

    public void read() {
        try {
            List<String> lines = Files.readAllLines(Paths.get(filename));
            for (String line : lines) {
                String trimmed = line.trim();
                if (trimmed.startsWith("set ")) {
                    String[] parts = trimmed.split("\\s+", 3);
                    if (parts.length >= 3 && properties.containsKey(parts[1])) {
                        foundValues.put(parts[1], parts[2]);
                    }
                }
            }
            printProperties();
        } catch (IOException e) {
            System.out.println("Error: " + filename + " not found.");
        }
    }

    private void printProperties() {
        System.out.println("File System Properties:");
        for (Map.Entry<String, String> entry : properties.entrySet()) {
            String name = entry.getKey();
            String value = foundValues.getOrDefault(name, "Not set in script (runtime value)");
            System.out.println(name + ": " + value);
            System.out.println("Description: " + entry.getValue() + "\n");
        }
    }

    public void write(String outputFilename) {
        if (outputFilename == null) outputFilename = filename;
        try {
            List<String> lines = Files.readAllLines(Paths.get(filename));
            List<String> newLines = new ArrayList<>();
            boolean modified = false;
            for (String line : lines) {
                String trimmed = line.trim();
                if (trimmed.startsWith("set ")) {
                    String[] parts = trimmed.split("\\s+", 3);
                    if (parts.length >= 3 && properties.containsKey(parts[1])) {
                        // Placeholder update
                        if (!foundValues.containsKey(parts[1])) {
                            foundValues.put(parts[1], "default_value");
                        }
                        newLines.add("set " + parts[1] + " " + foundValues.get(parts[1]));
                        modified = true;
                        continue;
                    }
                }
                newLines.add(line);
            }
            if (!modified) {
                for (String name : properties.keySet()) {
                    newLines.add("set " + name + " default_value  # Example");
                }
            }
            Files.write(Paths.get(outputFilename), newLines);
            System.out.println("Written to " + outputFilename);
        } catch (IOException e) {
            System.out.println("Write error: " + e.getMessage());
        }
    }

    // Usage example in main
    public static void main(String[] args) {
        GM9Decoder decoder = new GM9Decoder("example.gm9");
        decoder.read();
        decoder.write(null);
    }
}

This Java class reads the .GM9 file, parses set commands for properties, prints them, and writes updates (compiles with Java 8+; uses java.util.*).

6. JavaScript Class for .GM9 Handling

class GM9Decoder {
  constructor(filename) {
    this.filename = filename;
    this.properties = {
      CURRDIR: 'Path to the directory containing the script file (no trailing forward slash).',
      GM9OUT: "GodMode9's output directory path for dumps and logs.",
      EMUBASE: 'Base sector number of the current EmuNAND partition (integer value, or empty if no EmuNAND).',
      SDSIZE: 'Total size of the SD card, formatted with units (bytes if <1 KiB, KiB/MiB/GiB otherwise, rounded down to nearest tenth).',
      SDFREE: 'Free space on the SD card, formatted like SDSIZE (never exceeds SDSIZE).',
      NANDSIZE: 'Total size of SysNAND (including all partitions and bonus drive), formatted like SDSIZE.'
    };
    this.foundValues = {};
  }

  async read() {
    try {
      const response = await fetch(this.filename);
      const content = await response.text();
      const lines = content.split('\n');
      lines.forEach(line => {
        const trimmed = line.trim();
        if (trimmed.startsWith('set ')) {
          const parts = trimmed.split(/\s+/);
          if (parts.length >= 3 && this.properties.hasOwnProperty(parts[1])) {
            this.foundValues[parts[1]] = parts.slice(2).join(' ');
          }
        }
      });
      this.printProperties();
    } catch (e) {
      console.error(`Error: ${this.filename} not found.`);
    }
  }

  printProperties() {
    console.log('File System Properties:');
    Object.entries(this.properties).forEach(([name, desc]) => {
      const value = this.foundValues[name] || 'Not set in script (runtime value)';
      console.log(`${name}: ${value}`);
      console.log(`Description: ${desc}\n`);
    });
  }

  write(outputFilename = this.filename) {
    // For Node.js; browser would use File System Access API
    const fs = require('fs');
    fs.readFile(this.filename, 'utf8', (err, content) => {
      if (err) {
        console.error('Read error:', err);
        return;
      }
      const lines = content.split('\n');
      const newLines = [];
      let modified = false;
      lines.forEach(line => {
        const trimmed = line.trim();
        if (trimmed.startsWith('set ')) {
          const parts = trimmed.split(/\s+/);
          if (parts.length >= 3 && this.properties.hasOwnProperty(parts[1])) {
            if (!this.foundValues[parts[1]]) {
              this.foundValues[parts[1]] = 'default_value';
            }
            newLines.push(`set ${parts[1]} ${this.foundValues[parts[1]]}`);
            modified = true;
            return;
          }
        }
        newLines.push(line);
      });
      if (!modified) {
        Object.keys(this.properties).forEach(name => {
          newLines.push(`set ${name} default_value  # Example`);
        });
      }
      fs.writeFile(outputFilename, newLines.join('\n') + '\n', (err) => {
        if (err) console.error('Write error:', err);
        else console.log(`Written to ${outputFilename}`);
      });
    });
  }
}

// Usage (Node.js)
const decoder = new GM9Decoder('example.gm9');
decoder.read();
decoder.write();

This Node.js-compatible class reads/parses the .GM9, prints properties, and writes updates. For browser, adapt read() to use FileReader.

7. C Class (Struct) for .GM9 Handling

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char *filename;
    char *properties[6][2];  // [name][desc]
    char *found_values[6];
} GM9Decoder;

void init_decoder(GM9Decoder *decoder, const char *filename) {
    decoder->filename = strdup(filename);
    // Initialize properties
    strcpy(decoder->properties[0][0], "CURRDIR");
    strcpy(decoder->properties[0][1], "Path to the directory containing the script file (no trailing forward slash).");
    strcpy(decoder->properties[1][0], "GM9OUT");
    strcpy(decoder->properties[1][1], "GodMode9's output directory path for dumps and logs.");
    strcpy(decoder->properties[2][0], "EMUBASE");
    strcpy(decoder->properties[2][1], "Base sector number of the current EmuNAND partition (integer value, or empty if no EmuNAND).");
    strcpy(decoder->properties[3][0], "SDSIZE");
    strcpy(decoder->properties[3][1], "Total size of the SD card, formatted with units (bytes if <1 KiB, KiB/MiB/GiB otherwise, rounded down to nearest tenth).");
    strcpy(decoder->properties[4][0], "SDFREE");
    strcpy(decoder->properties[4][1], "Free space on the SD card, formatted like SDSIZE (never exceeds SDSIZE).");
    strcpy(decoder->properties[5][0], "NANDSIZE");
    strcpy(decoder->properties[5][1], "Total size of SysNAND (including all partitions and bonus drive), formatted like SDSIZE.");
    for (int i = 0; i < 6; i++) {
        decoder->found_values[i] = NULL;
    }
}

void read_gm9(GM9Decoder *decoder) {
    FILE *file = fopen(decoder->filename, "r");
    if (!file) {
        printf("Error: %s not found.\n", decoder->filename);
        return;
    }
    char line[1024];
    while (fgets(line, sizeof(line), file)) {
        char *trimmed = line;
        while (*trimmed == ' ' || *trimmed == '\t') trimmed++;
        if (strncmp(trimmed, "set ", 4) == 0) {
            char *second = strtok(trimmed + 4, " \t");
            if (second) {
                char *value = strtok(NULL, "\n");
                if (value) {
                    value += strspn(value, " \t");  // Skip spaces
                    for (int i = 0; i < 6; i++) {
                        if (strcmp(second, decoder->properties[i][0]) == 0) {
                            decoder->found_values[i] = strdup(value);
                            break;
                        }
                    }
                }
            }
        }
    }
    fclose(file);
    print_properties(decoder);
}

void print_properties(GM9Decoder *decoder) {
    printf("File System Properties:\n");
    for (int i = 0; i < 6; i++) {
        char *value = decoder->found_values[i] ? decoder->found_values[i] : "Not set in script (runtime value)";
        printf("%s: %s\n", decoder->properties[i][0], value);
        printf("Description: %s\n\n", decoder->properties[i][1]);
    }
}

void write_gm9(GM9Decoder *decoder, const char *output_filename) {
    char *out_name = strdup(output_filename ? output_filename : decoder->filename);
    FILE *in_file = fopen(decoder->filename, "r");
    FILE *out_file = fopen(out_name, "w");
    if (!in_file || !out_file) {
        printf("Write error.\n");
        free(out_name);
        if (in_file) fclose(in_file);
        return;
    }
    char line[1024];
    int modified = 0;
    while (fgets(line, sizeof(line), in_file)) {
        char *trimmed = line;
        while (*trimmed == ' ' || *trimmed == '\t') trimmed++;
        if (strncmp(trimmed, "set ", 4) == 0) {
            char *second = strtok(trimmed + 4, " \t\n");
            if (second) {
                for (int i = 0; i < 6; i++) {
                    if (strcmp(second, decoder->properties[i][0]) == 0) {
                        char new_line[1024];
                        snprintf(new_line, sizeof(new_line), "set %s %s\n", second, decoder->found_values[i] ? decoder->found_values[i] : "default_value");
                        fputs(new_line, out_file);
                        modified = 1;
                        break;
                    }
                }
                continue;
            }
        }
        fputs(line, out_file);
    }
    if (!modified) {
        for (int i = 0; i < 6; i++) {
            fprintf(out_file, "set %s default_value  # Example\n", decoder->properties[i][0]);
        }
    }
    fclose(in_file);
    fclose(out_file);
    printf("Written to %s\n", out_name);
    free(out_name);
}

void free_decoder(GM9Decoder *decoder) {
    free(decoder->filename);
    for (int i = 0; i < 6; i++) {
        if (decoder->found_values[i]) free(decoder->found_values[i]);
    }
}

// Usage
int main() {
    GM9Decoder decoder;
    init_decoder(&decoder, "example.gm9");
    read_gm9(&decoder);
    write_gm9(&decoder, NULL);
    free_decoder(&decoder);
    return 0;
}

This C implementation uses a struct for the decoder, reads/parses the .GM9 file for properties, prints them, and writes updates. Compile with gcc -o gm9dec gm9dec.c. Note: Descriptions are hardcoded as fixed strings for simplicity; dynamic allocation used for values.