diff --git a/cmd/test/main.go b/cmd/test/main.go new file mode 100644 index 0000000..5fab31f --- /dev/null +++ b/cmd/test/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "log" + + "git.influ.su/artmares/art3de/pkg/engine" + "git.influ.su/artmares/art3de/pkg/engine/renderer" +) + +func main() { + // Создаем движок + eng := engine.NewEngine("My Game", 1280, 1024) + + // Создаем игру + game := NewGame() + + // Запускаем + if err := eng.Run(game); err != nil { + log.Println(err) + } +} + +type Game struct { + renderer *renderer.Renderer + width, height int +} + +func NewGame() *Game { + return &Game{} +} + +func (g *Game) Init() error { + println("Game initialized!") + return nil +} + +func (g *Game) Update(deltaTime float32) { + // Обновление логики игры +} + +func (g *Game) Draw(r *renderer.Renderer) { + // Отрисовка игровых объектов + // Пока просто очищаем экран +} + +func (g *Game) OnResize(width, height int) { + g.width = width + g.height = height + if g.renderer != nil { + g.renderer.SetViewport(0, 0, int32(width), int32(height)) + } +} diff --git a/go.mod b/go.mod index 645b17d..3387ac4 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module git.influ.su/artmares/art3de go 1.25.6 + +require ( + github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..52be21b --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc= diff --git a/pkg/assets/hybrid_format.go b/pkg/assets/hybrid_format.go new file mode 100644 index 0000000..b1b684d --- /dev/null +++ b/pkg/assets/hybrid_format.go @@ -0,0 +1 @@ +package assets diff --git a/pkg/assets/res_format.go b/pkg/assets/res_format.go new file mode 100644 index 0000000..2b67f96 --- /dev/null +++ b/pkg/assets/res_format.go @@ -0,0 +1,213 @@ +package assets + +import ( + "encoding/binary" + "fmt" + "io" + "os" + "path/filepath" + "time" +) + +// RESHeader зоголовок оригинального файла +type RESHeader struct { + Magic [4]byte // 0x3C, 0xE2, 0x9C, 0x01 + FilesCount uint32 + FileTableOffset uint32 + NamesSize uint32 +} + +// RESFileRecord запись файла в оригинальном RES +type RESFileRecord struct { + NextIndex int32 + FileSize uint32 + FileOffset uint32 + LastChange uint32 + NameLen uint16 + NameOffset uint32 +} + +// RESFile информация о файле в RES +type RESFile struct { + Name string + Size uint32 + Offset uint32 + LastChange time.Time + NameLen uint16 // Добавим для корректной работы +} + +// RESArchive представляет оригинальный RES архив +type RESArchive struct { + File *os.File + Header RESHeader + Files []RESFile + isOpen bool +} + +// OpenRES открывает оригинальный RES файл +func OpenRES(path string) (*RESArchive, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open RES file: %w", err) + } + + archive := &RESArchive{ + File: file, + isOpen: true, + } + + // Читаем заголовок + if err = binary.Read(file, binary.LittleEndian, &archive.Header); err != nil { + _ = file.Close() + return nil, fmt.Errorf("failed to read RES header: %w", err) + } + + // Проверяем магиеские байты + if archive.Header.Magic != [4]byte{0x3C, 0xE2, 0x9C, 0x01} { + _ = file.Close() + return nil, fmt.Errorf("invalid RES file (wrong magic)") + } + + // Читаем таблицу файлов + if err = archive.readFileTable(); err != nil { + _ = file.Close() + return nil, err + } + + return archive, nil +} + +// readFileTable читает таблицу файлов RES +func (ra *RESArchive) readFileTable() error { + // Преходим к таблице файлов + if _, err := ra.File.Seek(int64(ra.Header.FileTableOffset), io.SeekStart); err != nil { + return err + } + + // Сначала читаем все записи файлов + records := make([]RESFileRecord, ra.Header.FilesCount) + for i := uint32(0); i < ra.Header.FilesCount; i++ { + var record RESFileRecord + if err := binary.Read(ra.File, binary.LittleEndian, &record); err != nil { + return fmt.Errorf("failed to read file record %d: %w", i, err) + } + records[i] = record + } + + // Вычисляем смещение таблицы имен + nameTableOffset := ra.Header.FileTableOffset + 22*ra.Header.FilesCount + ra.Files = make([]RESFile, ra.Header.FilesCount) + + // Читаем имена файлов и создаем записи + for i, record := range records { + // Переходим к имени файла + nameOffset := nameTableOffset + record.NameOffset + if _, err := ra.File.Seek(int64(nameOffset), io.SeekStart); err != nil { + return fmt.Errorf("failed to seek to name offset: %w", err) + } + + // Читаем имя в CP1251 + nameBytes := make([]byte, record.NameLen) + if _, err := io.ReadFull(ra.File, nameBytes); err != nil { + return fmt.Errorf("failed to read name: %w", err) + } + + // Конвертируем из CP1251 в UTF-8 + name := decodeCP1251(nameBytes) + + // Создаем запись файла + ra.Files[i] = RESFile{ + Name: name, + Size: record.FileSize, + Offset: record.FileOffset, + LastChange: time.Unix(int64(record.LastChange), 0), + NameLen: record.NameLen, + } + } + + return nil +} + +// GetFile получает файл из архива +func (ra *RESArchive) GetFile(name string) ([]byte, error) { + for _, file := range ra.Files { + if file.Name == name { + return ra.readFileData(file.Offset, file.Size) + } + } + return nil, fmt.Errorf("file not found: %s", name) +} + +// GetFileByIndex получает файл по индексу +func (ra *RESArchive) GetFileByIndex(index int) ([]byte, error) { + if index < 0 || index >= len(ra.Files) { + return nil, fmt.Errorf("invalid file index: %d", index) + } + + file := ra.Files[index] + return ra.readFileData(file.Offset, file.Size) +} + +// readFileData читает данные файла +func (ra *RESArchive) readFileData(offset, size uint32) ([]byte, error) { + if _, err := ra.File.Seek(int64(offset), io.SeekStart); err != nil { + return nil, fmt.Errorf("failed to seek to file data: %w", err) + } + + data := make([]byte, size) + if _, err := io.ReadFull(ra.File, data); err != nil { + return nil, fmt.Errorf("failed to read file data: %w", err) + } + + return data, nil +} + +// ListFiles возвращает список файлов +func (ra *RESArchive) ListFiles() []string { + names := make([]string, len(ra.Files)) + for i, file := range ra.Files { + names[i] = file.Name + } + return names +} + +// GetFileInfo возвращает информацию о файле +func (ra *RESArchive) GetFileInfo(name string) (*RESFile, error) { + for _, file := range ra.Files { + if file.Name == name { + return &file, nil + } + } + return nil, fmt.Errorf("file not found: %s", name) +} + +// ExtractAll извлекает все файлы +func (ra *RESArchive) ExtractAll(outputDir string) error { + for _, file := range ra.Files { + data, err := ra.GetFile(file.Name) + if err != nil { + return fmt.Errorf("failed to extract %s: %w", file.Name, err) + } + + outputPath := filepath.Join(outputDir, file.Name) + if err = os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil { + return err + } + + if err = os.WriteFile(outputPath, data, 0644); err != nil { + return err + } + } + + return nil +} + +// Close закрывает архив +func (ra *RESArchive) Close() error { + if ra.isOpen { + ra.isOpen = false + return ra.File.Close() + } + + return nil +} diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 71a1057..1ea0693 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -1,11 +1,15 @@ package engine -import "time" +import ( + "time" + + "git.influ.su/artmares/art3de/pkg/engine/renderer" +) type Engine struct { isRunning bool window *Window - renderer *Renderer + renderer *renderer.Renderer lastTime time.Time game Game } @@ -24,7 +28,7 @@ func (e *Engine) Run(game Game) error { } defer e.window.Destroy() - e.renderer = NewRenderer() + e.renderer = renderer.NewRenderer() if err := e.renderer.Init(); err != nil { return err } @@ -54,6 +58,12 @@ func (e *Engine) Run(game Game) error { e.game.Draw(e.renderer) // Отображение буфера - e.SwapBuffers() + e.window.SwapBuffers() } + + return nil +} + +func (e *Engine) Stop() { + e.isRunning = false } diff --git a/pkg/engine/game.go b/pkg/engine/game.go index 2024bdf..4d5b8ce 100644 --- a/pkg/engine/game.go +++ b/pkg/engine/game.go @@ -1,8 +1,10 @@ package engine +import "git.influ.su/artmares/art3de/pkg/engine/renderer" + type Game interface { Init() error Update(deltaTime float32) - Draw(renderer *Renderer) + Draw(renderer *renderer.Renderer) OnResize(width, height int) } diff --git a/pkg/engine/renderer/renderer.go b/pkg/engine/renderer/renderer.go new file mode 100644 index 0000000..00cc4e2 --- /dev/null +++ b/pkg/engine/renderer/renderer.go @@ -0,0 +1,45 @@ +package renderer + +import ( + "log" + + "github.com/go-gl/gl/v4.1-core/gl" +) + +type Renderer struct { + clearColor [4]float32 +} + +func NewRenderer() *Renderer { + return &Renderer{ + clearColor: [4]float32{0.1, 0.1, 0.1, 0.1}, + } +} + +func (r *Renderer) Init() error { + if err := gl.Init(); err != nil { + return err + } + + gl.Enable(gl.DEPTH_TEST) + gl.Enable(gl.BLEND) + gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + + version := gl.GoStr(gl.GetString(gl.VERSION)) + log.Println("OpenGL version:", version) + + return nil +} + +func (r *Renderer) Clear() { + gl.ClearColor(r.clearColor[0], r.clearColor[1], r.clearColor[2], r.clearColor[3]) + gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) +} + +func (r *Renderer) SetClearColor(rgba [4]float32) { + r.clearColor = rgba +} + +func (r *Renderer) SetViewport(x, y, width, height int32) { + gl.Viewport(x, y, width, height) +} diff --git a/pkg/engine/window.go b/pkg/engine/window.go new file mode 100644 index 0000000..a6798f7 --- /dev/null +++ b/pkg/engine/window.go @@ -0,0 +1,65 @@ +package engine + +import "github.com/go-gl/glfw/v3.3/glfw" + +type Window struct { + window *glfw.Window + title string + width, height int +} + +func NewWindow(title string, width, height int) *Window { + return &Window{ + title: title, + width: width, + height: height, + } +} + +func (w *Window) Init() error { + if err := glfw.Init(); err != nil { + return err + } + + glfw.WindowHint(glfw.ContextVersionMajor, 4) + glfw.WindowHint(glfw.ContextVersionMinor, 1) + glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile) + glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True) + glfw.WindowHint(glfw.Resizable, glfw.True) + + window, err := glfw.CreateWindow(w.width, w.height, w.title, nil, nil) + if err != nil { + return err + } + + w.window = window + w.window.MakeContextCurrent() + w.window.SetFramebufferSizeCallback(w.framebufferSizeCallback) + + return nil +} + +func (w *Window) framebufferSizeCallback(window *glfw.Window, width, height int) { + w.width, w.height = width, height + // Здесь можно добавить коллбэк для игры +} + +func (w *Window) ShouldClose() bool { + return w.window.ShouldClose() +} + +func (w *Window) PollEvents() { + glfw.PollEvents() +} + +func (w *Window) SwapBuffers() { + w.window.SwapBuffers() +} + +func (w *Window) Destroy() { + glfw.Terminate() +} + +func (w *Window) GetSize() (int, int) { + return w.width, w.height +}