Skip to main content

Python library for Nintendo's BCSV/JMap format

Project description

pyjmap

pyjmap is a high-level implementation of Nintendo's homemade BCSV/JMap data format. This includes methods to construct, analyze, manipulate, deserialize and serialize proper JMap data. Conversion between CSV and BCSV files is also supported. The reverse-engineered specifications of the file format can be accessed on the Luma's Workshop wiki. This flatbuffer-like data type was used in first-party GameCube and Wii games. As the field/column names are hashed, a lookup table needs to be used to retrieve proper field names. For this, the library provides hashtable implementations for Super Mario Galaxy, Super Mario Galaxy 2, Luigi's Mansion and Donkey Kong Jungle Beat.

Setup

This library requires Python 3.6 or newer. You can use pip to install pyjmap:

pip install pyjmap

Command usage

Command line operations to convert between JMap and CSV files are supported. The CSV files are required to be in a special format that has been found in some leftover source files from Super Mario Galaxy 2. That format is described down below.

You can dump the contents of a BCSV/JMap file to a CSV file using:

pyjmap tocsv [-le] [-jmapenc JMAP_ENCODING] [-csvenc CSV_ENCODING] {smg,dkjb,lm} JMAP_FILE_PATH CSV_FILE_PATH

Proper CSV files can be converted back to BCSV/JMap files using:

pyjmap tojmap [-le] [-jmapenc JMAP_ENCODING] [-csvenc CSV_ENCODING] {smg,dkjb,lm} CSV_FILE_PATH JMAP_FILE_PATH

If le is set, the data is expected to be stored using little-endian byte order. jmapenc specifies the encoding of strings in the JMap data and it defaults to shift_jisx0213. csvenc is the encoding of the CSV file and it uses utf-8 by default. The hash lookup table is specified by HASHTABLE. Supported values are smg for Super Mario Galaxy, lm for Luigi's Mansion, sms for Super Mario Sunshine and dkjb for Donkey Kong Jungle Beat.

Library usage

The library provides various high-level operations to deal with JMap data. Below is some example code showing the fundamentals of pyjmap. Look at jmap.py for more information about the different methods.

import pyjmap

# A hash lookup table is required to retrieve the proper names for hashed fields:
hashtbl_smg = pyjmap.SuperMarioGalaxyHashTable()    # Lookup table for Super Mario Galaxy 1/2
hashtbl_sms = pyjmap.SuperMarioSunshineHashTable()  # Lookup table for Super Mario Sunshine
hashtbl_lm = pyjmap.LuigisMansionHashTable()        # Lookup table for Luigi's Mansion
hashtbl_dkjb = pyjmap.JungleBeatHashTable()         # Lookup table for Donkey Kong Jungle Beat

# Create JMapInfo data from files and print number of entries
info = pyjmap.from_file(hashtbl_smg, "GalaxySortIndexTable.bcsv", big_endian=True)  # Big-endian is True by default
info_from_csv = pyjmap.from_csv(hashtbl_smg, "GalaxySortIndexTable.csv")            # Load data from CSV file
print("Number of entries: %d" % len(info))                                          # >> Number of entries: 55

# Print fields
for field in info.fields:
    print(field)  # >> name
                  # >> MapPaneName
                  # >> OpenCondition0
                  # >> OpenCondition1
                  # >> OpenCondition2
                  # >> PowerStarNum
                  # >> GrandGalaxyNo

# Checking if a field exists
print("MapPaneName" in info)  # >> True
print("StageName" in info)    # >> False

# Getting information about a field
field = info.get_field("MapPaneName")  # Get field by name
field = info.get_field(0x7991F36F)     # Get field by hash

print("[%08X]" % field.hash)  # >> [7991F36F]
print(field.name)             # >> MapPaneName
print(field.type)             # >> JMapFieldType.STRING_OFFSET
print("0x%08X" % field.mask)  # >> 0xFFFFFFFF
print(field.shift)            # >> 0

# Manually-specified offsets and bit-packed data
collision_pa = pyjmap.JMapInfo(hashtbl_smg)
collision_pa.manual_offsets = True
collision_pa.create_field("camera_id", pyjmap.JMapFieldType.LONG, 0, mask=0x000000FF, shift_amount=0, offset=0)
collision_pa.create_field("Sound_code", pyjmap.JMapFieldType.LONG, 0, mask=0x00007F00, shift_amount=8, offset=0)
collision_pa.create_field("Floor_code", pyjmap.JMapFieldType.LONG, 0, mask=0x001F8000, shift_amount=15, offset=0)
collision_pa.create_field("Wall_code", pyjmap.JMapFieldType.LONG, 0, mask=0x01E00000, shift_amount=21, offset=0)
collision_pa.create_field("Camera_through", pyjmap.JMapFieldType.LONG, 0, mask=0x02000000, shift_amount=25, offset=0)

