2023-09-22

ReOSing a Sun Netra X1


Steps for installing Solaris 10 on a sparc server over serial



I have a Sun Netra X1 running Solaris 5.8. I needed to blow it all away and install a fresh copy of Solaris 10 so I could sell it. I had done this before on a few sparcbooks and I remember it being complicated and there being a lot of conflicting information out there. So I figured I'd document the process, just in case I need to do it again.


Ubuntu on a desktop that has ports

I'm going to be using a desktop that has ethernet and a 9 pin serial port, running Ubuntu. The brand of Linux doesn't matter, but you definitely need ethernet and a serial port, and usb serial is rife with problems.


This little box is going to be the network host for everything.


First I connect the 8P8C LOM port (Lights Out Management) on the Sun to the 9 pin serial port on my linux box. Then I run the following to configure minicom


$ sudo minicom -s


The Sun manual says the port should be 9600 baud, 8N1. So I setup minicom to do just that, and then save the config and run minicom.


I plug in the server and sure enough, I see a lom> prompt in minicom!


lom> poweron


This turns on the server and it boots off the harddrive. As it boots up, I collect some information about the server.


Sun Netra X1 (UltraSPARC-IIe 400MHz)
OpenBoot 4.0, 1024 MB memory installed
ethernet: 0:3:ba:5:22:e4


Loading: /platform/SUNW,UltraAX-i2/ufsboot
Loading: /platform/sun4u/ufsboot
SunOS Release 5.8 Version Generic_108528-06 64-bit



The most important things here are the ethernet MAC address, as well as the fact that it uses the sun4u platform. I let the server continue booting until it reaches a console login prompt. I then connect a cross-over ethernet cable between the Sun and my linux machine.


Configure the net boot server



Now I focus on configuring the Linux machine to properly netboot the Sun machine. First I setup the ethernet network. I set the IP address of the Linux machine to 10.0.0.1 with a netmask of 10.255.255.255. If I don't set the netmask accordingly, bootparamd won't answer the broadcast later, and you'll get a "ERROR bpgetfile unable to access network" error when you try to do the installer.


My linux machine now has 2 networks.. the normal network over wifi, and the configuration subnet over ethernet.


First I'll need to install and configure rarpd so the net boot knows its own IP and server name.


$ sudo apt install rarpd
$ sudo vi /etc/ethers



/etc/ethers should have just the following line:
00:03:ba:05:22:e4 netboot.local


Which is the MAC address we recorded from the server boot, and a completely invented server name.


Next configure the ip address:


$ sudo vi /etc/hosts


and add the following line:
10.0.0.2 netboot.local


The net boot will then want to fetch its boot kernel from tftpd, so we'll install that next.


$ sudo apt install tftpd-hpa
$ sudo systemctl start tftpd-hpa



You'll notice we start the tftpd daemon already, but didn't with the rarpd. That's intentional, we'll run rarpd after we get the kernel.


In fact, the kernel is the next thing we need. So we download the Solaris 10 ISO from Oracle... and we'll mount it to a local endpoint in our home dir.


$ mkdir sun
$ sudo mount -o loop sol-10-u11-ga-sparc-dvd.iso sun
$ sudo cp sun/Solaris_10/Tools/Boot/platform/sun4u/inetboot /srv/tftp/0A000002



So we make a sun folder, mount the ISO on it, and then copy the proper boot kernel into the default tftp folder. Remember during the initial boot we noted that the system used sun4u? That's how we know to use it here. Finally, the /srv/tftp folder is the default folder for tftpd-hpa. We rename the file to 0A000002 which you'll notice is the hex encoding of the IP we assigned, 10.0.0.2.


Now we'll run rarpd, which checks the existence of the boot kernel, which is why we waited to run it.


$ sudo rarpd -v -b /srv/tftp eno1


Now you have to specify which ethernet interface to listen to. On older Linux, that's eth0. On Ubuntu 23, it happens to be eno1.


Next we need to specify the boot params, along with installing the boot param daemon.


$ sudo apt install bootparamd
$ sudo vi /etc/bootparams



/etc/bootparams should have the following line:
netboot.local root=10.0.0.1:/sun/Solaris_10/Tools/Boot install=10.0.0.1:/sun/ boottype=10.0.0.1:in rootopts=10.0.0.1:vers=2


This tells the server where it's boot partition and install partition should be (on NFS). The Solaris bootparams doesn't require the host ip in front of every field, but the linux bootparams does.. so do it this way instead of what you read elsewhere.


Now rpcbind intercepts all rpc broadcasts and doesn't forward them properly. So you'll also want to edit rpcbind:


$ sudo vi /etc/default/rpcbind


Find the OPTIONS line and add -r to it, so it should be OPTIONS="-w -r"


and restart rpcbind


$ sudo systemctl restart rpcbind


Now we just need to setup NFS.


$ sudo apt install nfs-kernel-server
$ sudo exportfs \*:~ -o fsid=0,ro,no_root_squash,crossmnt,no_subtree_check,sync



This shares my entire home folder over NFS. You'll probably want to stop exporting it when you're done.


Now, just because it's the most finicky service we have, I prefer to run the bootparam service in the foreground. So that's the last thing to do:


$ sudo rpc.bootparamd -d


This tells bootparam to display debug data to stdout... and it'll run until you ctl-c it.


Now, switch back over to minicom in another terminal preferably.


We'll interrupt the server and go back into lights out management by pressing the LOM interrupt keys which are hash followed by period: #.


That should return you to the LOM prompt... we'll then break out of that to openboot:


lom> break


You should now see the "ok" prompt from openboot. Finally we'll tell it to net boot and run the installer:


ok boot net - install


That should cause the system to reboot, and hit the linux machine for all the various things it needs in order to boot properly and then finally start the Solaris 10 installer.

2023-03-22

Setting up a cheap bench power supply to provide -12V, +12V and +5V with common ground.

I have a cheap bench power supply, $50 on ebay, marked RSR HY3002-3.  It has two variable outputs and one fixed 5V output.  The variable outputs are capable of 2A, the fixed of 3A.

If you put the machine in series mode, it ties the grounds together and locks the slave voltage to the master voltage.  Which turns out to be perfect for providing the various power rails needed by a variety of retro computers.

Using the master channel, set the voltage to 12V.

Then, using two paperclips, tie the ground to positive terminal of the slave, and the ground to the negative terminal of the master.  Finally tie the ground of the fixed output to the shared ground with a small wire.  The result looks like the following:


The negative terminal of the slave provides -12V, the positive terminal of the master provides +12V, the positive terminal of the fixed output provides +5V,  all three with respect to the common ground.

Thus, this inexpensive $50 bench power supply provides a 3 voltage setup that's perfect for testing retro computers, no need for multiple power supplies.

2023-02-06

World Builder Formats

 The following is documentation that I've created as a result of reverse engineering the various file formats used by World Builder games.

NOTE: All types are big-endian unless otherwise specified.

Overview

It is assumed that you can read HFS disk images. As a part of being able to read HFS disk images, you can also parse a file's resource fork.   All of this is well-documented on the internet.  The formats for standard resources (such as DLOG or MENU) are also well-documented and outside the scope of this blog post.

Main Resource Fork

The main executable contains a resource fork which holds nearly all of the assets and UI for the game. It can be identified by the filetype APPL and the creator WEDT.

Common Structures

The following structures are commonly used by the various data formats:

struct Rect {
    int16 top;     // 00
    int16 left;    // 02
    int16 bottom;  // 04
    int16 right;   // 06
}
struct String {
    uint8 length;  // 00
    uint8 data[];  // 01
}

ACHR

This contains the graphics and information on various characters in the game.  This includes the main player, or players if there are more than one.

All strings, except for initial_scene can be blank.

