wip save system

This commit is contained in:
Rowan 2025-05-08 00:35:30 -05:00
parent 0f1d693be6
commit ba9a169288
11 changed files with 227 additions and 22 deletions

View file

@ -62,6 +62,10 @@ advance_mode = 2
advance_mode = 2
advance_expression = "is_aiming()"
[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_qfm1y"]
advance_mode = 2
advance_expression = "not is_aiming()"
[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_oprun"]
advance_mode = 2
advance_expression = "is_firing()"
@ -70,18 +74,14 @@ advance_expression = "is_firing()"
switch_mode = 2
advance_mode = 2
[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_qfm1y"]
advance_mode = 2
advance_expression = "not is_aiming()"
[sub_resource type="AnimationNodeStateMachine" id="AnimationNodeStateMachine_3v2ag"]
states/Aim/node = SubResource("AnimationNodeAnimation_d2wvv")
states/Aim/position = Vector2(551, 100)
states/Fire/node = SubResource("AnimationNodeAnimation_3v2ag")
states/Fire/position = Vector2(687, 100)
states/Fire/position = Vector2(730, 100)
states/Movement/node = SubResource("AnimationNodeBlendTree_d2wvv")
states/Movement/position = Vector2(403, 100)
transitions = ["Start", "Movement", SubResource("AnimationNodeStateMachineTransition_jej6c"), "Movement", "Aim", SubResource("AnimationNodeStateMachineTransition_f1ej7"), "Aim", "Fire", SubResource("AnimationNodeStateMachineTransition_oprun"), "Fire", "Aim", SubResource("AnimationNodeStateMachineTransition_a8ls1"), "Aim", "Movement", SubResource("AnimationNodeStateMachineTransition_qfm1y")]
transitions = ["Start", "Movement", SubResource("AnimationNodeStateMachineTransition_jej6c"), "Movement", "Aim", SubResource("AnimationNodeStateMachineTransition_f1ej7"), "Aim", "Movement", SubResource("AnimationNodeStateMachineTransition_qfm1y"), "Aim", "Fire", SubResource("AnimationNodeStateMachineTransition_oprun"), "Fire", "Aim", SubResource("AnimationNodeStateMachineTransition_a8ls1")]
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_sk752"]
animation = &"Idle"
@ -162,7 +162,7 @@ tree_root = SubResource("AnimationNodeStateMachine_5lvsk")
advance_expression_base_node = NodePath("..")
anim_player = NodePath("../Mesh/AnimationPlayer")
parameters/Handgun/Movement/Blend2/blend_amount = 1.0
parameters/Handgun/Movement/BlendSpace1D/blend_position = 0.229277
parameters/Handgun/Movement/BlendSpace1D/blend_position = 0.5
parameters/Unarmed/blend_position = 0.872134
script = ExtResource("3_26yay")
move_speed_expression = "move_speed()"

View file

@ -0,0 +1,73 @@
import { GArray, GError, Node, Resource, ResourceLoader, ResourceSaver } from 'godot'
import { GodotError } from './godot_error'
export class ResourceLoadError extends Error { }
export default class AsyncResourceHandler extends Node {
private static _instance: AsyncResourceHandler
static readonly SaveFlags = ResourceSaver.SaverFlags
static readonly CacheMode = ResourceLoader.CacheMode
static get instance() {
return this._instance
}
_ready(): void {
AsyncResourceHandler._instance = this
}
static save<T extends Resource>(path: string, data: T, flags: ResourceSaver.SaverFlags): Promise<void> {
return this._instance.save(path, data, flags)
}
save<T extends Resource>(path: string, data: T, flags: ResourceSaver.SaverFlags): Promise<void> {
return new Promise((resolve, reject) => {
const err = ResourceSaver.save(data, path, flags)
if (err === GError.OK) {
resolve()
} else {
reject(err)
}
})
}
static load<T extends Resource>(path: string, hint_string: string = '', use_sub_threads?: boolean, cache_mode?: ResourceLoader.CacheMode): Promise<T> {
return this._instance.load(path, hint_string, use_sub_threads, cache_mode)
}
load<T extends Resource>(path: string, hint_string: string = '', use_sub_threads?: boolean, cache_mode?: ResourceLoader.CacheMode): Promise<T> {
return new Promise(async (resolve, reject) => {
const err = ResourceLoader.load_threaded_request(path, hint_string, use_sub_threads, cache_mode)
if (err != GError.OK) {
return reject(new GodotError(err))
}
const get_status = () => ResourceLoader.load_threaded_get_status(path)
let status = get_status()
while (status === ResourceLoader.ThreadLoadStatus.THREAD_LOAD_IN_PROGRESS) {
await this.get_tree().process_frame.as_promise()
status = get_status()
}
if (status === ResourceLoader.ThreadLoadStatus.THREAD_LOAD_LOADED) {
return resolve(ResourceLoader.load_threaded_get(path) as T)
} else {
return reject(new ResourceLoadError())
}
})
}
static get_progress(path: string): number {
return this._instance.get_progress(path)
}
get_progress(path: string): number {
const arr = new GArray()
ResourceLoader.load_threaded_get_status(path, arr)
return arr.get_indexed(0)
}
}

View file

@ -0,0 +1 @@
uid://byri2tpbtahpd

View file

@ -1,22 +1,39 @@
import { GError, Node, Resource, ResourceLoader } from 'godot'
import { GArray, GError, Node, Resource, ResourceLoader, ResourceSaver } from 'godot'
import { GodotError } from './godot_error'
export default class AsyncResourceLoader extends Node {
private static _instance: AsyncResourceLoader
export class ResourceLoadError extends Error { }
export default class AsyncResourceHandler extends Node {
private static _instance: AsyncResourceHandler
static readonly SaveFlags = ResourceSaver.SaverFlags
static readonly CacheMode = ResourceLoader.CacheMode
static get instance() {
return this._instance
}
_ready(): void {
AsyncResourceLoader._instance = this
AsyncResourceHandler._instance = this
}
load<T extends Resource>(path: string, hint_string: string = ''): Promise<T> {
save<T extends Resource>(path: string, data: T, flags: ResourceSaver.SaverFlags): Promise<void> {
return new Promise((resolve, reject) => {
const err = ResourceSaver.save(data, path, flags)
if (err === GError.OK) {
resolve()
} else {
reject(err)
}
})
}
load<T extends Resource>(path: string, hint_string: string = '', use_sub_threads?: boolean, cache_mode?: ResourceLoader.CacheMode): Promise<T> {
return new Promise(async (resolve, reject) => {
const err = ResourceLoader.load_threaded_request(path, hint_string)
const err = ResourceLoader.load_threaded_request(path, hint_string, use_sub_threads, cache_mode)
if (err != GError.OK) {
return reject(new Error(`failed to load ${path}`))
return reject(new GodotError(err))
}
const get_status = () => ResourceLoader.load_threaded_get_status(path)
@ -27,12 +44,18 @@ export default class AsyncResourceLoader extends Node {
status = get_status()
}
if (get_status() === ResourceLoader.ThreadLoadStatus.THREAD_LOAD_LOADED) {
if (status === ResourceLoader.ThreadLoadStatus.THREAD_LOAD_LOADED) {
return resolve(ResourceLoader.load_threaded_get(path) as T)
} else {
return reject(new Error(`failed to load ${path}`))
return reject(new ResourceLoadError())
}
})
}
get_progress(path: string): number {
const arr = new GArray()
ResourceLoader.load_threaded_get_status(path, arr)
return arr.get_indexed(0)
}
}

View file

@ -2,10 +2,10 @@ import { Color, GArray, Node, Node3D, PackedScene, PhysicsBody3D, PhysicsRayQuer
import { export_file, signal } from 'godot.annotations'
import { export_node, onready } from './annotations'
import Weapon from './weapon'
import AsyncResourceLoader from './async_resource_loader'
import AsyncResourceHandler from './async_resource_handler'
import { forward } from './vec'
import DebugDraw from './debug_draw'
import { find_in_anscestors, find_in_descendents, implements_interface } from './node'
import { find_in_anscestors, find_in_descendents, has_method } from './node'
import { Damageable } from './health'
const { MULTIPLY: mul, ADD: add } = Vector3
@ -36,7 +36,7 @@ export default class EquippedWeapon extends Node3D {
_ready(): void {
if (this.starting_weapon != null) {
AsyncResourceLoader.instance.load<Weapon>(this.starting_weapon, 'Weapon').then(weapon => this.equip(weapon))
AsyncResourceHandler.instance.load<Weapon>(this.starting_weapon, 'Weapon').then(weapon => this.equip(weapon))
}
}
@ -61,7 +61,7 @@ export default class EquippedWeapon extends Node3D {
}
equip(weapon: Weapon) {
AsyncResourceLoader.instance.load<PackedScene>(weapon.scene, 'PackedScene')
AsyncResourceHandler.instance.load<PackedScene>(weapon.scene, 'PackedScene')
.then(scene => this._parent_scene_to_transform(scene))
if (this._has_equipped_weapon) {
@ -86,7 +86,7 @@ export default class EquippedWeapon extends Node3D {
if (!result.is_empty()) {
DebugDraw.draw_ray(query, Color.DARK_RED)
const root = find_in_anscestors(result.get('collider'), n => n instanceof PhysicsBody3D)
const health = find_in_descendents<Node & Damageable>(root, n => implements_interface<Damageable>(['apply_damage'], n))
const health = find_in_descendents<Node & Damageable>(root, n => has_method('apply_damage', n))
if (health != null) {
health.apply_damage(this, this._equipped_weapon.damage)
}

12
src/godot_error.ts Normal file
View file

@ -0,0 +1,12 @@
import { GError } from "godot";
export class GodotError extends Error {
error: GError
constructor(err: GError, message?: string) {
super(message || `Godot error (${err})`)
this.error = err
}
}

1
src/godot_error.ts.uid Normal file
View file

@ -0,0 +1 @@
uid://o3gcj3wh78yt

View file

@ -11,8 +11,14 @@ export function queue_free_children<T extends Node>(node: T) {
}
}
type RequiredField<T, Field extends keyof T> = Omit<T, Field> & Required<Pick<T, Field>>
export function has_method(name: StringName, obj: Object): boolean {
return obj.has_method(name)
}
export function implements_interface<T>(method_names: StringName[], obj: Object): obj is Object & T {
return method_names.every(name => obj.has_method(name))
return method_names.every(name => has_method(name, obj))
}
export function find_in_anscestors<T extends Node>(root: T | null | undefined, predicate: Predicate<Node>, height: number = Infinity): T | null | undefined {

1
src/node.ts.uid Normal file
View file

@ -0,0 +1 @@
uid://d0npi365fttf6

87
src/save_serializer.ts Normal file
View file

@ -0,0 +1,87 @@
import { GDictionary, Node, PackedScene, Resource, ResourceLoader, ResourceSaver } from 'godot'
import AsyncResourceHandler from './async_resource_handler'
import { has_method } from './node'
interface Serializable {
on_save_game(save_data: GameState): void
on_before_load_game?(): void
on_load_game<T extends SaveState>(state: T): void
}
interface SaveState extends Resource {
scene: string
}
class PlayerState extends Resource implements SaveState {
condition: undefined = undefined
inventory: undefined = undefined
scene: string = ''
}
class ProgressState extends Resource {
variables?: GDictionary
}
export class GameState extends Resource {
player?: PlayerState
progress?: ProgressState
world?: SaveState[]
}
const StateMethod = {
Save: 'on_save_game',
BeforeLoad: 'on_before_load_game',
Load: 'on_load_game'
} as const
type StateMethod = typeof StateMethod[keyof typeof StateMethod]
export default class SaveSerializer extends Node {
private static _save_group: string = 'save'
private static _instance: SaveSerializer
_ready() {
SaveSerializer._instance = this
}
static save() {
this._instance.save()
}
private _announce(method: StateMethod, ...args: any) {
this.get_tree().call_group(
SaveSerializer._save_group,
method,
...args
)
}
save() {
const game_state: GameState = new GameState()
this._announce(StateMethod.Save, game_state)
ResourceSaver.save(game_state, 'user://save1.tres')
}
private async _restore_node<T extends SaveState>(state: T, parent: Node) {
const scene: PackedScene = await AsyncResourceHandler.load(state.scene, 'PackedScene')
const node = scene.instantiate()
parent.add_child(node)
if (has_method(StateMethod.Load, node)) {
// TODO: type check for state method
}
}
async load() {
const loaded_state: GameState = await AsyncResourceHandler.load('user://save1.tres')
this._announce(StateMethod.BeforeLoad)
// TODO: pass world root as 2nd param
const node_promises = loaded_state.world?.map(state => this._restore_node(state, this)) ?? []
return Promise.all(node_promises)
}
}

View file

@ -0,0 +1 @@
uid://xv4ix5esb3ij