improved file read/copy logic

This commit is contained in:
zomo
2025-12-24 13:58:02 -06:00
parent 658ad890f5
commit 32ed104b60
10 changed files with 337 additions and 153 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
rooms rooms
rooms_out rooms_out
bappackage.exe
bappackage-*.exe
run.bat

View File

@@ -4,8 +4,8 @@ func AssignExtraRooms(rooms []RoomFolder, roomsPerNight int) {
latestnight := 0 latestnight := 0
for _, r := range rooms { for _, r := range rooms {
if r.cfg.night > latestnight { if r.Cfg.night > latestnight {
latestnight = r.cfg.night latestnight = r.Cfg.night
} }
} }
@@ -13,12 +13,12 @@ func AssignExtraRooms(rooms []RoomFolder, roomsPerNight int) {
room := 1 room := 1
for i, r := range rooms { for i, r := range rooms {
if r.cfg.night > 0 { if r.Cfg.night > 0 {
continue continue
} }
rooms[i].cfg.night = night rooms[i].Cfg.night = night
rooms[i].cfg.room = room rooms[i].Cfg.room = room
room++ room++
if room > roomsPerNight { if room > roomsPerNight {

6
build.ps1 Normal file
View File

@@ -0,0 +1,6 @@
Remove-Item .\bappackage-*.exe -ErrorAction SilentlyContinue
Remove-Item .\bappackage.exe -ErrorAction SilentlyContinue
go build .
$hash = (Get-FileHash -Algorithm SHA256 .\bappackage.exe).Hash
$hash = $hash.Remove(0, ($hash.Length - 8))
Move-Item .\bappackage.exe .\bappackage-$hash.exe

68
gameinfo.go Normal file
View File

@@ -0,0 +1,68 @@
package main
import "slices"
var REQUIRED_IMAGES = []string{
"ART",
"CREDIT",
"FLOOR",
"ROOF",
"WALL",
}
var OPTIONAL_BUTREQUIRED_IMAGES = []string{
"MINI",
"INTERACT",
"LAYER",
}
var OPTIONAL_IMAGES = []string{
"WALL2",
"WALL3",
"WALL4",
}
var ACCEPTED_IMAGE_EXT = []string{
"PNG",
"JPG",
"BMP",
}
var OPTIONAL_BUTREQUIRED_AUDIO = []string{
"FOOT",
"MUSIC",
}
var ACCEPTED_AUDIO_EXT = []string{
"WAV",
"MP3",
"OGG",
"MIDI",
}
func gameRequiredImages() []string {
minimal := make([]string, len(REQUIRED_IMAGES))
copy(minimal, REQUIRED_IMAGES)
return minimal
}
func gameExtraRequiredImages() []string {
extra := make([]string, len(OPTIONAL_BUTREQUIRED_IMAGES))
copy(extra, OPTIONAL_BUTREQUIRED_IMAGES)
return extra
}
func gameExtraRequiredAudios() []string {
extra := make([]string, len(OPTIONAL_BUTREQUIRED_AUDIO))
copy(extra, OPTIONAL_BUTREQUIRED_AUDIO)
return extra
}
func isAcceptedImageExt(ext string) bool {
return slices.Contains(ACCEPTED_IMAGE_EXT, ext)
}
func isAcceptedAudioExt(ext string) bool {
return slices.Contains(ACCEPTED_AUDIO_EXT, ext)
}
func isAcceptedFile(name, ext string) bool {
return (isAcceptedImageExt(ext) && (slices.Contains(REQUIRED_IMAGES, name) || slices.Contains(OPTIONAL_BUTREQUIRED_IMAGES, name) || slices.Contains(OPTIONAL_IMAGES, name))) ||
(isAcceptedAudioExt(ext) && slices.Contains(OPTIONAL_BUTREQUIRED_AUDIO, name))
}

57
ini.go
View File

@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"log"
"strconv" "strconv"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
@@ -9,17 +10,17 @@ import (
type RoomIni struct { type RoomIni struct {
night int night int
room int room int
size float64 size float64
speed float64 speed float64
fov float64 fov float64
textureRepeat float64 textureRepeat float64
roomHeight float64 roomHeight float64
layerFrames int layerFrames int
fogColor int fogColor int
fogEnd float64 fogEnd float64
miniSpeed float64 miniSpeed float64
} }
func (r *RoomIni) ToIni() string { func (r *RoomIni) ToIni() string {
@@ -39,29 +40,29 @@ Fog_Color=%d
Fog_End=%s Fog_End=%s
Mini_speed=%s Mini_speed=%s
`, `,
r.night, r.room, r.night, r.room,
f(r.size), f(r.speed), f(r.size), f(r.speed),
f(r.fov), f(r.textureRepeat), f(r.fov), f(r.textureRepeat),
f(r.roomHeight), r.layerFrames, f(r.roomHeight), r.layerFrames,
r.fogColor, f(r.fogEnd), r.fogColor, f(r.fogEnd),
f(r.miniSpeed), f(r.miniSpeed),
) )
} }
func NewRoomIni() RoomIni { func NewRoomIni() RoomIni {
return RoomIni { return RoomIni{
night: -1, night: -1,
room: -1, room: -1,
size: 1, size: 1,
speed: 1, speed: 1,
fov: 55, fov: 55,
textureRepeat: 3, textureRepeat: 3,
roomHeight: 350, roomHeight: 350,
layerFrames: 1, layerFrames: 1,
fogColor: 0, fogColor: 0,
fogEnd: -1, fogEnd: -1,
miniSpeed: 1, miniSpeed: 1,
} }
} }
@@ -71,7 +72,6 @@ func ReadIni(path string) (RoomIni, error) {
return RoomIni{}, err return RoomIni{}, err
} }
meta, err := cfg.GetSection("DEFAULT") meta, err := cfg.GetSection("DEFAULT")
if err != nil { if err != nil {
return RoomIni{}, fmt.Errorf("unable to load DEFAULT section in %s: %+v", path, err) return RoomIni{}, fmt.Errorf("unable to load DEFAULT section in %s: %+v", path, err)
@@ -82,7 +82,7 @@ func ReadIni(path string) (RoomIni, error) {
for _, sec := range sections { for _, sec := range sections {
if room != nil { if room != nil {
fmt.Printf("WARN: extra section in %s, section: %s\n", path, sec.Name()) log.Printf("WARN: extra section in %s, section: %s", path, sec.Name())
continue continue
} }
if sec.Name() != "default" { if sec.Name() != "default" {
@@ -117,7 +117,6 @@ func loadIni(meta *ini.Section, room *ini.Section) (RoomIni, error) {
return RoomIni{}, fmt.Errorf("night is set but room is not set") return RoomIni{}, fmt.Errorf("night is set but room is not set")
} }
if val, err := loatFloat(room, "size", 1); err != nil { if val, err := loatFloat(room, "size", 1); err != nil {
return RoomIni{}, err return RoomIni{}, err
} else { } else {

View File

@@ -2,43 +2,16 @@ package main
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"path"
"path/filepath" "path/filepath"
"slices" "slices"
"strings"
) )
var REQUIRED_IMAGES = []string{
"ART",
"MINI",
"CREDIT",
"INTERACT",
"FLOOR",
"LAYER",
"ROOF",
"WALL",
}
var ACCEPTED_IMAGE_EXT = []string{
".PNG",
".JPG",
".BMP",
}
var REQUIRED_AUDIO = []string{
"FOOT",
"MUSIC",
}
var ACCEPTED_AUDIO_EXT = []string{
".WAV",
".MP3",
".OGG",
".MIDI",
}
type RoomFolder struct { type RoomFolder struct {
path string Path string
cfg RoomIni Cfg RoomIni
IsDefaultCfg bool
} }
func LoadRoomFolders(roomspath string) ([]RoomFolder, error) { func LoadRoomFolders(roomspath string) ([]RoomFolder, error) {
@@ -50,15 +23,14 @@ func LoadRoomFolders(roomspath string) ([]RoomFolder, error) {
rooms := make([]RoomFolder, 0) rooms := make([]RoomFolder, 0)
for _, entry := range entries { for _, entry := range entries {
entrypath := filepath.Join(roomspath, entry.Name()) if entry.Name()[0] == '.' || !entry.IsDir() {
if !entry.IsDir() {
fmt.Printf("WARN: unexpected non-directory found in rooms directory: %s\n", entrypath)
continue continue
} }
entrypath := filepath.Join(roomspath, entry.Name())
roomfolder, err := loadRoom(entrypath) roomfolder, err := loadRoom(entrypath)
if err != nil { if err != nil {
fmt.Printf("ERROR: unable to load directory: %s\n", entrypath) log.Printf("ERROR: unable to load directory: %s: %v", entrypath, err)
continue continue
} }
@@ -74,58 +46,43 @@ func loadRoom(roompath string) (RoomFolder, error) {
return RoomFolder{}, err return RoomFolder{}, err
} }
requiredImages := make([]string, len(REQUIRED_IMAGES)) minimalRequiredImages := gameRequiredImages()
copy(requiredImages, REQUIRED_IMAGES)
requiredAudio := make([]string, len(REQUIRED_AUDIO))
copy(requiredAudio, REQUIRED_AUDIO)
roominiName := "" roominiName := ""
for _, entry := range entries { for _, entry := range entries {
filename := entry.Name() if entry.Name()[0] == '.' || entry.IsDir() {
if strings.ToLower(filename) == "room.ini" { continue
roominiName = filename
} }
ext := strings.ToUpper(path.Ext(filename)) name, ext := SplitExt(entry.Name())
name := strings.ToUpper(filename[:len(filename)-len(ext)])
if index := slices.Index(requiredImages, name); index > -1 && slices.Contains(ACCEPTED_IMAGE_EXT, ext) { if name == "ROOM" && (ext == "INI" || ext == "TXT") {
requiredImages = append(requiredImages[:index], requiredImages[index+1:]...) roominiName = entry.Name()
} }
if index := slices.Index(requiredAudio, name); index > -1 && slices.Contains(ACCEPTED_AUDIO_EXT, ext) { if index := slices.Index(minimalRequiredImages, name); index > -1 && isAcceptedImageExt(ext) {
requiredAudio = append(requiredAudio[:index], requiredAudio[index+1:]...) minimalRequiredImages = append(minimalRequiredImages[:index], minimalRequiredImages[index+1:]...)
} }
} }
errormsg := "" if len(minimalRequiredImages) > 0 {
if len(requiredImages) > 0 && len(requiredAudio) > 0 { return RoomFolder{}, fmt.Errorf("unable to load room %s: missing required images: %v", roompath, minimalRequiredImages)
errormsg = fmt.Sprintf("images: %v, audio: %v", requiredImages, requiredAudio)
}
if len(requiredImages) > 0 {
errormsg = fmt.Sprintf("images: %v", requiredImages)
}
if len(requiredAudio) > 0 {
errormsg = fmt.Sprintf("audio: %v", requiredAudio)
}
if errormsg != "" {
return RoomFolder{}, fmt.Errorf("unable to load room %s: missing required %s", roompath, errormsg)
} }
cfg := NewRoomIni() cfg := NewRoomIni()
defaultCfg := true
if roominiName != "" { if roominiName != "" {
newcfg, err := ReadIni(filepath.Join(roompath, "room.ini")) readcfg, err := ReadIni(filepath.Join(roompath, roominiName))
if err != nil { if err != nil {
return RoomFolder{}, err return RoomFolder{}, err
} }
cfg = newcfg cfg = readcfg
defaultCfg = false
} }
return RoomFolder{ return RoomFolder{
path: roompath, Path: roompath,
cfg: cfg, Cfg: cfg,
IsDefaultCfg: defaultCfg,
}, nil }, nil
} }

25
main.go
View File

@@ -1,8 +1,6 @@
package main package main
const BASEPATH = "./rooms" import "flag"
const OUTPATH = "./rooms_out"
const ROOM_PER_NIGHT = 4
func main() { func main() {
if err := mainErr(); err != nil { if err := mainErr(); err != nil {
@@ -11,12 +9,27 @@ func main() {
} }
func mainErr() error { func mainErr() error {
roomFolders, err := LoadRoomFolders(BASEPATH) basePath := flag.String("basePath", "./rooms", "path to your rooms collection")
outPath := flag.String("outPath", "./rooms_out", "output path")
roomPerNight := flag.Int("roomPerNight", 4, "rooms per night")
splash1len := flag.Int("splash1", 2, "number of seconds splashscreen 1 will take")
splash2len := flag.Int("splash2", 2, "number of seconds splashscreen 2 will take")
flag.Parse()
roomFolders, err := LoadRoomFolders(*basePath)
if err != nil { if err != nil {
return err return err
} }
AssignExtraRooms(roomFolders, ROOM_PER_NIGHT) AssignExtraRooms(roomFolders, *roomPerNight)
return CopyOut(roomFolders, OUTPATH) // only deletes DATA/NIGHT_* folders and the CF.ini file
err = EmptyOut(*outPath)
if err != nil {
return err
}
return CopyOut(roomFolders, *outPath, *splash1len, *splash2len)
} }

193
out.go
View File

@@ -1,74 +1,183 @@
package main package main
import ( import (
_ "embed"
"fmt" "fmt"
"image"
"image/color"
"image/png"
"log"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
) )
//go:embed silence.wav
var SILENCE_WAV []byte
const FILEPERM = 0644 const FILEPERM = 0644
const INIPREFIX = `[SETTINGS]
Sensitivity=6
Splash1len=2
Splash2len=2
`
func CopyOut(roomFolders []RoomFolder, outpath string) error { // only deletes DATA/NIGHT_* folders and the CF.ini file
inistr := INIPREFIX // will not touch TITLE or any game files
// so this can be ran safely in your game's directory
for _, roomFolder := range roomFolders { func EmptyOut(outpath string) error {
err := copyFolder(roomFolder, outpath) err := emptyOutData(outpath)
if err != nil {
return fmt.Errorf("error copying folder %s: %+v", roomFolder.path, err)
}
inistr += roomFolder.cfg.ToIni()
}
outini := filepath.Join(outpath, "CF.ini")
fmt.Printf("Writing %s\n", outini)
return os.WriteFile(outini, []byte(inistr), FILEPERM)
}
func copyFolder(roomFolder RoomFolder, outpath string) error {
nightdir := fmt.Sprintf("NIGHT_%d", roomFolder.cfg.night)
roomdir := fmt.Sprintf("ROOM_%d", roomFolder.cfg.room)
roomoutpath := filepath.Join(outpath, "DATA", nightdir, roomdir)
os.MkdirAll(roomoutpath, FILEPERM)
fmt.Printf(`Copying:
%s
to %s
`, roomFolder.path, roomoutpath)
entries, err := os.ReadDir(roomFolder.path)
if err != nil { if err != nil {
return err return err
} }
cfpath := path.Join(outpath, "CF.ini")
exists, err := Exists(cfpath)
if err != nil {
return err
}
if !exists {
return nil
}
log.Printf("INFO: removing %s", cfpath)
return os.Remove(cfpath)
}
func emptyOutData(outpath string) error {
datapath := path.Join(outpath, "DATA")
exists, err := Exists(datapath)
if err != nil {
return err
}
if !exists {
return nil
}
entries, err := os.ReadDir(datapath)
if err != nil {
return err
}
nightprefix := "NIGHT_"
for _, entry := range entries { for _, entry := range entries {
// TODO whitelist files? if entry.Name()[0] == '.' || !entry.IsDir() {
entrypath := path.Join(roomFolder.path, entry.Name())
if entry.IsDir() {
fmt.Printf("WARN: unexpected folder found in room folder: %s\n", entrypath)
continue continue
} }
if strings.ToLower(entry.Name()) == "room.ini" { name := strings.ToUpper(entry.Name())
if len(name) >= len(nightprefix) && name[:len(nightprefix)] == nightprefix {
nightpath := path.Join(datapath, entry.Name())
log.Printf("INFO: removing %s", nightpath)
err = os.RemoveAll(nightpath)
if err != nil {
return err
}
}
}
return nil
}
func CopyOut(roomFolders []RoomFolder, outpath string, splash1len, splash2len int) error {
inistr := fmt.Sprintf(`[SETTINGS]
Sensitivity=6
Splash1len=%d
Splash2len=%d
`, splash1len, splash2len)
for _, roomFolder := range roomFolders {
err := copyFolder(roomFolder, outpath)
if err != nil {
return fmt.Errorf("error copying folder %s: %+v", roomFolder.Path, err)
}
inistr += roomFolder.Cfg.ToIni()
}
outini := filepath.Join(outpath, "CF.ini")
log.Println("INFO:")
log.Printf("INFO: Writing %s", outini)
return os.WriteFile(outini, []byte(inistr), FILEPERM)
}
func copyFolder(roomFolder RoomFolder, outpath string) error {
nightdir := fmt.Sprintf("NIGHT_%d", roomFolder.Cfg.night)
roomdir := fmt.Sprintf("ROOM_%d", roomFolder.Cfg.room)
roomoutpath := filepath.Join(outpath, "DATA", nightdir, roomdir)
os.MkdirAll(roomoutpath, FILEPERM)
log.Println("INFO:")
log.Printf("INFO: Copying %s", roomFolder.Path)
log.Printf("INFO: to %s", roomoutpath)
if roomFolder.IsDefaultCfg {
log.Println("INFO: room.ini was not found, using default settings")
}
entries, err := os.ReadDir(roomFolder.Path)
if err != nil {
return err
}
extraRequiredImages := gameExtraRequiredImages()
extraRequiredAudio := gameExtraRequiredAudios()
for _, entry := range entries {
if entry.Name()[0] == '.' || entry.IsDir() {
continue continue
} }
name, ext := SplitExt(entry.Name())
if !isAcceptedFile(name, ext) {
continue
}
if index := slices.Index(extraRequiredImages, name); index > -1 && isAcceptedImageExt(ext) {
extraRequiredImages = append(extraRequiredImages[:index], extraRequiredImages[index+1:]...)
}
if index := slices.Index(extraRequiredAudio, name); index > -1 && isAcceptedAudioExt(ext) {
extraRequiredAudio = append(extraRequiredAudio[:index], extraRequiredAudio[index+1:]...)
}
frompath := path.Join(roomFolder.Path, entry.Name())
topath := path.Join(roomoutpath, entry.Name()) topath := path.Join(roomoutpath, entry.Name())
err := copyFile(frompath, topath)
err := copyFile(entrypath, topath)
if err != nil { if err != nil {
return err return err
} }
} }
if len(extraRequiredImages) > 0 {
log.Printf("INFO: necessary optional images will use a transparent file: %v", extraRequiredImages)
transparent := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{1, 1}})
transparent.Set(0, 0, color.RGBA{0xff, 0xff, 0xff, 0x01})
for _, imagename := range extraRequiredImages {
imagepath := path.Join(roomoutpath, imagename+".PNG")
f, err := os.Create(imagepath)
if err != nil {
return err
}
png.Encode(f, transparent)
}
}
if len(extraRequiredAudio) > 0 {
log.Printf("INFO: necessary optional audios will use a silent file: %v", extraRequiredAudio)
for _, audioname := range extraRequiredAudio {
audiopath := path.Join(roomoutpath, audioname+".WAV")
err := os.WriteFile(audiopath, SILENCE_WAV, FILEPERM)
if err != nil {
return err
}
}
}
return nil return nil
} }

BIN
silence.wav Normal file

Binary file not shown.

29
util.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import (
"errors"
"io/fs"
"os"
"path"
"strings"
)
func Exists(p ...string) (bool, error) {
_, err := os.Stat(path.Join(p...))
if err == nil {
return true, nil
}
if errors.Is(err, fs.ErrNotExist) {
return false, nil
}
return false, err
}
func SplitExt(filename string) (string, string) {
ext := strings.ToUpper(path.Ext(filename))
name := strings.ToUpper(filename[:len(filename)-len(ext)])
if len(ext) > 0 {
ext = ext[1:]
}
return name, ext
}