Cubyz/src/gui/windows/save_selection.zig
IntegratedQuantum aae66ea77f
Update Zig to use the new Writergate version (#1664)
For reference: https://github.com/ziglang/zig/pull/24329

some commits have been extracted from #1583, but the x86_64 backend has
been disabled due to its horrible performance.

Remaining work:
- [x] Wait for official builds on ziglang.org and upload them to our
repository
- [x] Add workaround for https://github.com/ziglang/zig/pull/24466
- [x] Fix TODO comment about ANSI support in stdout
- [x] Check for compile-time performance changes → it went from 13.1 to
11.9 seconds 🎉
2025-07-15 23:00:29 +02:00

174 lines
6.1 KiB
Zig

const std = @import("std");
const main = @import("main");
const ConnectionManager = main.network.ConnectionManager;
const settings = main.settings;
const Vec2f = main.vec.Vec2f;
const NeverFailingAllocator = main.heap.NeverFailingAllocator;
const Texture = main.graphics.Texture;
const gui = @import("../gui.zig");
const GuiComponent = gui.GuiComponent;
const GuiWindow = gui.GuiWindow;
const Button = @import("../components/Button.zig");
const HorizontalList = @import("../components/HorizontalList.zig");
const Label = @import("../components/Label.zig");
const TextInput = @import("../components/TextInput.zig");
const VerticalList = @import("../components/VerticalList.zig");
pub var window = GuiWindow{
.contentSize = Vec2f{128, 256},
};
const padding: f32 = 8;
const width: f32 = 128;
var buttonNameArena: main.heap.NeverFailingArenaAllocator = undefined;
pub var needsUpdate: bool = false;
var deleteIcon: Texture = undefined;
var fileExplorerIcon: Texture = undefined;
const WorldInfo = struct {
lastUsedTime: i64,
name: []const u8,
fileName: []const u8,
};
var worldList: main.ListUnmanaged(WorldInfo) = .{};
pub fn init() void {
deleteIcon = Texture.initFromFile("assets/cubyz/ui/delete_icon.png");
fileExplorerIcon = Texture.initFromFile("assets/cubyz/ui/file_explorer_icon.png");
}
pub fn deinit() void {
deleteIcon.deinit();
fileExplorerIcon.deinit();
}
pub fn openWorld(name: []const u8) void {
const clientConnection = ConnectionManager.init(0, false) catch |err| {
std.log.err("Encountered error while opening connection: {s}", .{@errorName(err)});
return;
};
std.log.info("Opening world {s}", .{name});
main.server.thread = std.Thread.spawn(.{}, main.server.start, .{name, clientConnection.localPort}) catch |err| {
std.log.err("Encountered error while starting server thread: {s}", .{@errorName(err)});
return;
};
main.server.thread.?.setName("Server") catch |err| {
std.log.err("Failed to rename Server thread: {s}", .{@errorName(err)});
};
while(!main.server.running.load(.acquire)) {
std.Thread.sleep(1_000_000);
}
clientConnection.world = &main.game.testWorld;
const ipPort = std.fmt.allocPrint(main.stackAllocator.allocator, "127.0.0.1:{}", .{main.server.connectionManager.localPort}) catch unreachable;
defer main.stackAllocator.free(ipPort);
main.game.world = &main.game.testWorld;
main.game.testWorld.init(ipPort, clientConnection) catch |err| {
std.log.err("Encountered error while opening world: {s}", .{@errorName(err)});
};
for(gui.openWindows.items) |openWindow| {
gui.closeWindowFromRef(openWindow);
}
gui.openHud();
}
fn openWorldWrap(index: usize) void { // TODO: Improve this situation. Maybe it makes sense to always use 2 arguments in the Callback.
openWorld(worldList.items[index].fileName);
}
fn deleteWorld(index: usize) void {
main.gui.closeWindow("delete_world_confirmation");
main.gui.windowlist.delete_world_confirmation.setDeleteWorldName(worldList.items[index].fileName);
main.gui.openWindow("delete_world_confirmation");
}
fn openFolder(index: usize) void {
const path = std.fmt.allocPrint(main.stackAllocator.allocator, "saves/{s}", .{worldList.items[index].fileName}) catch unreachable;
defer main.stackAllocator.free(path);
main.files.openDirInWindow(path);
}
pub fn update() void {
if(needsUpdate) {
needsUpdate = false;
onClose();
onOpen();
}
}
pub fn onOpen() void {
buttonNameArena = main.heap.NeverFailingArenaAllocator.init(main.globalAllocator);
const list = VerticalList.init(.{padding, 16 + padding}, 300, 8);
list.add(Label.init(.{0, 0}, width, "**Select World**", .center));
list.add(Button.initText(.{0, 0}, 128, "Create New World", gui.openWindowCallback("save_creation")));
readingSaves: {
var dir = std.fs.cwd().makeOpenPath("saves", .{.iterate = true}) catch |err| {
list.add(Label.init(.{0, 0}, 128, "Encountered error while trying to open saves folder:", .center));
list.add(Label.init(.{0, 0}, 128, @errorName(err), .center));
break :readingSaves;
};
defer dir.close();
var iterator = dir.iterate();
while(iterator.next() catch |err| {
list.add(Label.init(.{0, 0}, 128, "Encountered error while iterating over saves folder:", .center));
list.add(Label.init(.{0, 0}, 128, @errorName(err), .center));
break :readingSaves;
}) |entry| {
if(entry.kind == .directory) {
const worldInfoPath = std.fmt.allocPrint(main.stackAllocator.allocator, "saves/{s}/world.zig.zon", .{entry.name}) catch unreachable;
defer main.stackAllocator.free(worldInfoPath);
const worldInfo = main.files.readToZon(main.stackAllocator, worldInfoPath) catch |err| {
std.log.err("Couldn't open save {s}: {s}", .{worldInfoPath, @errorName(err)});
continue;
};
defer worldInfo.deinit(main.stackAllocator);
worldList.append(main.globalAllocator, .{
.fileName = main.globalAllocator.dupe(u8, entry.name),
.lastUsedTime = worldInfo.get(i64, "lastUsedTime", 0),
.name = main.globalAllocator.dupe(u8, worldInfo.get([]const u8, "name", entry.name)),
});
}
}
}
std.sort.insertion(WorldInfo, worldList.items, {}, struct {
fn lessThan(_: void, lhs: WorldInfo, rhs: WorldInfo) bool {
return rhs.lastUsedTime -% lhs.lastUsedTime < 0;
}
}.lessThan);
for(worldList.items, 0..) |worldInfo, i| {
const row = HorizontalList.init();
row.add(Button.initText(.{0, 0}, 128, worldInfo.name, .{.callback = &openWorldWrap, .arg = i}));
row.add(Button.initIcon(.{8, 0}, .{16, 16}, fileExplorerIcon, false, .{.callback = &openFolder, .arg = i}));
row.add(Button.initIcon(.{8, 0}, .{16, 16}, deleteIcon, false, .{.callback = &deleteWorld, .arg = i}));
row.finish(.{0, 0}, .center);
list.add(row);
}
list.finish(.center);
window.rootComponent = list.toComponent();
window.contentSize = window.rootComponent.?.pos() + window.rootComponent.?.size() + @as(Vec2f, @splat(padding));
gui.updateWindowPositions();
}
pub fn onClose() void {
for(worldList.items) |worldInfo| {
main.globalAllocator.free(worldInfo.fileName);
main.globalAllocator.free(worldInfo.name);
}
worldList.clearAndFree(main.globalAllocator);
buttonNameArena.deinit();
if(window.rootComponent) |*comp| {
comp.deinit();
}
}