Skip to content

Instantly share code, notes, and snippets.

@leovoel
Last active January 7, 2024 10:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leovoel/399911503ee8dbce7ff0c54593d3eae7 to your computer and use it in GitHub Desktop.
Save leovoel/399911503ee8dbce7ff0c54593d3eae7 to your computer and use it in GitHub Desktop.
# USAGE
# =====
# save this code into a file called rcru.bms.
#
# then, for saving sounds, execute QuickBMS like so:
#
# quickbms rcru.bms audio.rcru audio
#
# this will dump ogg files in the "audio" folder.
#
# for saving sprites, you can:
#
# - save all textures.
# see `startfunction SAVE_TEXTURE`
# - save all sprites (texture slices)
# see `startfunction SAVE_TEXTURE_SLICE`
#
# if you want exported files to be saved as png, you'll need ImageMagick
# (https://imagemagick.org/) installed and available on your PATH.
# this is only set up for windows, though.
#
# once you have all that set up, you can use it in the same way as shown above:
#
# quickbms rcru.bms textures.rcru textures
#
# ignore the comments below, they're details of the script.
# MEMORY FILE USAGE
# =================
# for texture archives:
# - MEMORY_FILE1: decompressed archive
# - MEMORY_FILE2: texture_bundle texture_bundles[BUNDLE_COUNT]
# - MEMORY_FILE3: uint32 texture_palette[PALETTE_COLOR_COUNT]
# - MEMORY_FILE4: uint32 decompressed_texture[TEXTURE_PIXEL_COUNT]
# - MEMORY_FILE5: uint32 decompressed_textures[{pixel count of all textures}]
# - MEMORY_FILE6: texture_info texture_array[TEXTURE_COUNT]
# - MEMORY_FILE7: uint32 texture_slice[SLICE_PIXEL_COUNT]
# - MEMORY_FILE8: c code for rle, slicing, etc.
#
# for audio archives:
# - MEMORY_FILE1: decompressed archive
#
# with every array intended to be filled using putvarchr, there's an
# accompanying *_POINTER variable, which is supposed to be incremented on every
# write with the size of what was just written. cumbersome, yes.
# INTERNAL STRUCTURES
# ===================
# texture_bundle
# uint32 data_offset
# uint32 name_offset
# uint32 name_length
# 12 bytes in total
#
# texture_info
# uint32 width
# uint32 height
# uint32 offset
# uint32 size
# 16 bytes in total
idstring 0 "RCRU "
get ARCHIVE_TYPE_NAME string 0
get ARCHIVE_TYPE_NUMBER byte 0
if ARCHIVE_TYPE_NAME == "Audio "
callfunction CHECK_ARCHIVE_TYPE_NUMBER 1 ARCHIVE_TYPE_NUMBER 5
get VORBIS_OFFSET long 0
savepos TEMPORARY_OFFSET 0
math VORBIS_OFFSET + TEMPORARY_OFFSET
callfunction DECOMPRESS_ARCHIVE 1
get SOUND_COUNT long MEMORY_FILE1
set SOUND_DATA_OFFSET long 0
for SOUND_INDEX = 0 < SOUND_COUNT
# get length of this sound's name
callfunction READ_7_BIT_ENCODED_INT 1
set SOUND_NAME_LENGTH long R7BEI_RET
# get sound name
getdstring ORIGINAL_SOUND_NAME SOUND_NAME_LENGTH MEMORY_FILE1
set SOUND_NAME_WITHOUT_EXTENSION string ORIGINAL_SOUND_NAME
string SOUND_NAME_WITHOUT_EXTENSION > ".wav"
string SOUND_NAME P "%SOUND_NAME_WITHOUT_EXTENSION%.ogg"
# get offset of the next sound
get NEXT_SOUND_DATA_OFFSET long MEMORY_FILE1
# calculate size of this sound
set SOUND_DATA_SIZE long NEXT_SOUND_DATA_OFFSET
math SOUND_DATA_SIZE - SOUND_DATA_OFFSET
math SOUND_DATA_OFFSET + VORBIS_OFFSET
# maybe someone wants to use this info, so here it is
goto SOUND_DATA_OFFSET 0 SEEK_SET
get SAMPLE_COUNT long 0
get LOOP_START_IN_SAMPLES long 0
get LOOP_LENGTH_IN_SAMPLES long 0
# skip the 3 longs above
math SOUND_DATA_OFFSET + 12
math SOUND_DATA_SIZE - 12
# save sound
log SOUND_NAME SOUND_DATA_OFFSET SOUND_DATA_SIZE 0
set SOUND_DATA_OFFSET long NEXT_SOUND_DATA_OFFSET
next SOUND_INDEX
elif ARCHIVE_TYPE_NAME == "Textures "
callfunction CHECK_ARCHIVE_TYPE_NUMBER 1 ARCHIVE_TYPE_NUMBER 1
callfunction DECOMPRESS_ARCHIVE 1
get BUNDLE_COUNT long MEMORY_FILE1
# build up `texture_bundle texture_bundles[BUNDLE_COUNT]`
set TEXTURE_BUNDLES_SIZE long 12
math TEXTURE_BUNDLES_SIZE * BUNDLE_COUNT
set TEXTURE_BUNDLES_POINTER long 0
# allocate and reset position
putvarchr MEMORY_FILE2 TEXTURE_BUNDLES_SIZE 0 byte
log MEMORY_FILE2 0 0
set BUNDLE_NAMES_POINTER long BUNDLE_COUNT
math BUNDLE_NAMES_POINTER + 1 # include BUNDLE_COUNT itself
math BUNDLE_NAMES_POINTER * 4
for BUNDLE_INDEX = 0 < BUNDLE_COUNT
# the first offset is implicit
if BUNDLE_INDEX == 0
set BUNDLE_DATA_OFFSET long 0
else
get BUNDLE_DATA_OFFSET long MEMORY_FILE1
endif
# mark where in the data offset list we are
savepos TEMPORARY_OFFSET MEMORY_FILE1
# jump to where the current name is and get its length
goto BUNDLE_NAMES_POINTER MEMORY_FILE1 SEEK_SET
callfunction READ_7_BIT_ENCODED_INT 1
set BUNDLE_NAME_LENGTH long R7BEI_RET
# mark where the name starts, right after its length
savepos BUNDLE_NAME_OFFSET MEMORY_FILE1
# advance to the next name
set BUNDLE_NAMES_POINTER long BUNDLE_NAME_OFFSET
math BUNDLE_NAMES_POINTER + BUNDLE_NAME_LENGTH
# jump back to the data offset list
goto TEMPORARY_OFFSET MEMORY_FILE1 SEEK_SET
# write struct fields
putvarchr MEMORY_FILE2 TEXTURE_BUNDLES_POINTER BUNDLE_DATA_OFFSET long
math TEXTURE_BUNDLES_POINTER + 4
putvarchr MEMORY_FILE2 TEXTURE_BUNDLES_POINTER BUNDLE_NAME_OFFSET long
math TEXTURE_BUNDLES_POINTER + 4
putvarchr MEMORY_FILE2 TEXTURE_BUNDLES_POINTER BUNDLE_NAME_LENGTH long
math TEXTURE_BUNDLES_POINTER + 4
next BUNDLE_INDEX
set MEMORY_FILE8 string "
/* flimsy. */
unsigned long rcru_rle(
unsigned char *input,
unsigned char *output,
unsigned long texture_pixel_count,
unsigned char *palette_input
) {
unsigned long *output_array = (unsigned long *)(output);
unsigned long *end = output_array + texture_pixel_count;
unsigned long *palette_array = (unsigned long *)(palette_input);
unsigned char *input_start = input;
unsigned long color = 0;
while (output_array < end) {
unsigned char input_byte = *input++;
if ((input_byte & 0x80) == 0) {
if ((input_byte & 0x7E) != 0x7E) {
color = *(palette_array + input_byte);
} else {
color = (unsigned long)(*input++);
color |= (*input++) << 8ul;
color |= (*input++) << 16ul;
if ((input_byte & 1) == 0) {
color |= 0xFF << 24ul;
} else {
color |= (*input++) << 24ul;
}
}
*output_array++ = color;
} else {
unsigned long repeat_count = input_byte & 0x3F;
if ((input_byte & 0x40) != 0) {
repeat_count <<= 8ul;
repeat_count |= *input++;
}
repeat_count += 2;
while (repeat_count != 0) {
repeat_count--;
*output_array++ = color;
}
}
}
return (unsigned long)(input - input_start);
}
int clamp(int x, int a, int b) {
if (x < a) {
return a;
} else if (x > b) {
return b;
} else {
return x;
}
}
void rcru_slice(
unsigned char *input,
unsigned char *output,
int texture_width,
int texture_height,
int rectangle_x,
int rectangle_y,
int rectangle_width,
int rectangle_height
) {
unsigned long *input_array = (unsigned long *)(input);
unsigned long *output_array = (unsigned long *)(output);
int x0 = clamp(rectangle_x, 0, texture_width);
int y0 = clamp(rectangle_y, 0, texture_height);
int x1 = clamp(x0 + rectangle_width, 0, texture_width);
int y1 = clamp(y0 + rectangle_height, 0, texture_height);
int i;
int j;
for (j = y0; j < y1; j++) {
for (i = x0; i < x1; i++) {
*output_array++ = *(input_array + i + (j * texture_width));
}
}
}
"
# extract all textures from each bundle
set BUNDLE_DATA_BASE_OFFSET long BUNDLE_NAMES_POINTER
for BUNDLE_INDEX = 0 < BUNDLE_COUNT
# get struct fields for this bundle
get BUNDLE_DATA_OFFSET long MEMORY_FILE2
get BUNDLE_NAME_OFFSET long MEMORY_FILE2
get BUNDLE_NAME_LENGTH long MEMORY_FILE2
set BUNDLE_DATA_ORIGINAL_OFFSET long BUNDLE_DATA_OFFSET
math BUNDLE_DATA_OFFSET + BUNDLE_DATA_BASE_OFFSET
# get bundle name
goto BUNDLE_NAME_OFFSET MEMORY_FILE1 SEEK_SET
getdstring BUNDLE_NAME BUNDLE_NAME_LENGTH MEMORY_FILE1
# parse bundle data
goto BUNDLE_DATA_OFFSET MEMORY_FILE1 SEEK_SET
get TEXTURE_COUNT byte MEMORY_FILE1
get RECTANGLE_COUNT short MEMORY_FILE1
# build up `texture_info texture_array[TEXTURE_COUNT]`
set TEXTURE_ARRAY_SIZE long 16
math TEXTURE_ARRAY_SIZE * TEXTURE_COUNT
set TEXTURE_ARRAY_POINTER long 0
# allocate and reset position
putvarchr MEMORY_FILE6 TEXTURE_ARRAY_SIZE 0 byte
log MEMORY_FILE6 0 0
set TEXTURE_OFFSET long 0
for TEXTURE_INDEX = 0 < TEXTURE_COUNT
get TEXTURE_WIDTH short MEMORY_FILE1
get TEXTURE_HEIGHT short MEMORY_FILE1
set TEXTURE_PIXEL_COUNT long TEXTURE_WIDTH
math TEXTURE_PIXEL_COUNT * TEXTURE_HEIGHT
# build up `uint32 decompressed_texture[TEXTURE_PIXEL_COUNT]`
set TEXTURE_SIZE long 4
math TEXTURE_SIZE * TEXTURE_PIXEL_COUNT
# allocate
putvarchr MEMORY_FILE4 TEXTURE_SIZE 0 byte
# @TODO: reset position? all examples i can find omit this
get PALETTE_COLOR_COUNT byte MEMORY_FILE1
# build up `uint32 texture_palette[PALETTE_COLOR_COUNT]`
set PALETTE_SIZE long PALETTE_COLOR_COUNT
math PALETTE_SIZE + 1 # first palette entry is implicitly 0
math PALETTE_SIZE * 4
set PALETTE_POINTER long 0
# allocate and reset position
putvarchr MEMORY_FILE3 PALETTE_SIZE 0 byte
log MEMORY_FILE3 0 0
# write implicit palette entry
putvarchr MEMORY_FILE3 PALETTE_POINTER 0x00000000 long
math PALETTE_POINTER + 4
for I = 0 < PALETTE_COLOR_COUNT
set COLOR long 0xFF000000
get COLOR_CHANNELS threebyte MEMORY_FILE1
math COLOR | COLOR_CHANNELS
# write palette entry
putvarchr MEMORY_FILE3 PALETTE_POINTER COLOR long
math PALETTE_POINTER + 4
next I
# decompress texture
goto 0 MEMORY_FILE3 SEEK_SET
goto 0 MEMORY_FILE4 SEEK_SET
calldll MEMORY_FILE8 "rcru_rle" tcc RET MEMORY_FILE1 MEMORY_FILE4 TEXTURE_PIXEL_COUNT MEMORY_FILE3
goto RET MEMORY_FILE1 SEEK_CUR
# save texture
string TEXTURE_SIZE_STRING P "%TEXTURE_WIDTH%x%TEXTURE_HEIGHT%"
string TEXTURE_FILENAME_WITHOUT_EXTENSION P "%BUNDLE_NAME%/texture%TEXTURE_INDEX%__%TEXTURE_SIZE_STRING%"
callfunction SAVE_TEXTURE 1
# add texture to `uint32 decompressed_textures[{pixel count of all textures}]`
if TEXTURE_INDEX != 0
append # enable
endif
log MEMORY_FILE5 0 TEXTURE_SIZE MEMORY_FILE4
if TEXTURE_INDEX != 0
append # disable
endif
# write struct fields
putvarchr MEMORY_FILE6 TEXTURE_ARRAY_POINTER TEXTURE_WIDTH long
math TEXTURE_ARRAY_POINTER + 4
putvarchr MEMORY_FILE6 TEXTURE_ARRAY_POINTER TEXTURE_HEIGHT long
math TEXTURE_ARRAY_POINTER + 4
putvarchr MEMORY_FILE6 TEXTURE_ARRAY_POINTER TEXTURE_OFFSET long
math TEXTURE_ARRAY_POINTER + 4
putvarchr MEMORY_FILE6 TEXTURE_ARRAY_POINTER TEXTURE_SIZE long
math TEXTURE_ARRAY_POINTER + 4
math TEXTURE_OFFSET + TEXTURE_SIZE
next TEXTURE_INDEX
# extract slices from textures
if TEXTURE_COUNT > 1
# mark where the texture index list starts
savepos INDICES_BASE_OFFSET MEMORY_FILE1
# mark where the rectangle list starts
set RECTANGLES_BASE_OFFSET long INDICES_BASE_OFFSET
math RECTANGLES_BASE_OFFSET + RECTANGLE_COUNT
for RECTANGLE_INDEX = 0 < RECTANGLE_COUNT
# calculate where we are in the texture index list
set INDEX_OFFSET long RECTANGLE_INDEX
math INDEX_OFFSET + INDICES_BASE_OFFSET
# calculate where we are in the rectangle list
set RECTANGLE_OFFSET long RECTANGLE_INDEX
math RECTANGLE_OFFSET * 8
math RECTANGLE_OFFSET + RECTANGLES_BASE_OFFSET
# get current texture index
goto INDEX_OFFSET MEMORY_FILE1 SEEK_SET
get TEXTURE_INDEX byte MEMORY_FILE1
# get current rectangle
goto RECTANGLE_OFFSET MEMORY_FILE1 SEEK_SET
get RECTANGLE_X short MEMORY_FILE1
get RECTANGLE_Y short MEMORY_FILE1
get RECTANGLE_W short MEMORY_FILE1
get RECTANGLE_H short MEMORY_FILE1
# pick the right element from texture_array
set TEXTURE_INFO_OFFSET long TEXTURE_INDEX
math TEXTURE_INFO_OFFSET * 16
# get struct fields for this texture_array element
goto TEXTURE_INFO_OFFSET MEMORY_FILE6 SEEK_SET
get TEXTURE_WIDTH long MEMORY_FILE6
get TEXTURE_HEIGHT long MEMORY_FILE6
get TEXTURE_OFFSET long MEMORY_FILE6
get TEXTURE_SIZE long MEMORY_FILE6
set SLICE_PIXEL_COUNT long RECTANGLE_W
math SLICE_PIXEL_COUNT * RECTANGLE_H
# build up `uint32 texture_slice[SLICE_PIXEL_COUNT]`
set SLICE_SIZE long SLICE_PIXEL_COUNT
math SLICE_SIZE * 4
# allocate
putvarchr MEMORY_FILE7 SLICE_SIZE 0 byte
# @TODO: reset position? all examples i can find omit this
# slice texture
goto TEXTURE_OFFSET MEMORY_FILE5 SEEK_SET
goto 0 MEMORY_FILE7 SEEK_SET
calldll MEMORY_FILE8 "rcru_slice" tcc "" MEMORY_FILE5 MEMORY_FILE7 TEXTURE_WIDTH TEXTURE_HEIGHT RECTANGLE_X RECTANGLE_Y RECTANGLE_W RECTANGLE_H
# save texture slice
string RECTANGLE_SIZE_STRING P "%RECTANGLE_W%x%RECTANGLE_H%"
string SLICE_FILENAME_WITHOUT_EXTENSION P "%BUNDLE_NAME%/texture%TEXTURE_INDEX%_slice%RECTANGLE_INDEX%__%RECTANGLE_SIZE_STRING%"
callfunction SAVE_TEXTURE_SLICE 1
next RECTANGLE_INDEX
elif TEXTURE_COUNT == 1
set TEXTURE_INDEX long 0
goto 0 MEMORY_FILE6 SEEK_SET
get TEXTURE_WIDTH long MEMORY_FILE6
get TEXTURE_HEIGHT long MEMORY_FILE6
get TEXTURE_OFFSET long MEMORY_FILE6
get TEXTURE_SIZE long MEMORY_FILE6
for RECTANGLE_INDEX = 0 < RECTANGLE_COUNT
get RECTANGLE_X short MEMORY_FILE1
get RECTANGLE_Y short MEMORY_FILE1
get RECTANGLE_W short MEMORY_FILE1
get RECTANGLE_H short MEMORY_FILE1
set SLICE_PIXEL_COUNT long RECTANGLE_W
math SLICE_PIXEL_COUNT * RECTANGLE_H
# build up `uint32 texture_slice[SLICE_PIXEL_COUNT]`
set SLICE_SIZE long SLICE_PIXEL_COUNT
math SLICE_SIZE * 4
# allocate
putvarchr MEMORY_FILE7 SLICE_SIZE 0 byte
# @TODO: reset position? all examples i can find omit this
# slice texture
goto TEXTURE_OFFSET MEMORY_FILE5 SEEK_SET
goto 0 MEMORY_FILE7 SEEK_SET
calldll MEMORY_FILE8 "rcru_slice" tcc "" MEMORY_FILE5 MEMORY_FILE7 TEXTURE_WIDTH TEXTURE_HEIGHT RECTANGLE_X RECTANGLE_Y RECTANGLE_W RECTANGLE_H
# save texture slice
string RECTANGLE_SIZE_STRING P "%RECTANGLE_W%x%RECTANGLE_H%"
string SLICE_FILENAME_WITHOUT_EXTENSION P "%BUNDLE_NAME%/texture%TEXTURE_INDEX%_slice%RECTANGLE_INDEX%__%RECTANGLE_SIZE_STRING%"
callfunction SAVE_TEXTURE_SLICE 1
next RECTANGLE_INDEX
endif
next BUNDLE_INDEX
else
print "unsupported archive"
print " ARCHIVE_TYPE_NAME: %ARCHIVE_TYPE_NAME%"
print " ARCHIVE_TYPE_NUMBER: %ARCHIVE_TYPE_NUMBER%"
cleanexit
endif
startfunction CHECK_ARCHIVE_TYPE_NUMBER
if CHECK_ARCHIVE_TYPE_NUMBER_ARG1 != CHECK_ARCHIVE_TYPE_NUMBER_ARG2
print "expected ARCHIVE_TYPE_NUMBER to be %CHECK_ARCHIVE_TYPE_NUMBER_ARG2%, but got %CHECK_ARCHIVE_TYPE_NUMBER_ARG1%"
cleanexit
endif
endfunction
startfunction DECOMPRESS_ARCHIVE
savepos ARCHIVE_DATA_OFFSET 0
get ARCHIVE_SIZE asize 0
math ARCHIVE_SIZE - ARCHIVE_DATA_OFFSET
comtype gzip
clog MEMORY_FILE1 ARCHIVE_DATA_OFFSET ARCHIVE_SIZE ARCHIVE_SIZE 0
endfunction
startfunction READ_7_BIT_ENCODED_INT
# https://github.com/microsoft/referencesource/blob/e0bf122d0e52a42688b92bb4be2cfd66ca3c2f07/mscorlib/system/io/binaryreader.cs#L582-L599
set R7BEI_COUNT long 0
set R7BEI_SHIFT long 0
set R7BEI_B byte 0
do
if R7BEI_SHIFT == 35
print "bad 7-bit int32:"
print " R7BEI_COUNT: %R7BEI_COUNT%"
print " R7BEI_SHIFT: %R7BEI_SHIFT%"
print " R7BEI_B: %R7BEI_B%"
cleanexit
endif
get R7BEI_B byte MEMORY_FILE1 # can't parameterize this, unfortunately
set R7BEI_TMP1 byte R7BEI_B
math R7BEI_TMP1 & 0x7F
math R7BEI_TMP1 << R7BEI_SHIFT
math R7BEI_COUNT | R7BEI_TMP1
math R7BEI_SHIFT + 7
set R7BEI_TMP2 byte R7BEI_B
math R7BEI_TMP2 & 0x80
while R7BEI_TMP2 != 0
set R7BEI_RET long R7BEI_COUNT
endfunction
startfunction SAVE_TEXTURE
# uncomment the two lines below to save a "raw" file containing all the
# pixels (4 bytes each, rgba) of a texture, with the dimensions included
# in the filename.
# -------------------------------------------------------------------------
# string RAW_TEXTURE_FILENAME P "%TEXTURE_FILENAME_WITHOUT_EXTENSION%.raw"
# log RAW_TEXTURE_FILENAME 0 TEXTURE_SIZE MEMORY_FILE4
# -------------------------------------------------------------------------
# or uncomment the three lines below to feed that file into imagemagick
# and make it spit out a png (windows only).
# -------------------------------------------------------------------------
# string PNG_TEXTURE_FILENAME P "%TEXTURE_FILENAME_WITHOUT_EXTENSION%.png"
# comtype EXECUTE "convert.exe -size %TEXTURE_SIZE_STRING% -depth 8 RGBA:#INPUT# PNG32:#OUTPUT#"
# clog PNG_TEXTURE_FILENAME 0 TEXTURE_SIZE TEXTURE_SIZE MEMORY_FILE4
endfunction
startfunction SAVE_TEXTURE_SLICE
# uncomment the two lines below to save a "raw" file containing all the
# pixels (4 bytes each, rgba) of a texture slice, with the dimensions
# included in the filename.
# -------------------------------------------------------------------------
string RAW_SLICE_FILENAME P "%SLICE_FILENAME_WITHOUT_EXTENSION%.raw"
log RAW_SLICE_FILENAME 0 SLICE_SIZE MEMORY_FILE7
# -------------------------------------------------------------------------
# or uncomment the three lines below to feed that file into imagemagick
# and make it spit out a png (windows only).
# -------------------------------------------------------------------------
# string PNG_SLICE_FILENAME P "%SLICE_FILENAME_WITHOUT_EXTENSION%.png"
# comtype EXECUTE "convert.exe -size %RECTANGLE_SIZE_STRING% -depth 8 RGBA:#INPUT# PNG32:#OUTPUT#"
# clog PNG_SLICE_FILENAME 0 SLICE_SIZE SLICE_SIZE MEMORY_FILE7
endfunction
@nobodyinteresting
Copy link

Just wanted to say thanks for this - after installing ImageMagick and checking the "Legacy" option during install, then uncommenting those 3 lines pertaining to PNGs, this did indeed rip useable PNGs.

They had no filetype, so you still have to manually rename them, and they're all in the various slice folders, so it's still tons of messing around - but they are there, they do exist, and they are usable, so very much thanks a lot!

@leovoel
Copy link
Author

leovoel commented Dec 10, 2023

Hmm I probably messed up something then (been a while...I'm not using Windows anymore so testing this again would be a bit of a pain), it should output files with an extension...but yeah not that it makes much of a difference. Good to hear it worked for you.

@Triforceriku
Copy link

How do i uncomment things?
I'm new to this process

@leovoel
Copy link
Author

leovoel commented Jan 7, 2024

"Uncommenting" means removing the # that precedes a line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment