Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ RUN dnf copr enable -y sisyphus1813/odin-lang \
libXinerama-devel-1.1.5 \
libxkbcommon-devel-1.13.1 \
libXrandr-devel-1.5.4 \
lldb-22.1.8 \
mesa-libGL-devel-26.1.4 \
odin-lang-2026.07 \
ols-2026.05 \
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ifneq (,$(wildcard ./.env))
export
endif

.PHONY: all build clean coverage format lint run test
.PHONY: all build clean coverage debug format lint run test

all: lint test build

Expand All @@ -21,6 +21,10 @@ coverage:
odin build . -build-mode:test -debug
kcov --dump-summary --exclude-pattern=_test.odin --include-path=. .kcov spinward

debug:
odin build . -debug
lldb spinward

format:
odinfmt . -w

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@

# Spinward

![Build](https://img.shields.io/github/actions/workflow/status/theomund/spinward/linux.yml?style=for-the-badge&logo=linux&logoColor=white)
![License](https://img.shields.io/github/license/theomund/spinward?style=for-the-badge&logo=mozilla&logoColor=white)
![Toolchain](https://img.shields.io/badge/toolchain-dev--2026--07a-blue?style=for-the-badge&logo=odin&logoColor=white)

Traveller map viewer built with Odin.
6 changes: 5 additions & 1 deletion app.odin
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ package main
import rl "vendor:raylib"

run :: proc() -> Error {
rl.SetConfigFlags({.MSAA_4X_HINT})

rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
defer rl.CloseWindow()

Expand All @@ -17,7 +19,9 @@ run :: proc() -> Error {
}

camera := new_camera()
sector := new_sector("Spinward Marches")

sector := new_sector("assets/Spinward Marches.tab") or_return
defer delete_sector(sector)

for !rl.WindowShouldClose() {
rl.BeginDrawing()
Expand Down
443 changes: 443 additions & 0 deletions assets/Spinward Marches.tab

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion constants.odin
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

package main

HEX_SIZE :: 24
FONT_SIZE :: 16
FONT_SPACING :: 2

HEX_SIZE :: 64

ODD_OFFSET :: -1

Expand All @@ -16,3 +19,5 @@ WINDOW_TITLE :: "Spinward"

SECTOR_COLUMNS :: 32
SECTOR_ROWS :: 40

WORLD_SIZE :: 12
9 changes: 8 additions & 1 deletion hex.odin
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package main

import "core:fmt"
import "core:math"
import rl "vendor:raylib"

new_hex :: proc(q, r, s: f32) -> Hex {
assert(math.round(q + r + s) == 0)
Expand All @@ -18,7 +19,7 @@ new_hex :: proc(q, r, s: f32) -> Hex {
hex_index :: proc(hex: Hex) -> string {
offset := qoffset_from_cube(hex)

return fmt.tprintf("%02d%02d", i32(offset.x + 1), i32(offset.y + 1))
return fmt.aprintf("%02d%02d", i32(offset.x + 1), i32(offset.y + 1))
}

hex_lerp :: proc(a, b: Hex, t: f32) -> Hex {
Expand Down Expand Up @@ -55,3 +56,9 @@ hex_round :: proc(hex: Hex) -> Hex {

return new_hex(q, r, s)
}

draw_hex :: proc(layout: Layout, hex: Hex, color: rl.Color) {
center := hex_to_pixel(layout, hex)

rl.DrawPolyLines(center, 6, HEX_SIZE, 0, color)
}
19 changes: 15 additions & 4 deletions hex_test.odin
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,21 @@ test_new_hex :: proc(t: ^testing.T) {

@(test)
test_hex_index :: proc(t: ^testing.T) {
testing.expect_value(t, hex_index(new_hex(0, 0, 0)), "0101")
testing.expect_value(t, hex_index(new_hex(1, 0, -1)), "0201")
testing.expect_value(t, hex_index(new_hex(0, 1, -1)), "0102")
testing.expect_value(t, hex_index(new_hex(1, 1, -2)), "0202")
index := hex_index(new_hex(0, 0, 0))
testing.expect_value(t, index, "0101")
delete(index)

index = hex_index(new_hex(1, 0, -1))
testing.expect_value(t, index, "0201")
delete(index)

index = hex_index(new_hex(0, 1, -1))
testing.expect_value(t, index, "0102")
delete(index)

index = hex_index(new_hex(1, 1, -2))
testing.expect_value(t, index, "0202")
delete(index)
}

@(test)
Expand Down
7 changes: 7 additions & 0 deletions main.odin
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ package main
import "core:os"

main :: proc() {
when ODIN_DEBUG {
tracker := new_tracker()
context = tracker.ctx

defer delete_tracker(tracker)
}

if err := run(); err != nil {
os.exit(1)
}
Expand Down
45 changes: 45 additions & 0 deletions reader.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package main

import "core:encoding/csv"
import "core:os"
import "core:strings"

new_reader :: proc() -> Reader {
return {
comma = '\t',
comment = '#',
fields_per_record = -1,
reuse_record = true,
reuse_record_buffer = true,
}
}

destroy_reader :: proc(reader: ^Reader) {
csv.reader_destroy(reader)
}

read_sector :: proc(reader: ^Reader, path: string, systems: map[string]System) -> Error {
data := os.read_entire_file(path, context.allocator) or_return
defer delete(data)

csv.reader_init_with_string(reader, string(data))

for record, _, err in csv.iterator_next(reader) {
if err != nil {
return err
}

if system := &systems[record[2]]; system != nil {
system.allegiance = strings.clone_to_cstring(record[9])
system.name = strings.clone_to_cstring(record[3])
}
}

return nil
}
48 changes: 30 additions & 18 deletions sector.odin
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
package main

import "core:math"
import "core:path/filepath"
import "core:strings"
import rl "vendor:raylib"

new_sector :: proc(name: string, origin: Point = {0, 0}) -> Sector {
hexes: map[string]Hex
new_sector :: proc(path: string, origin: Point = {0, 0}) -> (sector: Sector, err: Error) {
name := strings.clone_to_cstring(filepath.short_stem(path))

systems: map[string]System

left := origin.x
right := origin.x + SECTOR_COLUMNS
Expand All @@ -23,17 +26,29 @@ new_sector :: proc(name: string, origin: Point = {0, 0}) -> Sector {
for r := top - q_offset; r <= bottom - q_offset; r += 1 {
hex := new_hex(q, r, -q - r)
index := hex_index(hex)
hexes[index] = hex
systems[index] = new_system(hex)
}
}

reader := new_reader()
defer destroy_reader(&reader)

read_sector(&reader, path, systems)

layout := new_layout(flat_orientation(), origin, {HEX_SIZE, HEX_SIZE})

return {name, hexes, layout}
return {name, systems, layout}, nil
}

delete_sector :: proc(sector: Sector) {
delete(sector.hexes)
delete(sector.name)

for index, system in sector.systems {
delete(index)
delete_system(system)
}

delete(sector.systems)
}

contains_hex :: proc(hex: Hex) -> bool {
Expand All @@ -43,24 +58,21 @@ contains_hex :: proc(hex: Hex) -> bool {
}

draw_sector :: proc(sector: Sector, camera: rl.Camera2D) {
for _, hex in sector.hexes {
draw_hex(sector.layout, hex, rl.DARKGRAY)
for index, system in sector.systems {
clone := strings.clone_to_cstring(index)
defer delete(clone)

draw_system(sector, clone, system)
}

for _, system in sector.systems {
draw_border(sector, system)
}

position := rl.GetScreenToWorld2D(rl.GetMousePosition(), camera)
hovered := pixel_to_hex_rounded(sector.layout, position)

if contains_hex(hovered) {
index := strings.clone_to_cstring(hex_index(hovered))

rl.DrawText(index, i32(position.x), i32(position.y - 8), 8, rl.WHITE)

draw_hex(sector.layout, hovered, rl.RED)
draw_hex(sector.layout, hovered, rl.YELLOW)
}
}

draw_hex :: proc(layout: Layout, hex: Hex, color: rl.Color) {
center := hex_to_pixel(layout, hex)

rl.DrawPolyLines(center, 6, HEX_SIZE, 0, color)
}
7 changes: 5 additions & 2 deletions sector_test.odin
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import "core:testing"

@(test)
test_new_sector :: proc(t: ^testing.T) {
sector := new_sector("Foo")
sector, err := new_sector("assets/Spinward Marches.tab")
if err != nil {
testing.fail(t)
}
defer delete_sector(sector)

testing.expect_value(t, sector.name, "Foo")
testing.expect_value(t, sector.name, "Spinward Marches")
testing.expect_value(t, sector.layout.origin, Point{0, 0})
testing.expect_value(t, sector.layout.size, Point{HEX_SIZE, HEX_SIZE})
}
71 changes: 71 additions & 0 deletions system.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package main

import rl "vendor:raylib"

new_system :: proc(hex: Hex) -> System {
return {hex = hex}
}

delete_system :: proc(system: System) {
if system.name != "" {
delete(system.name)
}

if system.allegiance != "" {
delete(system.allegiance)
}
}

draw_border :: proc(sector: Sector, system: System) {
color: rl.Color

switch system.allegiance {
case "DaCf":
color = rl.WHITE
case "ImDd":
color = rl.RED
case "SwCf":
color = rl.DARKBLUE
case "ZhIN":
color = rl.BLUE
}

draw_hex(sector.layout, system.hex, color)
}

draw_system :: proc(sector: Sector, index: cstring, system: System) {
draw_hex(sector.layout, system.hex, rl.DARKGRAY)

center := hex_to_pixel(sector.layout, system.hex)

if system.name != "" {
rl.DrawCircle(i32(center.x), i32(center.y), WORLD_SIZE, rl.BLUE)
}

font := rl.GetFontDefault()
name_size := rl.MeasureTextEx(font, system.name, FONT_SIZE, FONT_SPACING)
index_size := rl.MeasureTextEx(font, index, FONT_SIZE, FONT_SPACING)

rl.DrawTextEx(
font,
system.name,
{center.x - (name_size.x / 2), (center.y - (HEX_SIZE / 2)) - (name_size.y / 2)},
FONT_SIZE,
FONT_SPACING,
rl.WHITE,
)
rl.DrawTextEx(
font,
index,
{center.x - (index_size.x / 2), (center.y + (HEX_SIZE / 2)) - (index_size.y / 2)},
FONT_SIZE,
FONT_SPACING,
rl.DARKGRAY,
)
}
33 changes: 33 additions & 0 deletions tracker.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package main

import "base:runtime"
import "core:fmt"
import "core:mem"

new_tracker :: proc() -> Tracker {
allocator := new(mem.Tracking_Allocator)
ctx := runtime.default_context()

mem.tracking_allocator_init(allocator, ctx.allocator)

ctx.allocator = mem.tracking_allocator(allocator)

return {allocator, ctx}
}

delete_tracker :: proc(tracker: Tracker) {
if len(tracker.allocator.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed: ===\n", len(tracker.allocator.allocation_map))
for _, entry in tracker.allocator.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}

mem.tracking_allocator_destroy(tracker.allocator)
}
Loading