struct ACHR {
    uint16   offset_to_bounds;   // 00
    Graphics graphics[];         // 02
    Rect     bounds;             // offset_to_bounds
    uint8    physical_strength;  // +08
    uint8    physical_hp;        // +09
    uint8    physical_armor;     // +0a
    uint8    physical_accuracy;  // +0b
    uint8    spiritual_strength; // +0c
    uint8    spiritual_hp;       // +0d
    uint8    magic_resistance;   // +0e
    uint8    spiritual_accuracy; // +0f
    uint8    running_speed;      // +10
    uint8    reject_offers;      // +11
    uint8    follows_opponent;   // +12
    uint8    unknown;            // +13
    uint32   unknown;            // +14
    uint8    weapon1_damage;     // +18
    uint8    weapon2_damage;     // +19
    uint8    unknown;            // +1a
    uint8    is_player;          // +1b
    uint8    carry_amount;       // +1c
    uint8    return_to;          // +1d
    uint8    winning_weapon;     // +1e
    uint8    winning_magic;      // +1f
    uint8    winning_run;        // +20
    uint8    winning_offer;      // +21
    uint8    losing_weapon;      // +22
    uint8    losing_magic;       // +23
    uint8    losing_run;         // +24
    uint8    losing_offer;       // +25
    uint8    gender;             // +26
    uint8    proper_noun;        // +27
    String   initial_scene;      // +28
    String   weapon1_name;
    String   weapon1_verb;
    String   weapon2_name;
    String   weapon2_verb;
    String   initial_comment;
    String   score_hit;
    String   receive_hit;
    String   make_offer;
    String   reject_offer;
    String   accept_offer;
    String   die;
    String   initial_sound;
    String   score_hit_sound;
    String   receive_hit_sound;
    String   die_sound;
    String   weapon1_sound;
    String   weapon2_sound;
}

offset_to_bounds
    Byte offset to beginning of bounds rectangle. If there are no graphics, this will be 2.
graphics
    Visual representation of this character.  The format is documented in the Graphics section.
bounds
    The bounding box for the character's graphics.
physical_strength
    Determines damage done with physical weapons.
physical_hp
    The character's hit points.
physical_armor
    Reduces damage taken from physical attacks.
physical_accuracy
    Chances of missing a physical attack.
spiritual_strength
    Determines damage done with magic
spiritual_hp
    The character's mental hit points.
magic_resistance
    Reduces damage taken from magical attacks.
spiritual_accuracy
    Chances of missing a magical attack.
running_speed
    Used to check if you can flee an enemy, or chase one down.
reject_offers
    Maximum price of offers to reject.  Offers above this will be accepted.
follows_opponent
    Chance of following you if you run.
weapon1_damage
    Damage caused by first innate weapon (punches or claws, etc).
weapon2_damage
    Damage caused by second innate weapon (kicks or fire breath, etc)
is_player
    There can be multiple players.  In that case, the game will chose a random character marked with is_player as the main player character.
    0: enemy
    1: player
carry_amount
    Number of items you can have in your inventory.
return_to
    Scene to teleport the character to when it dies.
    0: STORAGE@
    1: RANDOM@
    otherwise: use initial_scene
winning_weapon
    Odds of using a weapon if the mob has more HP than you.
winning_magic
    Odds of using magic when winning.
winning_run
    Odds of fleeing when winning
winning_offer
    Odds of making an offer when winning.
losing_weapon
    Odds of using a weapon if mob has less HP than you.
losing_magic
    Odds of using magic when losing
losing_run
    Odds of fleeing when losing
losing_offer
    Odds of making an offer when losing.
gender
    0: male
    1: female
    2: it
proper_noun
    Whether or not a character's name is a proper noun.  "A robot attacks" vs "Robot attacks".
initial_scene
    The name of the initial scene for this character. Can also be STORAGE@ or RANDOM@.
    If so, then return_to should be set accordingly.
weapon1_name
    The name of the first innate weapon. (Claws, fire, etc).
weapon1_verb
    The verb to use for that weapon. ("breathe" fire, etc.)
weapon2_name
    The name of the second innate weapon.
weapon2_verb
    The verb to use for that weapon.
initial_comment
    Message to print when mob first appears.
score_hit
    Message to print when mob scores a hit.
receive_hit
    Message to print when mob gets hit.
make_offer
    Message to print when mob makes an offer.
reject_offer
    Message to print when mob rejects your offer.
accept_offer
    Message to print when mob accepts your offer.
die
    Message to print when mob dies.
initial_sound
    Sound to play when mob first appears.
score_hit_sound
    Sound to play when mob scores a hit
receive_hit_sound
    Sound to play when mob gets hit.
die_sound
    Sound to play when mob dies
weapon1_sound
    Sound to play when mob uses first innate weapon.
weapon2_sound
    Sound to play when mob uses second innate weapon.

AOBJ

This contains the graphics and information for various objects in the game.

struct AOBJ {
    uint16   offset_to_bounds;  // 00
    Graphics graphics[];        // 02
    Rect     bounds;            // offset_to_bounds
    uint8    plural;            // +08
    uint32   unknown;           // +09
    uint8    accuracy;          // +0d
    uint8    value;             // +0e
    uint8    type;              // +0f
    uint8    damage;            // +10
    uint8    effect;            // +11
    int16    uses;              // +12
    uint8    return_to;         // +14
    uint8    unknown;           // +15
    String   owner;             // +16
    String   pickup_message;
    String   operative_verb;
    String   failure_message;
    String   successful_use;
    String   operative_sound;
}
enum Types {
    unknown         = 0,
    regular_weapon  = 1,
    thrown_weapon   = 2,
    magical_object  = 3,
    helmet          = 4,
    shield          = 5,
    chest_armor     = 6,
    spiritual_armor = 7,
    mobile_object   = 8,
    immobile_object = 9,
}
enum Effects {
    cause_physical_damage  = 0,
    cause_spiritual_damage = 1,
    cause_both_damage      = 2,
    heal_physical_damage   = 3,
    heal_spiritual_damage  = 4,
    heal_both_damage       = 5,
    freeze_opponent        = 6,
}

offset_to_bounds
    Byte offset to beginning of bounds rectangle. If there are no graphics, this will be 2.
graphics
    Visual representation of this object. The format is described in the Graphics section.
bounds
    Bounding box for the object's graphics.
plural
    Whether or not this object is plural.  "You see an arrow" vs "You see some arrows".
accuracy
    Accuracy modifier if this is a weapon.
value
    Price of this object, for making offers.
type
    Type of object, Immobile_objects cannot be moved, they're used to decorate a scene.
damage
    If this is a weapon, this is the damage it causes. If this is armor, then this is the amount of protection in provides.  If this is a magical object, then this is the spell armor.
effect
    If this is a magical device, this determines what it does.
uses
    Number of times this object can be used before it breaks. -1 : indestructible.
return_to
    Where to respawn this object when it breaks.
    0: STORAGE@
    otherwise: Spawn in a random scene.
owner
    Initial owner of this object. Might be the name of a scene or a character. Can also be STORAGE@ or RANDOM@. If random, that means a random scene, not a random character.
pickup_message
    Message to print if the item is picked up
operative_verb
    Verb to use for this object. "Eat" a cake, for example.
failure_message
    Message to print if item breaks.
successful_use
    Message to print if item is used successfully.
operative_sound
    Sound to play when item is used.

ASCN

This contains the graphics and information for every room or "scene" in the game. It also determines the size and location of the graphics window.

struct ASCN {
    uint16   offset_to_bounds;    // 00
    Graphics graphics[];          // 02
    Rect     bounds;              // offset_to_bounds
    uint16   map_y;               // +08
    uint16   map_x;               // +0a
    uint8    blocked_north;       // +0c
    uint8    blocked_south;       // +0d
    uint8    blocked_east;        // +0e
    uint8    blocked_west;        // +0f
    uint16   frequency;           // +10
    uint16   loop_type;           // +12
    String   north_text;          // +14
    String   south_text;
    String   east_text;
    String   west_text;
    String   ambient_sound;
}

offset_to_bounds
    Byte offset to beginning of bounding box. If there are no graphics, this will be 2.
graphics
    Visual representation of this scene. The format is documented in the Graphics section.
bounds
    Bounding box for the main graphics window.
map_y
    Y coordinate (0-49) of this scene in the world map.
map_x
    X coordinate (0-49) of this scene in the world map.
blocked_north
    Whether or not the north exit is blocked
blocked_south
    Whether or not the south exit is blocked.
blocked_east
    Whether or not the east exit is blocked.
blocked_west
    Whether or not the west exit is blocked.
frequency
    How often to play the ambient sound while in this scene. The actual meaning of the value depends on loop_type.
loop_type
    How to loop the ambient sound. 0 means play the sound frequency times per hour, with the first time being when you enter the room. Otherwise, play the ambient sound randomly.  In that case, there is a frequency out of 3,000 chance of playing for every second you remain in the room.
north_text
    Message to print if you try to go north while it is blocked.
