Task 696: .STO File Format
Task 696: .STO File Format
.STO File Format Specifications
The .STO file format is used in Infinity Engine games (e.g., Baldur's Gate) to describe stores, inns, taverns, or temples, including items for sale, services, and related data. It is a binary format with a fixed header, followed by variable sections for purchased item categories, items for sale, drinks, and cures. The specification is based on version V1.1 (common in enhanced editions). All multi-byte values are little-endian.
1. List of All Properties Intrinsic to This File Format
The properties are the structural fields and sections defined in the format. They include the header fields, as well as the sub-structures for purchased categories, items for sale, drinks, and cures. Enums and bit flags are also intrinsic.
Header Properties (Fixed Size: 0x0078 + 36 unknown = 180 bytes)
- Signature: 4 bytes (char array, always 'STOR')
- Version: 4 bytes (char array, 'V1.1')
- Type: 4 bytes (dword, enum: 0=Store, 1=Tavern, 2=Inn, 3=Temple)
- Name: 4 bytes (strref, string reference index)
- Flags: 4 bytes (dword, bitfield: bit 0=Buy allowed, bit 1=Sell allowed, bit 2=Identify allowed, bit 3=Steal allowed, bit 4=Donate allowed, bit 5=Cures allowed, bit 6=Drinks allowed, bit 7-9=Unknown, bit 10=Recharge?, bit 11=Unknown, bit 12=Buy fenced goods)
- Sell markup: 4 bytes (dword, percentage)
- Buy markup: 4 bytes (dword, percentage)
- Depreciation rate: 4 bytes (dword, percentage)
- Steal failure chance: 2 bytes (word, percentage)
- Capacity: 2 bytes (word)
- Unknown1: 8 bytes (bytes)
- Purchased categories offset: 4 bytes (dword, offset from file start)
- Purchased categories count: 4 bytes (dword)
- Items for sale offset: 4 bytes (dword, offset from file start)
- Items for sale count: 4 bytes (dword)
- Lore: 4 bytes (dword)
- ID price: 4 bytes (dword)
- Rumours tavern: 8 bytes (resref, resource reference)
- Drinks offset: 4 bytes (dword, offset from file start)
- Drinks count: 4 bytes (dword)
- Rumours temple: 8 bytes (resref, resource reference)
- Room flags: 4 bytes (dword, bitfield: bit 0=Peasant, bit 1=Merchant, bit 2=Noble, bit 3=Royal)
- Price peasant: 4 bytes (dword)
- Price merchant: 4 bytes (dword)
- Price noble: 4 bytes (dword)
- Price royal: 4 bytes (dword)
- Cures offset: 4 bytes (dword, offset from file start)
- Cures count: 4 bytes (dword)
- Unknown2: 36 bytes (bytes)
Purchased Categories Section (Variable, Each Entry: 4 bytes)
- Category: 4 bytes (dword, enum for item type, e.g., 0=Books/misc, 1=Amulets, 2=Armor, ..., up to 0x0049=Gauntlet; full enum list as per spec)
Items for Sale Section (Variable, Each Entry: 88 bytes)
- Item resref: 8 bytes (resref)
- Expiration time: 2 bytes (word)
- Quantity/charges1: 2 bytes (word)
- Quantity/charges2: 2 bytes (word)
- Quantity/charges3: 2 bytes (word)
- Item flags: 4 bytes (dword, bitfield: bit 0=Identified, bit 1=Unstealable, bit 2=Stolen, bit 3=Undroppable)
- Stock amount: 4 bytes (dword)
- Infinite supply: 4 bytes (dword, 0=limited, 1=infinite)
- Trigger strref: 4 bytes (strref)
- Unknown: 56 bytes (bytes)
Drinks Section (Variable, Each Entry: 20 bytes, Tavern-specific)
- Rumour resref: 8 bytes (resref)
- Drink name strref: 4 bytes (strref)
- Price: 4 bytes (dword)
- Alcoholic strength: 4 bytes (dword)
Cures Section (Variable, Each Entry: 12 bytes, Temple-specific)
- Spell resref: 8 bytes (resref)
- Price: 4 bytes (dword)
2. Two Direct Download Links for .STO Files
- https://raw.githubusercontent.com/Pocket-Plane-Group/bg1ub/master/bg1ub/sto/ar0108.sto
- https://raw.githubusercontent.com/Pocket-Plane-Group/bg1ub/master/bg1ub/sto/ar0112.sto
3. Ghost Blog Embedded HTML JavaScript for Drag and Drop .STO File Dump
Drag and drop .STO file here
4. Python Class for .STO File Handling
import struct
import os
class STOFile:
def __init__(self):
self.signature = b'STOR'
self.version = b'V1.1'
self.type = 0
self.name = 0
self.flags = 0
self.sell_markup = 100
self.buy_markup = 100
self.depreciation = 0
self.steal_failure = 0
self.capacity = 0
self.unknown1 = bytes(8)
self.purchased_categories = []
self.items_for_sale = [] # list of dicts
self.lore = 0
self.id_price = 0
self.rumours_tavern = b''
self.drinks = [] # list of dicts
self.rumours_temple = b''
self.room_flags = 0
self.price_peasant = 0
self.price_merchant = 0
self.price_noble = 0
self.price_royal = 0
self.cures = [] # list of dicts
self.unknown2 = bytes(36)
def read(self, filename):
with open(filename, 'rb') as f:
data = f.read()
if len(data) < 156:
raise ValueError("Invalid STO file")
(self.signature, self.version, self.type, self.name, self.flags, self.sell_markup,
self.buy_markup, self.depreciation, self.steal_failure, self.capacity,
self.unknown1, purchased_offset, purchased_count, items_offset, items_count,
self.lore, self.id_price, self.rumours_tavern, drinks_offset, drinks_count,
self.rumours_temple, self.room_flags, self.price_peasant, self.price_merchant,
self.price_noble, self.price_royal, cures_offset, cures_count,
self.unknown2) = struct.unpack('<4s4sIIIHH8sIIIII8sII8sIII4sII36s', data[:156])
# Purchased categories
self.purchased_categories = []
for i in range(purchased_count):
cat = struct.unpack('<I', data[purchased_offset + i*4 : purchased_offset + (i+1)*4])[0]
self.purchased_categories.append(cat)
# Items for sale
self.items_for_sale = []
for i in range(items_count):
base = items_offset + i*88
(resref, exp_time, q1, q2, q3, item_flags, stock, infinite, trigger, unknown) = struct.unpack('<8sHHHHIII56s', data[base:base+88])
self.items_for_sale.append({
'resref': resref.rstrip(b'\0').decode(),
'exp_time': exp_time,
'q1': q1, 'q2': q2, 'q3': q3,
'flags': item_flags,
'stock': stock,
'infinite': infinite,
'trigger': trigger,
'unknown': unknown
})
# Drinks
self.drinks = []
for i in range(drinks_count):
base = drinks_offset + i*20
(resref, name_strref, price, strength) = struct.unpack('<8sIII', data[base:base+20])
self.drinks.append({
'resref': resref.rstrip(b'\0').decode(),
'name_strref': name_strref,
'price': price,
'strength': strength
})
# Cures
self.cures = []
for i in range(cures_count):
base = cures_offset + i*12
(resref, price) = struct.unpack('<8sI', data[base:base+12])
self.cures.append({
'resref': resref.rstrip(b'\0').decode(),
'price': price
})
def write(self, filename):
header = struct.pack('<4s4sIIIHH8sIIIII8sII8sIII4sII36s',
self.signature, self.version, self.type, self.name, self.flags, self.sell_markup,
self.buy_markup, self.depreciation, self.steal_failure, self.capacity,
self.unknown1, 0, 0, 0, 0, # placeholders for offsets/counts
self.lore, self.id_price, self.rumours_tavern.ljust(8, b'\0'), 0, 0,
self.rumours_temple.ljust(8, b'\0'), self.room_flags, self.price_peasant, self.price_merchant,
self.price_noble, self.price_royal, 0, 0,
self.unknown2)
purchased_data = b''.join(struct.pack('<I', cat) for cat in self.purchased_categories)
items_data = b''
for item in self.items_for_sale:
items_data += struct.pack('<8sHHHHIII56s',
item['resref'].encode().ljust(8, b'\0'), item['exp_time'], item['q1'], item['q2'], item['q3'],
item['flags'], item['stock'], item['infinite'], item['trigger'], item['unknown'])
drinks_data = b''
for drink in self.drinks:
drinks_data += struct.pack('<8sIII',
drink['resref'].encode().ljust(8, b'\0'), drink['name_strref'], drink['price'], drink['strength'])
cures_data = b''
for cure in self.cures:
cures_data += struct.pack('<8sI', cure['resref'].encode().ljust(8, b'\0'), cure['price'])
purchased_offset = len(header)
items_offset = purchased_offset + len(purchased_data)
drinks_offset = items_offset + len(items_data)
cures_offset = drinks_offset + len(drinks_data)
header = struct.pack('<4s4sIIIHH8sIIIII8sII8sIII4sII36s',
self.signature, self.version, self.type, self.name, self.flags, self.sell_markup,
self.buy_markup, self.depreciation, self.steal_failure, self.capacity,
self.unknown1, purchased_offset, len(self.purchased_categories), items_offset, len(self.items_for_sale),
self.lore, self.id_price, self.rumours_tavern.ljust(8, b'\0'), drinks_offset, len(self.drinks),
self.rumours_temple.ljust(8, b'\0'), self.room_flags, self.price_peasant, self.price_merchant,
self.price_noble, self.price_royal, cures_offset, len(self.cures),
self.unknown2)
with open(filename, 'wb') as f:
f.write(header + purchased_data + items_data + drinks_data + cures_data)
def print_properties(self):
print('Header:')
print(f'Signature: {self.signature.decode()}')
print(f'Version: {self.version.decode()}')
print(f'Type: {self.type}')
print(f'Name (strref): {self.name}')
print(f'Flags: {self.flags} (0x{self.flags:x})')
print(f'Sell markup: {self.sell_markup}')
print(f'Buy markup: {self.buy_markup}')
print(f'Depreciation rate: {self.depreciation}')
print(f'Steal failure chance: {self.steal_failure}')
print(f'Capacity: {self.capacity}')
print(f'Unknown1: {list(self.unknown1)}')
print(f'Lore: {self.lore}')
print(f'ID price: {self.id_price}')
print(f'Rumours tavern: {self.rumours_tavern.decode()}')
print(f'Rumours temple: {self.rumours_temple.decode()}')
print(f'Room flags: {self.room_flags} (0x{self.room_flags:x})')
print(f'Price peasant: {self.price_peasant}')
print(f'Price merchant: {self.price_merchant}')
print(f'Price noble: {self.price_noble}')
print(f'Price royal: {self.price_royal}')
print(f'Unknown2: {list(self.unknown2)}')
print('\nPurchased Categories:')
for i, cat in enumerate(self.purchased_categories):
print(f'Category {i}: {cat}')
print('\nItems for Sale:')
for i, item in enumerate(self.items_for_sale):
print(f'Item {i}:')
for k, v in item.items():
if k == 'unknown':
print(f' {k}: {list(v)}')
else:
print(f' {k}: {v}')
print('\nDrinks:')
for i, drink in enumerate(self.drinks):
print(f'Drink {i}:')
for k, v in drink.items():
print(f' {k}: {v}')
print('\nCures:')
for i, cure in enumerate(self.cures):
print(f'Cure {i}:')
for k, v in cure.items():
print(f' {k}: {v}')
# Example usage:
# sto = STOFile()
# sto.read('example.sto')
# sto.print_properties()
# sto.write('output.sto')
5. Java Class for .STO File Handling
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
public class STOFile {
private String signature = "STOR";
private String version = "V1.1";
private int type;
private int name;
private int flags;
private int sellMarkup;
private int buyMarkup;
private int depreciation;
private short stealFailure;
private short capacity;
private byte[] unknown1 = new byte[8];
private List<Integer> purchasedCategories = new ArrayList<>();
private List<ItemForSale> itemsForSale = new ArrayList<>();
private int lore;
private int idPrice;
private String rumoursTavern = "";
private List<Drink> drinks = new ArrayList<>();
private String rumoursTemple = "";
private int roomFlags;
private int pricePeasant;
private int priceMerchant;
private int priceNoble;
private int priceRoyal;
private List<Cure> cures = new ArrayList<>();
private byte[] unknown2 = new byte[36];
static class ItemForSale {
String resref;
short expTime;
short q1, q2, q3;
int flags;
int stock;
int infinite;
int trigger;
byte[] unknown = new byte[56];
}
static class Drink {
String resref;
int nameStrref;
int price;
int strength;
}
static class Cure {
String resref;
int price;
}
public void read(String filename) throws IOException {
RandomAccessFile file = new RandomAccessFile(filename, "r");
FileChannel channel = file.getChannel();
ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
buffer.order(ByteOrder.LITTLE_ENDIAN);
channel.read(buffer);
buffer.flip();
byte[] sigBytes = new byte[4];
buffer.get(sigBytes);
signature = new String(sigBytes);
buffer.get(sigBytes);
version = new String(sigBytes);
type = buffer.getInt();
name = buffer.getInt();
flags = buffer.getInt();
sellMarkup = buffer.getInt();
buyMarkup = buffer.getInt();
depreciation = buffer.getInt();
stealFailure = buffer.getShort();
capacity = buffer.getShort();
buffer.get(unknown1);
int purchasedOffset = buffer.getInt();
int purchasedCount = buffer.getInt();
int itemsOffset = buffer.getInt();
int itemsCount = buffer.getInt();
lore = buffer.getInt();
idPrice = buffer.getInt();
byte[] resBytes = new byte[8];
buffer.get(resBytes);
rumoursTavern = new String(resBytes).trim();
int drinksOffset = buffer.getInt();
int drinksCount = buffer.getInt();
buffer.get(resBytes);
rumoursTemple = new String(resBytes).trim();
roomFlags = buffer.getInt();
pricePeasant = buffer.getInt();
priceMerchant = buffer.getInt();
priceNoble = buffer.getInt();
priceRoyal = buffer.getInt();
int curesOffset = buffer.getInt();
int curesCount = buffer.getInt();
buffer.get(unknown2);
buffer.position(purchasedOffset);
for (int i = 0; i < purchasedCount; i++) {
purchasedCategories.add(buffer.getInt());
}
buffer.position(itemsOffset);
for (int i = 0; i < itemsCount; i++) {
ItemForSale item = new ItemForSale();
buffer.get(resBytes);
item.resref = new String(resBytes).trim();
item.expTime = buffer.getShort();
item.q1 = buffer.getShort();
item.q2 = buffer.getShort();
item.q3 = buffer.getShort();
item.flags = buffer.getInt();
item.stock = buffer.getInt();
item.infinite = buffer.getInt();
item.trigger = buffer.getInt();
buffer.get(item.unknown);
itemsForSale.add(item);
}
buffer.position(drinksOffset);
for (int i = 0; i < drinksCount; i++) {
Drink drink = new Drink();
buffer.get(resBytes);
drink.resref = new String(resBytes).trim();
drink.nameStrref = buffer.getInt();
drink.price = buffer.getInt();
drink.strength = buffer.getInt();
drinks.add(drink);
}
buffer.position(curesOffset);
for (int i = 0; i < curesCount; i++) {
Cure cure = new Cure();
buffer.get(resBytes);
cure.resref = new String(resBytes).trim();
cure.price = buffer.getInt();
cures.add(cure);
}
file.close();
}
public void write(String filename) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // large enough
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put(signature.getBytes());
buffer.put(version.getBytes());
buffer.putInt(type);
buffer.putInt(name);
buffer.putInt(flags);
buffer.putInt(sellMarkup);
buffer.putInt(buyMarkup);
buffer.putInt(depreciation);
buffer.putShort(stealFailure);
buffer.putShort(capacity);
buffer.put(unknown1);
int purchasedOffsetPos = buffer.position();
buffer.putInt(0); // placeholder
buffer.putInt(purchasedCategories.size());
int itemsOffsetPos = buffer.position();
buffer.putInt(0); // placeholder
buffer.putInt(itemsForSale.size());
buffer.putInt(lore);
buffer.putInt(idPrice);
buffer.put(rumoursTavern.getBytes());
for (int i = rumoursTavern.length(); i < 8; i++) buffer.put((byte) 0);
int drinksOffsetPos = buffer.position();
buffer.putInt(0); // placeholder
buffer.putInt(drinks.size());
buffer.put(rumoursTemple.getBytes());
for (int i = rumoursTemple.length(); i < 8; i++) buffer.put((byte) 0);
buffer.putInt(roomFlags);
buffer.putInt(pricePeasant);
buffer.putInt(priceMerchant);
buffer.putInt(priceNoble);
buffer.putInt(priceRoyal);
int curesOffsetPos = buffer.position();
buffer.putInt(0); // placeholder
buffer.putInt(cures.size());
buffer.put(unknown2);
int purchasedOffset = buffer.position();
for (int cat : purchasedCategories) {
buffer.putInt(cat);
}
int itemsOffset = buffer.position();
for (ItemForSale item : itemsForSale) {
buffer.put(item.resref.getBytes());
for (int i = item.resref.length(); i < 8; i++) buffer.put((byte) 0);
buffer.putShort(item.expTime);
buffer.putShort(item.q1);
buffer.putShort(item.q2);
buffer.putShort(item.q3);
buffer.putInt(item.flags);
buffer.putInt(item.stock);
buffer.putInt(item.infinite);
buffer.putInt(item.trigger);
buffer.put(item.unknown);
}
int drinksOffset = buffer.position();
for (Drink drink : drinks) {
buffer.put(drink.resref.getBytes());
for (int i = drink.resref.length(); i < 8; i++) buffer.put((byte) 0);
buffer.putInt(drink.nameStrref);
buffer.putInt(drink.price);
buffer.putInt(drink.strength);
}
int curesOffset = buffer.position();
for (Cure cure : cures) {
buffer.put(cure.resref.getBytes());
for (int i = cure.resref.length(); i < 8; i++) buffer.put((byte) 0);
buffer.putInt(cure.price);
}
buffer.position(purchasedOffsetPos);
buffer.putInt(purchasedOffset);
buffer.position(itemsOffsetPos);
buffer.putInt(itemsOffset);
buffer.position(drinksOffsetPos);
buffer.putInt(drinksOffset);
buffer.position(curesOffsetPos);
buffer.putInt(curesOffset);
buffer.flip();
try (FileOutputStream fos = new FileOutputStream(filename)) {
fos.getChannel().write(buffer);
}
}
public void printProperties() {
System.out.println("Header:");
System.out.println("Signature: " + signature);
System.out.println("Version: " + version);
System.out.println("Type: " + type);
System.out.println("Name (strref): " + name);
System.out.println("Flags: " + flags + " (0x" + Integer.toHexString(flags) + ")");
System.out.println("Sell markup: " + sellMarkup);
System.out.println("Buy markup: " + buyMarkup);
System.out.println("Depreciation rate: " + depreciation);
System.out.println("Steal failure chance: " + stealFailure);
System.out.println("Capacity: " + capacity);
System.out.print("Unknown1: [");
for (byte b : unknown1) System.out.print(b + " ");
System.out.println("]");
System.out.println("Lore: " + lore);
System.out.println("ID price: " + idPrice);
System.out.println("Rumours tavern: " + rumoursTavern);
System.out.println("Rumours temple: " + rumoursTemple);
System.out.println("Room flags: " + roomFlags + " (0x" + Integer.toHexString(roomFlags) + ")");
System.out.println("Price peasant: " + pricePeasant);
System.out.println("Price merchant: " + priceMerchant);
System.out.println("Price noble: " + priceNoble);
System.out.println("Price royal: " + priceRoyal);
System.out.print("Unknown2: [");
for (byte b : unknown2) System.out.print(b + " ");
System.out.println("]");
System.out.println("\nPurchased Categories:");
for (int i = 0; i < purchasedCategories.size(); i++) {
System.out.println("Category " + i + ": " + purchasedCategories.get(i));
}
System.out.println("\nItems for Sale:");
for (int i = 0; i < itemsForSale.size(); i++) {
ItemForSale item = itemsForSale.get(i);
System.out.println("Item " + i + ":");
System.out.println(" resref: " + item.resref);
System.out.println(" expTime: " + item.expTime);
System.out.println(" q1: " + item.q1);
System.out.println(" q2: " + item.q2);
System.out.println(" q3: " + item.q3);
System.out.println(" flags: " + item.flags + " (0x" + Integer.toHexString(item.flags) + ")");
System.out.println(" stock: " + item.stock);
System.out.println(" infinite: " + item.infinite);
System.out.println(" trigger: " + item.trigger);
System.out.print(" unknown: [");
for (byte b : item.unknown) System.out.print(b + " ");
System.out.println("]");
}
System.out.println("\nDrinks:");
for (int i = 0; i < drinks.size(); i++) {
Drink drink = drinks.get(i);
System.out.println("Drink " + i + ":");
System.out.println(" resref: " + drink.resref);
System.out.println(" nameStrref: " + drink.nameStrref);
System.out.println(" price: " + drink.price);
System.out.println(" strength: " + drink.strength);
}
System.out.println("\nCures:");
for (int i = 0; i < cures.size(); i++) {
Cure cure = cures.get(i);
System.out.println("Cure " + i + ":");
System.out.println(" resref: " + cure.resref);
System.out.println(" price: " + cure.price);
}
}
// Example usage:
// public static void main(String[] args) throws IOException {
// STOFile sto = new STOFile();
// sto.read("example.sto");
// sto.printProperties();
// sto.write("output.sto");
// }
}
6. JavaScript Class for .STO File Handling
class STOFile {
constructor() {
this.signature = 'STOR';
this.version = 'V1.1';
this.type = 0;
this.name = 0;
this.flags = 0;
this.sellMarkup = 100;
this.buyMarkup = 100;
this.depreciation = 0;
this.stealFailure = 0;
this.capacity = 0;
this.unknown1 = new Uint8Array(8);
this.purchasedCategories = [];
this.itemsForSale = [];
this.lore = 0;
this.idPrice = 0;
this.rumoursTavern = '';
this.drinks = [];
this.rumoursTemple = '';
this.roomFlags = 0;
this.pricePeasant = 0;
this.priceMerchant = 0;
this.priceNoble = 0;
this.priceRoyal = 0;
this.cures = [];
this.unknown2 = new Uint8Array(36);
}
async read(filename) {
const response = await fetch(filename);
const buffer = await response.arrayBuffer();
const view = new DataView(buffer);
this.signature = getString(view, 0, 4);
this.version = getString(view, 4, 4);
this.type = view.getUint32(8, true);
this.name = view.getUint32(12, true);
this.flags = view.getUint32(16, true);
this.sellMarkup = view.getUint32(20, true);
this.buyMarkup = view.getUint32(24, true);
this.depreciation = view.getUint32(28, true);
this.stealFailure = view.getUint16(32, true);
this.capacity = view.getUint16(34, true);
this.unknown1 = new Uint8Array(buffer.slice(36, 44));
const purchasedOffset = view.getUint32(44, true);
const purchasedCount = view.getUint32(48, true);
const itemsOffset = view.getUint32(52, true);
const itemsCount = view.getUint32(56, true);
this.lore = view.getUint32(60, true);
this.idPrice = view.getUint32(64, true);
this.rumoursTavern = getString(view, 68, 8);
const drinksOffset = view.getUint32(76, true);
const drinksCount = view.getUint32(80, true);
this.rumoursTemple = getString(view, 84, 8);
this.roomFlags = view.getUint32(92, true);
this.pricePeasant = view.getUint32(96, true);
this.priceMerchant = view.getUint32(100, true);
this.priceNoble = view.getUint32(104, true);
this.priceRoyal = view.getUint32(108, true);
const curesOffset = view.getUint32(112, true);
const curesCount = view.getUint32(116, true);
this.unknown2 = new Uint8Array(buffer.slice(120, 156));
this.purchasedCategories = [];
for (let i = 0; i < purchasedCount; i++) {
this.purchasedCategories.push(view.getUint32(purchasedOffset + i * 4, true));
}
this.itemsForSale = [];
for (let i = 0; i < itemsCount; i++) {
const base = itemsOffset + i * 88;
this.itemsForSale.push({
resref: getString(view, base, 8),
expTime: view.getUint16(base + 8, true),
q1: view.getUint16(base + 10, true),
q2: view.getUint16(base + 12, true),
q3: view.getUint16(base + 14, true),
flags: view.getUint32(base + 16, true),
stock: view.getUint32(base + 20, true),
infinite: view.getUint32(base + 24, true),
trigger: view.getUint32(base + 28, true),
unknown: new Uint8Array(buffer.slice(base + 32, base + 88))
});
}
this.drinks = [];
for (let i = 0; i < drinksCount; i++) {
const base = drinksOffset + i * 20;
this.drinks.push({
resref: getString(view, base, 8),
nameStrref: view.getUint32(base + 8, true),
price: view.getUint32(base + 12, true),
strength: view.getUint32(base + 16, true)
});
}
this.cures = [];
for (let i = 0; i < curesCount; i++) {
const base = curesOffset + i * 12;
this.cures.push({
resref: getString(view, base, 8),
price: view.getUint32(base + 8, true)
});
}
}
write() {
// Note: Writing to file requires Node.js or browser download; here we return ArrayBuffer
let size = 156 + this.purchasedCategories.length * 4 + this.itemsForSale.length * 88 +
this.drinks.length * 20 + this.cures.length * 12;
const buffer = new ArrayBuffer(size);
const view = new DataView(buffer);
setString(view, 0, this.signature, 4);
setString(view, 4, this.version, 4);
view.setUint32(8, this.type, true);
view.setUint32(12, this.name, true);
view.setUint32(16, this.flags, true);
view.setUint32(20, this.sellMarkup, true);
view.setUint32(24, this.buyMarkup, true);
view.setUint32(28, this.depreciation, true);
view.setUint16(32, this.stealFailure, true);
view.setUint16(34, this.capacity, true);
for (let i = 0; i < 8; i++) view.setUint8(36 + i, this.unknown1[i]);
const purchasedOffset = 156;
view.setUint32(44, purchasedOffset, true);
view.setUint32(48, this.purchasedCategories.length, true);
const itemsOffset = purchasedOffset + this.purchasedCategories.length * 4;
view.setUint32(52, itemsOffset, true);
view.setUint32(56, this.itemsForSale.length, true);
view.setUint32(60, this.lore, true);
view.setUint32(64, this.idPrice, true);
setString(view, 68, this.rumoursTavern, 8);
const drinksOffset = itemsOffset + this.itemsForSale.length * 88;
view.setUint32(76, drinksOffset, true);
view.setUint32(80, this.drinks.length, true);
setString(view, 84, this.rumoursTemple, 8);
view.setUint32(92, this.roomFlags, true);
view.setUint32(96, this.pricePeasant, true);
view.setUint32(100, this.priceMerchant, true);
view.setUint32(104, this.priceNoble, true);
view.setUint32(108, this.priceRoyal, true);
const curesOffset = drinksOffset + this.drinks.length * 20;
view.setUint32(112, curesOffset, true);
view.setUint32(116, this.cures.length, true);
for (let i = 0; i < 36; i++) view.setUint8(120 + i, this.unknown2[i]);
let offset = purchasedOffset;
for (let cat of this.purchasedCategories) {
view.setUint32(offset, cat, true);
offset += 4;
}
offset = itemsOffset;
for (let item of this.itemsForSale) {
setString(view, offset, item.resref, 8);
view.setUint16(offset + 8, item.expTime, true);
view.setUint16(offset + 10, item.q1, true);
view.setUint16(offset + 12, item.q2, true);
view.setUint16(offset + 14, item.q3, true);
view.setUint32(offset + 16, item.flags, true);
view.setUint32(offset + 20, item.stock, true);
view.setUint32(offset + 24, item.infinite, true);
view.setUint32(offset + 28, item.trigger, true);
for (let i = 0; i < 56; i++) view.setUint8(offset + 32 + i, item.unknown[i]);
offset += 88;
}
offset = drinksOffset;
for (let drink of this.drinks) {
setString(view, offset, drink.resref, 8);
view.setUint32(offset + 8, drink.nameStrref, true);
view.setUint32(offset + 12, drink.price, true);
view.setUint32(offset + 16, drink.strength, true);
offset += 20;
}
offset = curesOffset;
for (let cure of this.cures) {
setString(view, offset, cure.resref, 8);
view.setUint32(offset + 8, cure.price, true);
offset += 12;
}
return buffer;
}
printProperties() {
let result = 'Header:\n';
result += `Signature: ${this.signature}\n`;
result += `Version: ${this.version}\n`;
result += `Type: ${this.type}\n`;
result += `Name (strref): ${this.name}\n`;
result += `Flags: ${this.flags} (0x${this.flags.toString(16)})\n`;
result += `Sell markup: ${this.sellMarkup}\n`;
result += `Buy markup: ${this.buyMarkup}\n`;
result += `Depreciation rate: ${this.depreciation}\n`;
result += `Steal failure chance: ${this.stealFailure}\n`;
result += `Capacity: ${this.capacity}\n`;
result += `Unknown1: [${Array.from(this.unknown1).join(', ')}]\n`;
result += `Lore: ${this.lore}\n`;
result += `ID price: ${this.idPrice}\n`;
result += `Rumours tavern: ${this.rumoursTavern}\n`;
result += `Rumours temple: ${this.rumoursTemple}\n`;
result += `Room flags: ${this.roomFlags} (0x${this.roomFlags.toString(16)})\n`;
result += `Price peasant: ${this.pricePeasant}\n`;
result += `Price merchant: ${this.priceMerchant}\n`;
result += `Price noble: ${this.priceNoble}\n`;
result += `Price royal: ${this.priceRoyal}\n`;
result += `Unknown2: [${Array.from(this.unknown2).join(', ')}]\n\n`;
result += 'Purchased Categories:\n';
this.purchasedCategories.forEach((cat, i) => {
result += `Category ${i}: ${cat}\n`;
});
result += '\nItems for Sale:\n';
this.itemsForSale.forEach((item, i) => {
result += `Item ${i}:\n`;
result += ` resref: ${item.resref}\n`;
result += ` expTime: ${item.expTime}\n`;
result += ` q1: ${item.q1}\n`;
result += ` q2: ${item.q2}\n`;
result += ` q3: ${item.q3}\n`;
result += ` flags: ${item.flags} (0x${item.flags.toString(16)})\n`;
result += ` stock: ${item.stock}\n`;
result += ` infinite: ${item.infinite}\n`;
result += ` trigger: ${item.trigger}\n`;
result += ` unknown: [${Array.from(item.unknown).join(', ')}]\n`;
});
result += '\nDrinks:\n';
this.drinks.forEach((drink, i) => {
result += `Drink ${i}:\n`;
result += ` resref: ${drink.resref}\n`;
result += ` nameStrref: ${drink.nameStrref}\n`;
result += ` price: ${drink.price}\n`;
result += ` strength: ${drink.strength}\n`;
});
result += '\nCures:\n';
this.cures.forEach((cure, i) => {
result += `Cure ${i}:\n`;
result += ` resref: ${cure.resref}\n`;
result += ` price: ${cure.price}\n`;
});
console.log(result);
}
}
function getString(view, offset, length) {
let str = '';
for (let i = 0; i < length; i++) {
const char = view.getUint8(offset + i);
if (char === 0) break;
str += String.fromCharCode(char);
}
return str;
}
function setString(view, offset, str, maxLength) {
for (let i = 0; i < maxLength; i++) {
view.setUint8(offset + i, i < str.length ? str.charCodeAt(i) : 0);
}
}
// Example usage:
// const sto = new STOFile();
// await sto.read('example.sto');
// sto.printProperties();
// const buffer = sto.write();
// // Use buffer to save file, e.g., in Node.js or browser blob download
7. C "Class" for .STO File Handling
In C, we use structs and functions instead of classes.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef struct {
char resref[9];
uint16_t exp_time;
uint16_t q1, q2, q3;
uint32_t flags;
uint32_t stock;
uint32_t infinite;
uint32_t trigger;
uint8_t unknown[56];
} ItemForSale;
typedef struct {
char resref[9];
uint32_t name_strref;
uint32_t price;
uint32_t strength;
} Drink;
typedef struct {
char resref[9];
uint32_t price;
} Cure;
typedef struct {
char signature[5];
char version[5];
uint32_t type;
uint32_t name;
uint32_t flags;
uint32_t sell_markup;
uint32_t buy_markup;
uint32_t depreciation;
uint16_t steal_failure;
uint16_t capacity;
uint8_t unknown1[8];
uint32_t* purchased_categories;
size_t purchased_count;
ItemForSale* items_for_sale;
size_t items_count;
uint32_t lore;
uint32_t id_price;
char rumours_tavern[9];
Drink* drinks;
size_t drinks_count;
char rumours_temple[9];
uint32_t room_flags;
uint32_t price_peasant;
uint32_t price_merchant;
uint32_t price_noble;
uint32_t price_royal;
Cure* cures;
size_t cures_count;
uint8_t unknown2[36];
} STOFile;
STOFile* sto_create() {
STOFile* sto = calloc(1, sizeof(STOFile));
strcpy(sto->signature, "STOR");
strcpy(sto->version, "V1.1");
sto->sell_markup = 100;
sto->buy_markup = 100;
return sto;
}
void sto_free(STOFile* sto) {
free(sto->purchased_categories);
free(sto->items_for_sale);
free(sto->drinks);
free(sto->cures);
free(sto);
}
int sto_read(STOFile* sto, const char* filename) {
FILE* f = fopen(filename, "rb");
if (!f) return -1;
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t* data = malloc(size);
fread(data, 1, size, f);
fclose(f);
memcpy(sto->signature, data, 4); sto->signature[4] = 0;
memcpy(sto->version, data + 4, 4); sto->version[4] = 0;
memcpy(&sto->type, data + 8, 4);
memcpy(&sto->name, data + 12, 4);
memcpy(&sto->flags, data + 16, 4);
memcpy(&sto->sell_markup, data + 20, 4);
memcpy(&sto->buy_markup, data + 24, 4);
memcpy(&sto->depreciation, data + 28, 4);
memcpy(&sto->steal_failure, data + 32, 2);
memcpy(&sto->capacity, data + 34, 2);
memcpy(sto->unknown1, data + 36, 8);
uint32_t purchased_offset; memcpy(&purchased_offset, data + 44, 4);
uint32_t purchased_count; memcpy(&purchased_count, data + 48, 4);
uint32_t items_offset; memcpy(&items_offset, data + 52, 4);
uint32_t items_count; memcpy(&items_count, data + 56, 4);
memcpy(&sto->lore, data + 60, 4);
memcpy(&sto->id_price, data + 64, 4);
memcpy(sto->rumours_tavern, data + 68, 8); sto->rumours_tavern[8] = 0;
uint32_t drinks_offset; memcpy(&drinks_offset, data + 76, 4);
uint32_t drinks_count; memcpy(&drinks_count, data + 80, 4);
memcpy(sto->rumours_temple, data + 84, 8); sto->rumours_temple[8] = 0;
memcpy(&sto->room_flags, data + 92, 4);
memcpy(&sto->price_peasant, data + 96, 4);
memcpy(&sto->price_merchant, data + 100, 4);
memcpy(&sto->price_noble, data + 104, 4);
memcpy(&sto->price_royal, data + 108, 4);
uint32_t cures_offset; memcpy(&cures_offset, data + 112, 4);
uint32_t cures_count; memcpy(&cures_count, data + 116, 4);
memcpy(sto->unknown2, data + 120, 36);
sto->purchased_count = purchased_count;
sto->purchased_categories = malloc(purchased_count * 4);
memcpy(sto->purchased_categories, data + purchased_offset, purchased_count * 4);
sto->items_count = items_count;
sto->items_for_sale = malloc(items_count * sizeof(ItemForSale));
for (size_t i = 0; i < items_count; i++) {
uint8_t* base = data + items_offset + i * 88;
ItemForSale* item = &sto->items_for_sale[i];
memcpy(item->resref, base, 8); item->resref[8] = 0;
memcpy(&item->exp_time, base + 8, 2);
memcpy(&item->q1, base + 10, 2);
memcpy(&item->q2, base + 12, 2);
memcpy(&item->q3, base + 14, 2);
memcpy(&item->flags, base + 16, 4);
memcpy(&item->stock, base + 20, 4);
memcpy(&item->infinite, base + 24, 4);
memcpy(&item->trigger, base + 28, 4);
memcpy(item->unknown, base + 32, 56);
}
sto->drinks_count = drinks_count;
sto->drinks = malloc(drinks_count * sizeof(Drink));
for (size_t i = 0; i < drinks_count; i++) {
uint8_t* base = data + drinks_offset + i * 20;
Drink* drink = &sto->drinks[i];
memcpy(drink->resref, base, 8); drink->resref[8] = 0;
memcpy(&drink->name_strref, base + 8, 4);
memcpy(&drink->price, base + 12, 4);
memcpy(&drink->strength, base + 16, 4);
}
sto->cures_count = cures_count;
sto->cures = malloc(cures_count * sizeof(Cure));
for (size_t i = 0; i < cures_count; i++) {
uint8_t* base = data + cures_offset + i * 12;
Cure* cure = &sto->cures[i];
memcpy(cure->resref, base, 8); cure->resref[8] = 0;
memcpy(&cure->price, base + 8, 4);
}
free(data);
return 0;
}
int sto_write(STOFile* sto, const char* filename) {
FILE* f = fopen(filename, "wb");
if (!f) return -1;
fwrite(sto->signature, 1, 4, f);
fwrite(sto->version, 1, 4, f);
fwrite(&sto->type, 4, 1, f);
fwrite(&sto->name, 4, 1, f);
fwrite(&sto->flags, 4, 1, f);
fwrite(&sto->sell_markup, 4, 1, f);
fwrite(&sto->buy_markup, 4, 1, f);
fwrite(&sto->depreciation, 4, 1, f);
fwrite(&sto->steal_failure, 2, 1, f);
fwrite(&sto->capacity, 2, 1, f);
fwrite(sto->unknown1, 1, 8, f);
long purchased_offset_pos = ftell(f);
uint32_t placeholder = 0;
fwrite(&placeholder, 4, 1, f);
fwrite(&sto->purchased_count, 4, 1, f);
long items_offset_pos = ftell(f);
fwrite(&placeholder, 4, 1, f);
fwrite(&sto->items_count, 4, 1, f);
fwrite(&sto->lore, 4, 1, f);
fwrite(&sto->id_price, 4, 1, f);
char resref_pad[8] = {0};
strncpy(resref_pad, sto->rumours_tavern, 8);
fwrite(resref_pad, 1, 8, f);
long drinks_offset_pos = ftell(f);
fwrite(&placeholder, 4, 1, f);
fwrite(&sto->drinks_count, 4, 1, f);
memset(resref_pad, 0, 8);
strncpy(resref_pad, sto->rumours_temple, 8);
fwrite(resref_pad, 1, 8, f);
fwrite(&sto->room_flags, 4, 1, f);
fwrite(&sto->price_peasant, 4, 1, f);
fwrite(&sto->price_merchant, 4, 1, f);
fwrite(&sto->price_noble, 4, 1, f);
fwrite(&sto->price_royal, 4, 1, f);
long cures_offset_pos = ftell(f);
fwrite(&placeholder, 4, 1, f);
fwrite(&sto->cures_count, 4, 1, f);
fwrite(sto->unknown2, 1, 36, f);
uint32_t purchased_offset = ftell(f);
for (size_t i = 0; i < sto->purchased_count; i++) {
fwrite(&sto->purchased_categories[i], 4, 1, f);
}
uint32_t items_offset = ftell(f);
for (size_t i = 0; i < sto->items_count; i++) {
ItemForSale* item = &sto->items_for_sale[i];
memset(resref_pad, 0, 8);
strncpy(resref_pad, item->resref, 8);
fwrite(resref_pad, 1, 8, f);
fwrite(&item->exp_time, 2, 1, f);
fwrite(&item->q1, 2, 1, f);
fwrite(&item->q2, 2, 1, f);
fwrite(&item->q3, 2, 1, f);
fwrite(&item->flags, 4, 1, f);
fwrite(&item->stock, 4, 1, f);
fwrite(&item->infinite, 4, 1, f);
fwrite(&item->trigger, 4, 1, f);
fwrite(item->unknown, 1, 56, f);
}
uint32_t drinks_offset = ftell(f);
for (size_t i = 0; i < sto->drinks_count; i++) {
Drink* drink = &sto->drinks[i];
memset(resref_pad, 0, 8);
strncpy(resref_pad, drink->resref, 8);
fwrite(resref_pad, 1, 8, f);
fwrite(&drink->name_strref, 4, 1, f);
fwrite(&drink->price, 4, 1, f);
fwrite(&drink->strength, 4, 1, f);
}
uint32_t cures_offset = ftell(f);
for (size_t i = 0; i < sto->cures_count; i++) {
Cure* cure = &sto->cures[i];
memset(resref_pad, 0, 8);
strncpy(resref_pad, cure->resref, 8);
fwrite(resref_pad, 1, 8, f);
fwrite(&cure->price, 4, 1, f);
}
fseek(f, purchased_offset_pos, SEEK_SET);
fwrite(&purchased_offset, 4, 1, f);
fseek(f, items_offset_pos, SEEK_SET);
fwrite(&items_offset, 4, 1, f);
fseek(f, drinks_offset_pos, SEEK_SET);
fwrite(&drinks_offset, 4, 1, f);
fseek(f, cures_offset_pos, SEEK_SET);
fwrite(&cures_offset, 4, 1, f);
fclose(f);
return 0;
}
void sto_print_properties(STOFile* sto) {
printf("Header:\n");
printf("Signature: %s\n", sto->signature);
printf("Version: %s\n", sto->version);
printf("Type: %u\n", sto->type);
printf("Name (strref): %u\n", sto->name);
printf("Flags: %u (0x%x)\n", sto->flags, sto->flags);
printf("Sell markup: %u\n", sto->sell_markup);
printf("Buy markup: %u\n", sto->buy_markup);
printf("Depreciation rate: %u\n", sto->depreciation);
printf("Steal failure chance: %u\n", sto->steal_failure);
printf("Capacity: %u\n", sto->capacity);
printf("Unknown1: [");
for (int i = 0; i < 8; i++) printf("%u ", sto->unknown1[i]);
printf("]\n");
printf("Lore: %u\n", sto->lore);
printf("ID price: %u\n", sto->id_price);
printf("Rumours tavern: %s\n", sto->rumours_tavern);
printf("Rumours temple: %s\n", sto->rumours_temple);
printf("Room flags: %u (0x%x)\n", sto->room_flags, sto->room_flags);
printf("Price peasant: %u\n", sto->price_peasant);
printf("Price merchant: %u\n", sto->price_merchant);
printf("Price noble: %u\n", sto->price_noble);
printf("Price royal: %u\n", sto->price_royal);
printf("Unknown2: [");
for (int i = 0; i < 36; i++) printf("%u ", sto->unknown2[i]);
printf("]\n\n");
printf("Purchased Categories:\n");
for (size_t i = 0; i < sto->purchased_count; i++) {
printf("Category %zu: %u\n", i, sto->purchased_categories[i]);
}
printf("\nItems for Sale:\n");
for (size_t i = 0; i < sto->items_count; i++) {
ItemForSale* item = &sto->items_for_sale[i];
printf("Item %zu:\n", i);
printf(" resref: %s\n", item->resref);
printf(" exp_time: %u\n", item->exp_time);
printf(" q1: %u\n", item->q1);
printf(" q2: %u\n", item->q2);
printf(" q3: %u\n", item->q3);
printf(" flags: %u (0x%x)\n", item->flags, item->flags);
printf(" stock: %u\n", item->stock);
printf(" infinite: %u\n", item->infinite);
printf(" trigger: %u\n", item->trigger);
printf(" unknown: [");
for (int j = 0; j < 56; j++) printf("%u ", item->unknown[j]);
printf("]\n");
}
printf("\nDrinks:\n");
for (size_t i = 0; i < sto->drinks_count; i++) {
Drink* drink = &sto->drinks[i];
printf("Drink %zu:\n", i);
printf(" resref: %s\n", drink->resref);
printf(" name_strref: %u\n", drink->name_strref);
printf(" price: %u\n", drink->price);
printf(" strength: %u\n", drink->strength);
}
printf("\nCures:\n");
for (size_t i = 0; i < sto->cures_count; i++) {
Cure* cure = &sto->cures[i];
printf("Cure %zu:\n", i);
printf(" resref: %s\n", cure->resref);
printf(" price: %u\n", cure->price);
}
}
// Example usage:
// int main() {
// STOFile* sto = sto_create();
// sto_read(sto, "example.sto");
// sto_print_properties(sto);
// sto_write(sto, "output.sto");
// sto_free(sto);
// return 0;
// }