entity per world

This commit is contained in:
Patedam committed 2026-10-16 21:33:57 -04:00
1 parent 16ba641d8a
commit b70e2d21d2
15 files changed
+694 -19

No files matched your search

+42
View File
@@ -21,6 +21,48 @@ internal void serialize(Archive& ar, uint16 /*version*/, Entity& entity)
SERIALIZE(ar, position, entity.position); SERIALIZE(ar, position, entity.position);
} }
String format_entity_id(NonNullPtr<Arena> arena, EntityID entity_id)
{
// Format:
// U = UserID
// LC = Local Entity Counter
// UU_LCLCLCLCLCLCLC
uint8 user_id = get_user_id(entity_id);
uint64 counter = get_local_counter(entity_id);
constexpr size_t kBufferSize = 18;
char* buffer = ArenaPushArray<char>(arena, kBufferSize);
Assert(buffer);
juliet_snprintf(buffer, kBufferSize, "%02X_%014llX", (unsigned int)user_id, counter);
return { buffer, kBufferSize - 1 };
}
bool parse_entity_id(String str, EntityID& out_id)
{
bool result = false;
out_id = kInvalid_entity_id;
if (!(str.Size != 17 || str.Str == nullptr || str.Str[2] != '_'))
{
char user_hex[3]{ str.Str[0], str.Str[1], '\0' };
char* end = user_hex;
uint8 user_id = strtoul(user_hex, &end, 16);
if (end != user_hex && user_id != kInvalid_user_id)
{
char* counter_hex = str.Str + 3;
end = counter_hex;
uint64 counter = strtoull(counter_hex, &end, 16);
if (end != counter_hex && counter < kEntityID_user_id_mask)
{
out_id = make_entity_id(user_id, counter);
result = true;
}
}
}
return result;
}
const Class* resolve_entity_class(uint8 kind, uint32 crc) const Class* resolve_entity_class(uint8 kind, uint32 crc)
{ {
Assert(kind <= ENTITY(Count)); Assert(kind <= ENTITY(Count));
+32
View File
@@ -30,6 +30,32 @@ concept EntityConcept = requires(EntityType entity) {
requires std::same_as<decltype(entity.base), Entity*>; requires std::same_as<decltype(entity.base), Entity*>;
}; };
inline constexpr EntityID make_entity_id(uint8 user_id, uint64 local_counter)
{
Assert(user_id != kInvalid_user_id);
Assert((local_counter & kEntityID_user_id_mask) == kInvalid_entity_id);
EntityID result = kInvalid_user_id;
result = ((EntityID)user_id << kEntityID_user_id_shift) | (local_counter & kEntityID_user_id_mask);
return result;
}
inline constexpr uint8 get_user_id(EntityID entity_id)
{
return (((uint8)entity_id & kEntityID_user_id_mask) << kEntityID_user_id_shift);
}
inline constexpr uint64 get_local_counter(EntityID entity_id)
{
return (uint64)entity_id & kEntityID_user_id_mask;
}
inline constexpr bool is_entity_id_valid(EntityID entity_id)
{
bool result = entity_id != kInvalid_entity_id;
return result;
}
template <typename EntityType> template <typename EntityType>
requires EntityConcept<EntityType> requires EntityConcept<EntityType>
[[nodiscard]] bool IsA(const Entity* entity) [[nodiscard]] bool IsA(const Entity* entity)
@@ -66,6 +92,12 @@ template <typename EntityType>
void serialize(Archive& ar, NonNullPtr<Entity> entity); void serialize(Archive& ar, NonNullPtr<Entity> entity);
JULIET_API uint8 get_local_user_id();
JULIET_API String format_entity_id(NonNullPtr<Arena> arena, EntityID id);
JULIET_API bool parse_entity_id(String str, EntityID& outId);
[[nodiscard]] const Class* find_class_by_name(String name); [[nodiscard]] const Class* find_class_by_name(String name);
[[nodiscard]] const Class* resolve_entity_class(uint8 kind, uint32 crc); [[nodiscard]] const Class* resolve_entity_class(uint8 kind, uint32 crc);
+25
View File
@@ -7,6 +7,7 @@
#include <Entity/entity_types.h> #include <Entity/entity_types.h>
#include <game.h> #include <game.h>
#include <Graphics/MeshRenderer.h> #include <Graphics/MeshRenderer.h>
#include "Engine/user.h"
EntityID EntityManager::ID = 0; EntityID EntityManager::ID = 0;
@@ -45,6 +46,17 @@ EntityManager& GetEntityManager()
return *entityManager; return *entityManager;
} }
EntityID generate_next_entity_id(EntityManager& manager)
{
UserID user = get_local_user_id();
Assert(user != kInvalid_user_id);
Assert(manager.next_local_counter != kEntityID_user_id_mask);
EntityID new_id = make_entity_id(user, manager.next_local_counter);
manager.next_local_counter += 1ULL;
return new_id;
}
EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity) EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity)
{ {
base->ID = EntityManager::ID++; base->ID = EntityManager::ID++;
@@ -143,3 +155,16 @@ Entity* deserialize_entity(Archive& ar, EntityManager& manager)
return entity_base; return entity_base;
} }
void update_local_counter(EntityManager& manager, EntityID entity_id)
{
uint8 user_id = get_local_user_id();
if (get_user_id(entity_id) == user_id)
{
uint64 counter = get_local_counter(entity_id);
if (counter >= manager.next_local_counter)
{
manager.next_local_counter = counter + 1ULL;
}
}
}
+3 -1
View File
@@ -18,6 +18,7 @@ struct typed_entity_array
struct EntityManager struct EntityManager
{ {
static EntityID ID; static EntityID ID;
uint64 next_local_counter = 1ULL;
// TODO: Should be a pool // TODO: Should be a pool
VectorArena<Entity, 100'000> Entities; VectorArena<Entity, 100'000> Entities;
@@ -31,4 +32,5 @@ EntityManager& GetEntityManager();
EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity); EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity);
[[nodiscard]] Entity* allocate_entity(EntityManager& manager, NonNullPtr<const Class> derived_type_class); [[nodiscard]] Entity* allocate_entity(EntityManager& manager, NonNullPtr<const Class> derived_type_class);
void UpdateEntityManager(EntityManager& manager); void UpdateEntityManager(EntityManager& manager);
[[nodisacrd]] Entity* deserialize_entity(Archive& archive, EntityManager& manager); [[nodiscard]] Entity* deserialize_entity(Archive& archive, EntityManager& manager);
void update_local_counter(EntityManager& manager, EntityID entity_id);
+6
View File
@@ -7,6 +7,12 @@ struct Class;
using DerivedType = void*; using DerivedType = void*;
using EntityID = uint64_t; using EntityID = uint64_t;
constexpr uint8 kInvalid_user_id = 0U;
constexpr EntityID kInvalid_entity_id = 0ULL;
constexpr EntityID kEntityID_user_id_shift = 56ULL;
constexpr EntityID kEntityID_user_id_mask = 0xFF00'0000'0000'0000ULL;
// clang-format off // clang-format off
#define ENTITY_TYPE_LIST(X) X(Inert), #define ENTITY_TYPE_LIST(X) X(Inert),
@@ -1,4 +1,4 @@
#pragma once #pragma once
#include <Core/Common/String.h> #include <Core/Common/String.h>
@@ -33,3 +33,10 @@ JULIET_API Path fs_create_clean_path(NonNullPtr<Arena> arena, String path);
JULIET_API bool fs_create_directory(String path, bool recursive); JULIET_API bool fs_create_directory(String path, bool recursive);
JULIET_API bool fs_path_exists(String path); JULIET_API bool fs_path_exists(String path);
JULIET_API bool fs_move_file(Path old_path, Path new_path);
JULIET_API bool fs_delete_file(String path);
using fs_iterate_director_callback = void (*)(Path path, bool is_director, void* user_payload);
JULIET_API void fs_iterate_directory(Path directory_path, String extension, void* user_payload, fs_iterate_director_callback callback);
+11 -8
View File
@@ -3,6 +3,7 @@
#include <Core/Common/NonNullPtr.h> #include <Core/Common/NonNullPtr.h>
#include <Core/Common/String.h> #include <Core/Common/String.h>
#include <Juliet.h> #include <Juliet.h>
#include "Core/HAL/Filesystem/Filesystem.h"
// Opaque type // Opaque type
struct IOStream; struct IOStream;
@@ -52,15 +53,17 @@ extern JULIET_API IOStream* IOFromInterface(NonNullPtr<Arena> arena, NonNullPtr<
NonNullPtr<IOStreamDataPayload> payload); NonNullPtr<IOStreamDataPayload> payload);
// Write formatted string into the stream. // Write formatted string into the stream.
extern JULIET_API size_t IOPrintf(NonNullPtr<IOStream> stream, _Printf_format_string_ const char* format, ...); JULIET_API size_t IOPrintf(NonNullPtr<IOStream> stream, _Printf_format_string_ const char* format, ...);
extern JULIET_API size_t IOWrite(NonNullPtr<IOStream> stream, ByteBuffer inBuffer); JULIET_API size_t IOWrite(NonNullPtr<IOStream> stream, ByteBuffer inBuffer);
extern JULIET_API size_t IORead(NonNullPtr<IOStream> stream, void* ptr, size_t size); JULIET_API size_t IORead(NonNullPtr<IOStream> stream, void* ptr, size_t size);
extern JULIET_API int64 IOSeek(NonNullPtr<IOStream> stream, int64 offset, IOStreamSeekPivot pivot); JULIET_API int64 IOSeek(NonNullPtr<IOStream> stream, int64 offset, IOStreamSeekPivot pivot);
extern JULIET_API int64 IOSize(NonNullPtr<IOStream> stream); JULIET_API int64 IOSize(NonNullPtr<IOStream> stream);
extern JULIET_API ByteBuffer LoadFile(NonNullPtr<Arena> arena, String filename); JULIET_API ByteBuffer LoadFile(NonNullPtr<Arena> arena, String filename);
extern JULIET_API ByteBuffer LoadFile(NonNullPtr<Arena> arena, NonNullPtr<IOStream> stream, bool closeStreamWhenDone); JULIET_API ByteBuffer LoadFile(NonNullPtr<Arena> arena, NonNullPtr<IOStream> stream, bool closeStreamWhenDone);
extern JULIET_API bool IOClose(NonNullPtr<IOStream> stream); JULIET_API size_t IO_atomic_file_write(Path filepath, ByteBuffer file_content);
JULIET_API bool IOClose(NonNullPtr<IOStream> stream);
+1 -1
View File
@@ -5,4 +5,4 @@ using UserID = uint8;
void initialize_user_system(); void initialize_user_system();
void shutdown_user_system(); void shutdown_user_system();
uint8 get_local_user_id(); JULIET_API uint8 get_local_user_id();
+22 -1
View File
@@ -1,4 +1,4 @@
#include <Core/Common/CoreUtils.h> #include <Core/Common/CoreUtils.h>
#include <Core/Common/String.h> #include <Core/Common/String.h>
#include <Core/HAL/Filesystem/Filesystem.h> #include <Core/HAL/Filesystem/Filesystem.h>
#include <Core/HAL/Filesystem/Filesystem_Platform.h> #include <Core/HAL/Filesystem/Filesystem_Platform.h>
@@ -106,6 +106,27 @@ bool fs_path_exists(String path)
return Platform::fs_path_exists(path); return Platform::fs_path_exists(path);
} }
bool fs_move_file(Path old_path, Path new_path)
{
return Platform::fs_move_file(old_path, new_path);
}
bool fs_delete_file(String path)
{
Assert(IsValid(path));
return Platform::fs_delete_file(path);
}
void fs_iterate_directory(Path directory_path, String extension, void* user_payload, fs_iterate_director_callback callback)
{
Assert(IsValid(directory_path));
Assert(callback);
TempArena scratch = scratch_begin(nullptr, 0);
scratch_end(scratch);
}
void InitFilesystem(NonNullPtr<Arena> arena) void InitFilesystem(NonNullPtr<Arena> arena)
{ {
CachedBasePath = Platform::GetBasePath(arena); CachedBasePath = Platform::GetBasePath(arena);
@@ -1,4 +1,4 @@
#pragma once #pragma once
#include <Core/HAL/Filesystem/Filesystem.h> #include <Core/HAL/Filesystem/Filesystem.h>
@@ -9,4 +9,6 @@ namespace Platform
Path fs_create_clean_path(NonNullPtr<Arena> arena, String path); Path fs_create_clean_path(NonNullPtr<Arena> arena, String path);
bool fs_create_directory(String path, bool recursive); bool fs_create_directory(String path, bool recursive);
bool fs_path_exists(String path); bool fs_path_exists(String path);
bool fs_move_file(Path old_path, Path new_path);
bool fs_delete_file(String path);
} // namespace Platform } // namespace Platform
@@ -1,4 +1,4 @@
#include <Core/Common/String.h> #include <Core/Common/String.h>
#include <Core/HAL/Filesystem/Filesystem_Platform.h> #include <Core/HAL/Filesystem/Filesystem_Platform.h>
#include <Core/HAL/Win32.h> #include <Core/HAL/Win32.h>
#include <Core/Logging/LogManager.h> #include <Core/Logging/LogManager.h>
@@ -167,10 +167,9 @@ namespace Platform
if (!create_result) if (!create_result)
{ {
const DWORD error = GetLastError(); const DWORD error = GetLastError();
if (error != ERROR_ALREADY_EXISTS) if (error == ERROR_ALREADY_EXISTS)
{ {
LogDebug(LogCategory::Core, "Folder already exists"); LogDebug(LogCategory::Core, "Folder already exists");
result = false;
} }
else else
{ {
@@ -191,4 +190,20 @@ namespace Platform
const bool result = (attribs != INVALID_FILE_ATTRIBUTES); const bool result = (attribs != INVALID_FILE_ATTRIBUTES);
return result; return result;
} }
bool fs_move_file(Path old_path, Path new_path)
{
bool result = MoveFileExA(CStr(old_path.FullPath), CStr(new_path.FullPath), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
return result;
}
bool fs_delete_file(String path)
{
if (!DeleteFileA(CStr(path)))
{
LogError(LogCategory::Core, "Failed to delete file %s. Error: %lu", CStr(path), GetLastError());
return false;
}
return true;
}
} // namespace Platform } // namespace Platform
+46
View File
@@ -5,6 +5,7 @@
#include <Core/Logging/LogManager.h> #include <Core/Logging/LogManager.h>
#include <Core/Logging/LogTypes.h> #include <Core/Logging/LogTypes.h>
#include <Core/Thread/Thread.h> #include <Core/Thread/Thread.h>
#include "Core/Thread/ThreadContext.h"
IOStream* IOFromFile(NonNullPtr<Arena> arena, String filename, String mode) IOStream* IOFromFile(NonNullPtr<Arena> arena, String filename, String mode)
{ {
@@ -212,6 +213,51 @@ ByteBuffer LoadFile(NonNullPtr<Arena> arena, NonNullPtr<IOStream> stream, bool c
return resultBuffer; return resultBuffer;
} }
size_t IO_atomic_file_write(Path filepath, ByteBuffer file_content)
{
size_t written_size = 0;
TempArena temp_arena = scratch_begin(nullptr, 0);
size_t temp_size = filepath.FullPath.Size + sizeof(".tmp");
char* tmp_str = (char*)ArenaPushSize(temp_arena.Arena, temp_size, 8, true JULIET_DEBUG_PARAM("Temp path"));
juliet_snprintf(tmp_str, temp_size, "%s.tmp", CStr(filepath.FullPath));
Path temp_path = fs_create_clean_path(temp_arena.Arena, WrapString(tmp_str));
if (IOStream* temp_stream = IOFromFile(temp_arena.Arena, temp_path.FullPath, ConstString("w+")))
{
written_size = IOWrite(temp_stream, file_content);
bool closed = IOClose(temp_stream);
if (closed && written_size == file_content.Size)
{
if (!fs_move_file(temp_path, filepath))
{
LogError(LogCategory::Core, "IO_atomic_file_write [%s] failed to move from temp to final file. Aborting",
CStr(filepath.FullPath));
written_size = 0;
}
}
else
{
LogError(LogCategory::Core, "IO_atomic_file_write [%s] failed to write into temporary file. Aborting",
CStr(filepath.FullPath));
}
if (written_size == 0)
{
fs_delete_file(temp_path.FullPath);
}
}
else
{
LogError(LogCategory::Core, "IO_atomic_file_write [%s] failed creating temporary file. Aborting", CStr(filepath.FullPath));
}
scratch_end(temp_arena);
return written_size;
}
bool IOClose(NonNullPtr<IOStream> stream) bool IOClose(NonNullPtr<IOStream> stream)
{ {
bool result = true; bool result = true;
+3 -3
View File
@@ -1,4 +1,4 @@
#include <Engine/Engine.h> #include <Engine/Engine.h>
#include <Core/Common/CoreUtils.h> #include <Core/Common/CoreUtils.h>
#include <Core/HAL/Event/SystemEvent.h> #include <Core/HAL/Event/SystemEvent.h>
@@ -154,12 +154,12 @@ void InitializeEngine(JulietInit_Flags flags)
InitializeLogManager(); InitializeLogManager();
InitFilesystem(EngineInstance.PlatformArena);
#if JULIET_DEBUG #if JULIET_DEBUG
UnitTest::RunUnitTests(); UnitTest::RunUnitTests();
#endif #endif
InitFilesystem(EngineInstance.PlatformArena);
initialize_user_system(); initialize_user_system();
JulietInit(flags); JulietInit(flags);
+2
View File
@@ -12,6 +12,7 @@ namespace UnitTest
void TestMemoryArena(); void TestMemoryArena();
void SerializationUnitTest(); void SerializationUnitTest();
void os_unit_test(); void os_unit_test();
void iostream_unit_test();
void RunUnitTests() void RunUnitTests()
{ {
@@ -21,6 +22,7 @@ namespace UnitTest
TestMemoryArena(); TestMemoryArena();
VectorUnitTest(); VectorUnitTest();
SerializationUnitTest(); SerializationUnitTest();
iostream_unit_test();
LogMessage(LogCategory::Core, "Unit Tests Completed Successfully."); LogMessage(LogCategory::Core, "Unit Tests Completed Successfully.");
} }
+472
View File
@@ -0,0 +1,472 @@
#if JULIET_DEBUG
#include <Core/Common/CoreUtils.h>
#include <Core/Common/String.h>
#include <Core/HAL/Filesystem/Filesystem.h>
#include <Core/HAL/IO/IOStream.h>
#include <Core/Logging/LogManager.h>
#include <Core/Logging/LogTypes.h>
#include <Core/Memory/MemoryArena.h>
#include <Core/Memory/Utils.h>
#include <Core/Thread/ThreadContext.h>
namespace UnitTest
{
struct IOStreamTestMemoryPayload : IOStreamDataPayload
{
Byte* data;
size_t capacity;
size_t size;
size_t position;
bool is_closed;
};
internal IOStreamTestMemoryPayload* iostream_test_get_payload(NonNullPtr<IOStreamDataPayload> payload)
{
return static_cast<IOStreamTestMemoryPayload*>(payload.Get());
}
internal int64 iostream_test_mem_size(NonNullPtr<IOStreamDataPayload> payload)
{
return static_cast<int64>(iostream_test_get_payload(payload)->size);
}
internal int64 iostream_test_mem_seek(NonNullPtr<IOStreamDataPayload> payload, int64 offset, IOStreamSeekPivot pivot)
{
auto* memory = iostream_test_get_payload(payload);
int64 base = 0;
switch (pivot)
{
case IOStreamSeekPivot::Begin: base = 0; break;
case IOStreamSeekPivot::Current: base = static_cast<int64>(memory->position); break;
case IOStreamSeekPivot::End: base = static_cast<int64>(memory->size); break;
case IOStreamSeekPivot::Count: return -1;
}
int64 new_position = base + offset;
if ((new_position < 0) || (new_position > static_cast<int64>(memory->size)))
{
return -1;
}
memory->position = static_cast<size_t>(new_position);
return new_position;
}
internal size_t iostream_test_mem_read(NonNullPtr<IOStreamDataPayload> payload, void* out_buffer, size_t size,
NonNullPtr<IOStreamStatus> /*status*/)
{
Assert(out_buffer != nullptr);
auto* memory = iostream_test_get_payload(payload);
size_t bytes_left = memory->size - memory->position;
size_t to_read = Min(size, bytes_left);
MemCopy(out_buffer, memory->data + memory->position, to_read);
memory->position += to_read;
return to_read;
}
internal size_t iostream_test_mem_write(NonNullPtr<IOStreamDataPayload> payload, ByteBuffer in_buffer,
NonNullPtr<IOStreamStatus> /*status*/)
{
Assert(IsValid(in_buffer));
auto* memory = iostream_test_get_payload(payload);
size_t space_left = memory->capacity - memory->position;
size_t to_write = Min(in_buffer.Size, space_left);
MemCopy(memory->data + memory->position, in_buffer.Data, to_write);
memory->position += to_write;
memory->size = Max(memory->size, memory->position);
return to_write;
}
internal bool iostream_test_mem_flush(NonNullPtr<IOStreamDataPayload> /*payload*/, NonNullPtr<IOStreamStatus> /*status*/)
{
return true;
}
internal bool iostream_test_mem_close(NonNullPtr<IOStreamDataPayload> payload)
{
iostream_test_get_payload(payload)->is_closed = true;
return true;
}
internal IOStreamInterface iostream_test_make_full_interface()
{
IOStreamInterface stream_interface = {};
stream_interface.Version = sizeof(IOStreamInterface);
stream_interface.Size = iostream_test_mem_size;
stream_interface.Seek = iostream_test_mem_seek;
stream_interface.Read = iostream_test_mem_read;
stream_interface.Write = iostream_test_mem_write;
stream_interface.Flush = iostream_test_mem_flush;
stream_interface.Close = iostream_test_mem_close;
return stream_interface;
}
internal IOStreamTestMemoryPayload iostream_test_make_payload(NonNullPtr<Arena> arena, size_t capacity)
{
Assert(capacity > 0);
IOStreamTestMemoryPayload payload = {};
payload.data = ArenaPushArray<Byte>(arena, capacity);
payload.capacity = capacity;
Assert(payload.data != nullptr);
return payload;
}
internal ByteBuffer iostream_test_buffer_from_string(String str)
{
Assert(IsValid(str));
return { .Data = reinterpret_cast<Byte*>(str.Str), .Size = str.Size };
}
internal bool iostream_test_buffer_equals(ByteBuffer buffer, String expected)
{
return (buffer.Size == expected.Size) && (MemCompare(buffer.Data, expected.Str, expected.Size) == 0);
}
internal String iostream_test_append_suffix(NonNullPtr<Arena> arena, String str, String suffix)
{
Assert(IsValid(str));
Assert(IsValid(suffix));
size_t total_size = str.Size + suffix.Size;
char* buffer = ArenaPushArray<char>(arena, total_size + 1);
Assert(buffer != nullptr);
MemCopy(buffer, str.Str, str.Size);
MemCopy(buffer + str.Size, suffix.Str, suffix.Size);
buffer[total_size] = '\0';
return { buffer, total_size };
}
internal void iostream_test_delete_file(String filename)
{
Assert(IsValid(filename));
if (fs_path_exists(filename))
{
Assert(fs_delete_file(filename));
}
Assert(!fs_path_exists(filename));
}
// Test files live in <repo>/Intermediate/UnitTest/ (exe is in <repo>/bin/x64-<Config>/)
internal Path iostream_test_make_path(NonNullPtr<Arena> arena, String filename)
{
Assert(IsValid(filename));
Path base_path = GetBasePath();
String raw_directory = iostream_test_append_suffix(arena, base_path.Directories,
ConstString("..\\..\\Intermediate\\UnitTest\\"));
Path test_directory = fs_create_clean_path(arena, raw_directory);
Assert(IsValid(test_directory));
if (!fs_path_exists(test_directory.FullPath))
{
Assert(fs_create_directory(test_directory.FullPath, true));
}
Path file_path = fs_create_clean_path(arena, iostream_test_append_suffix(arena, test_directory.FullPath, filename));
Assert(IsValid(file_path));
return file_path;
}
internal void iostream_test_write_file(String filename, String mode, String content)
{
TempArena scratch = scratch_begin(nullptr, 0);
IOStream* stream = IOFromFile(scratch.Arena, filename, mode);
Assert(stream != nullptr);
Assert(IOWrite(stream, iostream_test_buffer_from_string(content)) == content.Size);
Assert(IOClose(stream));
scratch_end(scratch);
}
// IOFromInterface + IOWrite / IOPrintf / IOSize / IOSeek / IORead / IOClose on a full interface
internal void iostream_test_memory_stream()
{
TempArena scratch = scratch_begin(nullptr, 0);
IOStreamInterface stream_interface = iostream_test_make_full_interface();
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, 256);
IOStream* stream = IOFromInterface(scratch.Arena, &stream_interface, &payload);
Assert(stream != nullptr);
String hello = ConstString("Hello");
Assert(IOWrite(stream, iostream_test_buffer_from_string(hello)) == hello.Size);
Assert(IOWrite(stream, {}) == 0);
Assert(IOPrintf(stream, " %s %d", "World", 42) == 9);
String expected = ConstString("Hello World 42");
Assert(IOSize(stream) == static_cast<int64>(expected.Size));
Assert(IOSeek(stream, 0, IOStreamSeekPivot::Begin) == 0);
char read_buffer[64] = {};
Assert(IORead(stream, read_buffer, expected.Size) == expected.Size);
Assert(MemCompare(read_buffer, expected.Str, expected.Size) == 0);
Assert(IORead(stream, read_buffer, sizeof(read_buffer)) == 0);
Assert(IORead(stream, read_buffer, 0) == 0);
Assert(IOSeek(stream, -2, IOStreamSeekPivot::End) == static_cast<int64>(expected.Size - 2));
Assert(IORead(stream, read_buffer, 2) == 2);
Assert(MemCompare(read_buffer, "42", 2) == 0);
Assert(IOSeek(stream, -8, IOStreamSeekPivot::Current) == 6);
Assert(IORead(stream, read_buffer, 5) == 5);
Assert(MemCompare(read_buffer, "World", 5) == 0);
Assert(!payload.is_closed);
Assert(IOClose(stream));
Assert(payload.is_closed);
scratch_end(scratch);
LogMessage(LogCategory::Core, "iostream_test_memory_stream passed.");
}
// Behavior when the interface is missing some functions
internal void iostream_test_capability_fallbacks()
{
TempArena scratch = scratch_begin(nullptr, 0);
String content = ConstString("Hello World 42");
// Read only, no seek, no size, no close
{
IOStreamInterface read_only_interface = {};
read_only_interface.Version = sizeof(IOStreamInterface);
read_only_interface.Read = iostream_test_mem_read;
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, 64);
MemCopy(payload.data, content.Str, content.Size);
payload.size = content.Size;
IOStream* stream = IOFromInterface(scratch.Arena, &read_only_interface, &payload);
Assert(stream != nullptr);
Assert(IOWrite(stream, iostream_test_buffer_from_string(content)) == 0);
Assert(IOSeek(stream, 0, IOStreamSeekPivot::Begin) == -1);
Assert(IOSize(stream) == -1);
char read_buffer[64] = {};
Assert(IORead(stream, read_buffer, content.Size) == content.Size);
Assert(MemCompare(read_buffer, content.Str, content.Size) == 0);
Assert(IOClose(stream));
Assert(!payload.is_closed);
}
// Write only
{
IOStreamInterface write_only_interface = {};
write_only_interface.Version = sizeof(IOStreamInterface);
write_only_interface.Write = iostream_test_mem_write;
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, 64);
IOStream* stream = IOFromInterface(scratch.Arena, &write_only_interface, &payload);
Assert(stream != nullptr);
Assert(IOWrite(stream, iostream_test_buffer_from_string(content)) == content.Size);
char read_buffer[64] = {};
Assert(IORead(stream, read_buffer, content.Size) == 0);
Assert(IOClose(stream));
}
// No Size function: IOSize falls back on Seek and restores the position
{
IOStreamInterface seek_interface = iostream_test_make_full_interface();
seek_interface.Size = nullptr;
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, 64);
MemCopy(payload.data, content.Str, content.Size);
payload.size = content.Size;
IOStream* stream = IOFromInterface(scratch.Arena, &seek_interface, &payload);
Assert(stream != nullptr);
Assert(IOSeek(stream, 3, IOStreamSeekPivot::Begin) == 3);
Assert(IOSize(stream) == static_cast<int64>(content.Size));
Assert(IOSeek(stream, 0, IOStreamSeekPivot::Current) == 3);
Assert(IOClose(stream));
}
scratch_end(scratch);
LogMessage(LogCategory::Core, "iostream_test_capability_fallbacks passed.");
}
// LoadFile(arena, stream, closeStreamWhenDone) with known size and chunked fallback
internal void iostream_test_load_file_from_stream()
{
TempArena scratch = scratch_begin(nullptr, 0);
String content = ConstString("Juliet LoadFile content");
// Known size, stream closed when done
{
IOStreamInterface stream_interface = iostream_test_make_full_interface();
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, 64);
MemCopy(payload.data, content.Str, content.Size);
payload.size = content.Size;
IOStream* stream = IOFromInterface(scratch.Arena, &stream_interface, &payload);
Assert(stream != nullptr);
ByteBuffer loaded = LoadFile(scratch.Arena, stream, true);
Assert(iostream_test_buffer_equals(loaded, content));
Assert(loaded.Data[loaded.Size] == Byte{ 0 });
Assert(payload.is_closed);
}
// Known size, stream kept open
{
IOStreamInterface stream_interface = iostream_test_make_full_interface();
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, 64);
MemCopy(payload.data, content.Str, content.Size);
payload.size = content.Size;
IOStream* stream = IOFromInterface(scratch.Arena, &stream_interface, &payload);
Assert(stream != nullptr);
ByteBuffer loaded = LoadFile(scratch.Arena, stream, false);
Assert(iostream_test_buffer_equals(loaded, content));
Assert(!payload.is_closed);
Assert(IOClose(stream));
}
// Unknown size (no Size / Seek): loaded chunk by chunk, bigger than one chunk
{
constexpr size_t k_big_content_size = 3000;
IOStreamInterface stream_interface = iostream_test_make_full_interface();
stream_interface.Size = nullptr;
stream_interface.Seek = nullptr;
IOStreamTestMemoryPayload payload = iostream_test_make_payload(scratch.Arena, k_big_content_size);
for (size_t index = 0; index < k_big_content_size; ++index)
{
payload.data[index] = static_cast<Byte>('a' + (index % 26));
}
payload.size = k_big_content_size;
IOStream* stream = IOFromInterface(scratch.Arena, &stream_interface, &payload);
Assert(stream != nullptr);
ByteBuffer loaded = LoadFile(scratch.Arena, stream, true);
Assert(loaded.Size == k_big_content_size);
Assert(MemCompare(loaded.Data, payload.data, k_big_content_size) == 0);
Assert(loaded.Data[loaded.Size] == Byte{ 0 });
Assert(payload.is_closed);
}
scratch_end(scratch);
LogMessage(LogCategory::Core, "iostream_test_load_file_from_stream passed.");
}
// IOFromFile + LoadFile(arena, filename) on a real file
internal void iostream_test_file_stream()
{
TempArena scratch = scratch_begin(nullptr, 0);
Path file_path = iostream_test_make_path(scratch.Arena, ConstString("juliet_iostream_unit_test.bin"));
Assert(IsValid(file_path));
iostream_test_delete_file(file_path.FullPath);
Assert(IOFromFile(scratch.Arena, {}, ConstString("rb")) == nullptr);
Assert(IOFromFile(scratch.Arena, file_path.FullPath, {}) == nullptr);
Assert(IOFromFile(scratch.Arena, file_path.FullPath, ConstString("rb")) == nullptr);
String first_part = ConstString("First part.");
String second_part = ConstString(" Second part.");
String full = ConstString("First part. Second part.");
iostream_test_write_file(file_path.FullPath, ConstString("wb"), first_part);
Assert(fs_path_exists(file_path.FullPath));
Assert(iostream_test_buffer_equals(LoadFile(scratch.Arena, file_path.FullPath), first_part));
iostream_test_write_file(file_path.FullPath, ConstString("ab"), second_part);
Assert(iostream_test_buffer_equals(LoadFile(scratch.Arena, file_path.FullPath), full));
// Seek / Size / partial read on a file stream
{
IOStream* stream = IOFromFile(scratch.Arena, file_path.FullPath, ConstString("rb"));
Assert(stream != nullptr);
Assert(IOSize(stream) == static_cast<int64>(full.Size));
Assert(IOSeek(stream, static_cast<int64>(first_part.Size), IOStreamSeekPivot::Begin) ==
static_cast<int64>(first_part.Size));
char read_buffer[64] = {};
Assert(IORead(stream, read_buffer, second_part.Size) == second_part.Size);
Assert(MemCompare(read_buffer, second_part.Str, second_part.Size) == 0);
Assert(IORead(stream, read_buffer, sizeof(read_buffer)) == 0);
Assert(IOWrite(stream, iostream_test_buffer_from_string(first_part)) == 0);
Assert(IOClose(stream));
}
// "w" truncates
iostream_test_write_file(file_path.FullPath, ConstString("wb"), second_part);
Assert(iostream_test_buffer_equals(LoadFile(scratch.Arena, file_path.FullPath), second_part));
iostream_test_delete_file(file_path.FullPath);
scratch_end(scratch);
LogMessage(LogCategory::Core, "iostream_test_file_stream passed.");
}
// IO_atomic_file_write: writes to <file>.tmp then moves it to <file>
internal void iostream_test_atomic_file_write()
{
TempArena scratch = scratch_begin(nullptr, 0);
Path file_path = iostream_test_make_path(scratch.Arena, ConstString("juliet_iostream_atomic_unit_test.bin"));
Assert(IsValid(file_path));
String temp_filename = iostream_test_append_suffix(scratch.Arena, file_path.FullPath, ConstString(".tmp"));
iostream_test_delete_file(file_path.FullPath);
iostream_test_delete_file(temp_filename);
// Create a new file
String first_content = ConstString("Atomic content that is long enough");
Assert(IO_atomic_file_write(file_path, iostream_test_buffer_from_string(first_content)) == first_content.Size);
Assert(fs_path_exists(file_path.FullPath));
Assert(!fs_path_exists(temp_filename));
Assert(iostream_test_buffer_equals(LoadFile(scratch.Arena, file_path.FullPath), first_content));
// Replace an existing file with shorter content
String second_content = ConstString("Shorter");
Assert(IO_atomic_file_write(file_path, iostream_test_buffer_from_string(second_content)) == second_content.Size);
Assert(!fs_path_exists(temp_filename));
Assert(iostream_test_buffer_equals(LoadFile(scratch.Arena, file_path.FullPath), second_content));
// A leftover temp file from a previous crash must not break the write
iostream_test_write_file(temp_filename, ConstString("wb"), ConstString("Stale temp data"));
Assert(IO_atomic_file_write(file_path, iostream_test_buffer_from_string(first_content)) == first_content.Size);
Assert(!fs_path_exists(temp_filename));
Assert(iostream_test_buffer_equals(LoadFile(scratch.Arena, file_path.FullPath), first_content));
iostream_test_delete_file(file_path.FullPath);
scratch_end(scratch);
LogMessage(LogCategory::Core, "iostream_test_atomic_file_write passed.");
}
void iostream_unit_test()
{
LogMessage(LogCategory::Core, "=== Running IOStream Unit Tests ===");
iostream_test_memory_stream();
iostream_test_capability_fallbacks();
iostream_test_load_file_from_stream();
iostream_test_file_stream();
iostream_test_atomic_file_write();
LogMessage(LogCategory::Core, "=== All IOStream Unit Tests Passed Successfully ===");
}
} // namespace UnitTest
#endif