south_text
    Message to print if you try to go south while it is blocked.
east_text
    Message to print if you try to go east while it is blocked.
west_text
    Message to print if you try to go west while it is blocked.
ambient_sound
    Sound to play as ambient noise.  See frequency and loop_type for how to play this.

ATXT

This contains the intro text for a room or scene, as well as information on the text window's location and font.  There is one ATXT for every ASCN.

struct ATXT {
    Rect    bounds;  // 00
    uint16  fontid;  // 08
    uint16  size;    // 0a
    char    text[];  // 0c
}

bounds
    The bounding box for the text window
fontid
    The Macintosh font ID for the font to use
size
    Font size (in points) to use
text
    Text to show. This just runs until the end of the resource.

ASND

This contains a single digitized sound.

struct ASND {
    uint16  channels;    // 00
    uint16  loops;       // 02
    int8    deltas[16];  // 04
    uint8   data[];      // 14
}

channels
    I believe this is the number of channels. Every sound I've found is always 1 (mono) however.
loops
    The number of times to loop this sound.
deltas
    A 16-byte array of deltas to use when parsing the sample data.
data
    Each byte represents 2 samples.  The high nibble is the first sample, the low nibble is the second. You use each nibble to look up a delta in the deltas array, and add it to the current sample value. The resulting byte is the next sample.  The sample value starts at $80.

The final sample is an 8-bit unsigned PCM array.  The sound is always played back at 22.2545 kHz, but stored at half that.  The way the engine compensates for this is to store last + delta in buffer[i + 1] and last + delta / 2 in buffer[i], essentially interpolating the audio.

ACOD / GCOD

This contains the code for the scene or global context.  There is an ACOD for every ASCN in the game, and a single GCOD resource.  The GCOD resource is only run when the ACOD resource for the scene exits without hitting an EXIT opcode.

These code resources are run whenever the user does something, like clicking on an object or typing a command.  Whenever a user changes scenes, a fake LOOK command is issued.

struct ACOD {
    Rect    bounds;  // 00
    uint16  fontid;  // 02
    uint16  size;    // 0a
    char    code[];  // 0c
}

bounds
    Bounding box for the script window.  Only used in the world editor, since the script window isn't visible in game.
fontid
    Macintosh font ID for the script window.
size
    Font size (in points) for the script window.
code
    The code to run for this script, see the section below on Code format.

Code Format

The code format is basically just a tokenized scripting language. You can translate the tokens back into their original instructions for displaying the scripting window.  The tokens are listed below, along with how each instruction changes the tab depth (which is used both visually, as well as to determine execution flow).

$80    "IF ("      depth++
$81    "="
$82    "<"
$83    ">"
$84    ") AND ("
$85    ") OR ("
$87    "EXIT\n"    --depth
$88    "END\n"     --depth
$89    "MOVE ("
$8a    ") TO ("
$8b    "PRINT("
$8c    "SOUND("
$8e    "LET("
$8f    "+"
$90    "-"
$91    "*"
$92    "/"
$93    "="
$94    "!"
$95    "MENU("
$a0    "TEXT$"
$a1    "CLICK$"
$b0    "VISITS#"
$b1    "RANDOM#"
$b2    "LOOP#"
$b3    "VICTORY#"
$b4    "BADCOPY#"
$c0    "STORAGE@"
$c1    "SCENE@"
$c2    "PLAYER@"
$c3    "MONSTER@"
$c4    "RANDOMSCN@"
$c5    "RANDOMCHR@"
$c6    "RANDOMOBJ@"
$d0    "PHYS.STR.BAS#"
$d1    "PHYS.HIT.BAS#"
$d2    "PHYS.ARM.BAS#"
$d3    "PHYS.ACC.BAS#"
$d4    "SPIR.STR.BAS#"
$d5    "SPIR.HIT.BAS#"
$d6    "SPIR.ARM.BAS#"
$d7    "SPIR.ACC.BAS#"
$d8    "PHYS.SPE.BAS#"
$e0    "PHYS.STR.CUR#"
$e1    "PHYS.HIT.CUR#"
$e2    "PHYS.ARM.CUR#"
$e3    "PHYS.ACC.CUR#"
$e4    "SPIR.STR.CUR#"
$e5    "SPIR.HIT.CUR#"
$e6    "SPIR.ARM.CUR#"
$e7    "SPIR.ACC.CUR#"
$e8    "PHYS.SPE.CUR#"
$fd    ")\n"
$fe    ") THEN\n"        depth++
$ff    global

Globals are stored in a single byte that follows the $ff opcode.  The names of the globals are represented as A0-A9, B0-B9, C0-C9, and so on.  Thus, globals can only be from $00 to $ea, since $ea is Z9.

Any character in the script without the high-bit set represents itself.  So strings and such are just stored in plaintext.

NOTE: This also includes any constant numbers.  42 is stored as the string "42".

Graphics Data Format

The graphics data found in various assets, including the background, all follow the same format.  It is comprised of a list of shapes, with their corresponding patterns and pens.  The patterns are indices into the PAT# resource, with the first pattern identified with the index 1.

You will need to use the quickdraw routines to accurately draw these shapes.

There is no END code, so just keep parsing and drawing shapes until you hit the end of the graphics section.

Each shape begins with the same header:

struct Shape {
    uint8  fillPattern;  // 00
    uint8  penSize;      // 01
    uint8  penPattern;   // 02
    uint8  shapeType;    // 03
}
enum ShapeType {
    rectangle   = 4,
    roundedRect = 8,
    oval        = 0xc,
    polygon     = 0x10,
    freeHand    = 0x14,
    bitmap      = 0x18,
}

fillPattern
    This is the pattern to use as a fill for the shape.  $1e is reserved to represent no fill at all.
penSize
    This is the size (in pixels) of the pen to use as a stroke for the shape.  $00 is reserved to represent no stroke.
penPattern
    This is the pattern to use for the stroke, $1e is reserved to represent no stroke.
shapeType
    This indentifies which shape to draw.  The shape-specific data follows the header.

Rectangle ($04)

Draws a rectangle at the provided coordinates.  These may be partially or even entirely offscreen.

struct Rectangle {
    Shape header;  // 00
    Rect  bounds;  // 04
}

Rounded Rectangle ($08)

Draws a rounded rectangle with the provided coordinates and corner radius.  This may be partially or entirely offscreen.

struct RoundedRect {
    Shape  header;  // 00
    Rect   bounds;  // 04
    uint16 radius;  // 0c
}

Oval ($0c)

Draws an oval in the provided bounds.  This may be partially or entirely offscreen.

struct Oval {
    Shape header;  // 00
    Rect  bounds;  // 04
}

Polygon ($10)

Draw a polygon

struct Polygon {
    Shape  header;     // 00
    uint16 smoothness; // 04
    uint16 length;     // 06
    Rect   bounds;     // 08
    int16  start_y;    // 10
    int16  start_x;    // 12
    int8   data[];     // 14
}

The bounds aren't used for drawing, but for highlighting the shape in the world editor.  The length determines the size of all the data following the length field, including the length field.

Smoothness determines how many times to subdivide the polygon before drawing it.

The polygon data itself uses a light compression algorithm.  The starting point for the polygon is given in start_x and start_y.  Loop through the data, and if the value is -128, then the next int16 is a new Y coordinate.  Otherwise, add the value to the current Y coordinate.  The next value is for the X coordinate, and so on until the end of the array.

FreeHand ($14)

This shape was actually created using a different drawing tool in the editor, but the storage and appearance is identical to the Polygon.

Bitmap ($18)

Draws a bitmap in the viewport.

struct Bitmap {
    Shape  header;    // 00
    uint16 length;    // 04
    Rect   bounds;    // 06
    uint8  packed[];  // 0a
}

The width of the bitmap is always a multiple of 8.  To unpack the data, you must calculate how many bytes each expanded row will be.  You can simply divide the width by 8 for this.

Next, you will run the algorithm below for each scanline.  Run until you've expanded the correct number of bytes.

The algorithm is a simple RLE format.  The first byte is the opcode.  If it is $80, do nothing and move onto the next opcode.  Otherwise, if the high bit is set, copy the next byte into the output -opcode + 1 times.  If the high bit is not set, copy opcode + 1 bytes from the input into the output.

This is widely known as the PackBits algorithm,  You'll see it referenced as the TIFF or PICT PackBits algorithm.

And that's everything I know about the World Builder file formats.