# Creating an exact copy of the data
copied = info.copy()

# The following creates a new field called CometMedalNum which uses the LONG data type. The field's default value
# that is applied to all fields is -1. The optional bitmask and shift amount are 0xFFFFFFFF and 0, respectively.
copied.create_field("CometMedalNum", pyjmap.JMapFieldType.LONG, -1, mask=0xFFFFFFFF, shift_amount=0)

# This removes the field OpenCondition2 and its data in all entries.
copied.drop_field("OpenCondition2")

# Accessing entries directly
first = copied[0] # Get first entry from copied data
last = copied[-1] # Get last entry from copied data

# Adding and deleting entries
new_entry = copied.create_entry()  # Creates a new entry with default data for all fields
del copied[-3:]                    # Delete the last three entries from the copied data

# Iterate over all entries and set GrandGalaxyNo to 0
for entry in copied:
    entry["GrandGalaxyNo"] = 0

# Sort entries by name in lexicographic descending order
info.sort_entries(lambda e: e["name"].lower(), reverse=True)

# Get all entries whose name start with "Koopa"
for entry in filter(lambda e: e["name"].startswith("Koopa"), info):
    print(entry)  # >> {'name': 'KoopaJrShipLv1Galaxy', ... }
                  # >> {'name': 'KoopaBattleVs3Galaxy', ... }
                  # >> {'name': 'KoopaBattleVs2Galaxy', ... }
                  # >> {'name': 'KoopaBattleVs1Galaxy', ... }

# Write data to files
pyjmap.write_file(info, "GalaxySortIndexTable_edited.bcsv", big_endian=True)  # Pack and write binary
pyjmap.dump_csv(copied, "GalaxySortIndexTable_copied.csv", encoding="utf-8")  # Dump CSV content

# Pack as little-endian buffer
packed_copied = pyjmap.pack_buffer(copied, big_endian=False)

Data types

The following field data types are supported:

Identifier CSV type Description
JMapFieldType.LONG Int 32-bit integer
JMapFieldType.UNSIGNED_LONG UnsignedInt 32-bit unsigned integer
JMapFieldType.SHORT Short 16-bit integer
JMapFieldType.CHAR Char 8-bit integer
JMapFieldType.FLOAT Float single-precission float
JMapFieldType.STRING EmbeddedString embedded SJIS string (occupies 31 bytes at max)
JMapFieldType.STRING_OFFSET String SJIS string (not supported in Luigi's Mansion)

CSV format

The CSV format is based on the format of known source files that were left in the files of Super Mario Galaxy 2:

  • All CSV files are comma-delimited and use quote-marks for quoted cell strings. Quoting is only used when necessary.
  • The first CSV-row contains the field descriptors. A field descriptor always consists of three components that are separated by double-colons: the field's name, the data type and the default value. All existing CSV types are described in the previous section and are case sensitive!
  • The default value for strings is 0 and is always ignored. It is only kept for syntax.
  • If an entry's field data is empty, the default value will be used.
  • The field name may be a hash if it's a hex-string encapsulated between two square brackets (for example [DEADBEEF])

Here is an example of a properly-formated CSV file:

name:String:0,MapPaneName:String:0,OpenCondition0:String:0,OpenCondition1:String:0,OpenCondition2:String:0,PowerStarNum:Char:0,GrandGalaxyNo:Char:0
AstroGalaxy,dummy,,,,0,0
AstroDome,dummy,,,,0,0
LibraryRoom,dummy,,,,0,0
PeachCastleGardenGalaxy,dummy,,,,0,0
EpilogueDemoStage,dummy,,,,0,0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyjmap-1.0.9.tar.gz (37.6 kB view details)

Uploaded Source

File details

Details for the file pyjmap-1.0.9.tar.gz.

File metadata

  • Download URL: pyjmap-1.0.9.tar.gz
  • Upload date:
  • Size: 37.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.10.7

File hashes

Hashes for pyjmap-1.0.9.tar.gz
Algorithm Hash digest
SHA256 a24ead8a993ac3156ace3dd8fb3e5a797605c375abd346924df387e981365af1
MD5 b7bc9d26c66be35bc56523d1d996c2fb
BLAKE2b-256 4b1980ce6ccd47347eed8973128fb3802a14ee8f14407900825aef9fbe5b779c

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page