write simple version engine and try write asset packer
This commit is contained in:
parent
f990b270f9
commit
72b8118c60
52
cmd/test/main.go
Normal file
52
cmd/test/main.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
5
go.mod
5
go.mod
@ -1,3 +1,8 @@
|
|||||||
module git.influ.su/artmares/art3de
|
module git.influ.su/artmares/art3de
|
||||||
|
|
||||||
go 1.25.6
|
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
|
||||||
|
)
|
||||||
|
|||||||
4
go.sum
Normal file
4
go.sum
Normal file
@ -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=
|
||||||
1
pkg/assets/hybrid_format.go
Normal file
1
pkg/assets/hybrid_format.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package assets
|
||||||
213
pkg/assets/res_format.go
Normal file
213
pkg/assets/res_format.go
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -1,11 +1,15 @@
|
|||||||
package engine
|
package engine
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.influ.su/artmares/art3de/pkg/engine/renderer"
|
||||||
|
)
|
||||||
|
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
isRunning bool
|
isRunning bool
|
||||||
window *Window
|
window *Window
|
||||||
renderer *Renderer
|
renderer *renderer.Renderer
|
||||||
lastTime time.Time
|
lastTime time.Time
|
||||||
game Game
|
game Game
|
||||||
}
|
}
|
||||||
@ -24,7 +28,7 @@ func (e *Engine) Run(game Game) error {
|
|||||||
}
|
}
|
||||||
defer e.window.Destroy()
|
defer e.window.Destroy()
|
||||||
|
|
||||||
e.renderer = NewRenderer()
|
e.renderer = renderer.NewRenderer()
|
||||||
if err := e.renderer.Init(); err != nil {
|
if err := e.renderer.Init(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -54,6 +58,12 @@ func (e *Engine) Run(game Game) error {
|
|||||||
e.game.Draw(e.renderer)
|
e.game.Draw(e.renderer)
|
||||||
|
|
||||||
// Отображение буфера
|
// Отображение буфера
|
||||||
e.SwapBuffers()
|
e.window.SwapBuffers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) Stop() {
|
||||||
|
e.isRunning = false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
package engine
|
package engine
|
||||||
|
|
||||||
|
import "git.influ.su/artmares/art3de/pkg/engine/renderer"
|
||||||
|
|
||||||
type Game interface {
|
type Game interface {
|
||||||
Init() error
|
Init() error
|
||||||
Update(deltaTime float32)
|
Update(deltaTime float32)
|
||||||
Draw(renderer *Renderer)
|
Draw(renderer *renderer.Renderer)
|
||||||
OnResize(width, height int)
|
OnResize(width, height int)
|
||||||
}
|
}
|
||||||
|
|||||||
45
pkg/engine/renderer/renderer.go
Normal file
45
pkg/engine/renderer/renderer.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
65
pkg/engine/window.go
Normal file
65
pkg/engine/window.go
Normal file
@ -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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user