reckfellENGINE

Saving progress

Character position, data validation and a backup file.

On this page

Writing a position

ConfigFile stores values by section and key. SetValue() changes data in memory; Save() writes the file. Use user:// for player progress.

Place the methods below inside your C# script. player is an existing Entity3D in the game tree; path can be "user://progress.cfg".

C#
public Error SavePosition(Entity3D player, string path)
{
    using var data = new ConfigFile();
    data.SetValue("player", "position", player.GlobalPosition);
    return data.Save(path);
}

Check the result: Error.Ok means the write succeeded. This short version overwrites a single file without creating a backup.

Loading and validation

Load the file and check the value type first. On failure, the character stays at its current position. After teleporting, reset interpolation and clear velocity for a CharacterBody3D.

C#
public Error LoadPosition(Entity3D player, string path)
{
    using var data = new ConfigFile();
    Error error = data.Load(path);
    if (error != Error.Ok)
        return error;

    if (!data.HasSectionKey("player", "position"))
        return Error.InvalidData;
    using Variant value = data.GetValue("player", "position");
    if (value.VariantType != Variant.Type.Vector3)
        return Error.InvalidData;
    Vector3 position = value.AsVector3();
    if (!position.IsFinite())
        return Error.InvalidData;

    player.GlobalPosition = position;
    player.ResetPhysicsInterpolation();
    if (player is CharacterBody3D character)
        character.Velocity = Vector3.Zero;
    return Error.Ok;
}

A missing file returns Error.FileNotFound: this is normal on first launch. Do not automatically overwrite existing progress with initial data whenever loading fails.

Complete script with a backup

Add a plain Entity to the level, attach ProgressSave.cs, build the project and assign the character to Target. Add save_game on F2 and load_game on F3 in Input Map.

ProgressSave.cs — writing, schema validation and a backup
C#
using System;
using System.Text;
using Reckfell;
using IO = System.IO;

public partial class ProgressSave : Entity
{
    [Export] public Entity3D Target { get; set; } = null!;
    [Export] public string SavePath { get; set; } = "user://progress.cfg";

    public Error SaveProgress()
    {
        if (!ReckfellObject.IsInstanceValid(Target))
            return Error.Unconfigured;

        if (string.IsNullOrWhiteSpace(SavePath) || SavePath.IndexOf('\0') >= 0 ||
            !SavePath.StartsWith("user://", StringComparison.Ordinal) || SavePath.Length <= 7)
            return Error.FileBadPath;

        Vector3 position = Target.GlobalPosition;
        if (!position.IsFinite())
            return Error.InvalidData;

        using var data = new ConfigFile();
        data.SetValue("save", "format", 1);
        data.SetValue("player", "position", position);
        try
        {
            string path = ProjectSettings.GlobalizePath(SavePath);
            byte[] bytes = Encoding.UTF8.GetBytes(data.EncodeToText());
            using (var stream = new IO.FileStream(path + ".tmp", IO.FileMode.Create, IO.FileAccess.Write))
            {
                stream.Write(bytes);
                stream.Flush(flushToDisk: true);
            }

            if (IO.File.Exists(path))
                IO.File.Replace(path + ".tmp", path, path + ".bak");
            else
                IO.File.Move(path + ".tmp", path);

            return Error.Ok;
        }
        catch (UnauthorizedAccessException)
        {
            return Error.FileNoPermission;
        }
        catch (IO.IOException)
        {
            return Error.CantCreate;
        }
        catch (ArgumentException)
        {
            return Error.FileBadPath;
        }
        catch (NotSupportedException)
        {
            return Error.FileBadPath;
        }
    }

    public Error LoadProgress(bool backup = false)
    {
        if (!ReckfellObject.IsInstanceValid(Target))
            return Error.Unconfigured;

        if (string.IsNullOrWhiteSpace(SavePath) || SavePath.IndexOf('\0') >= 0 ||
            !SavePath.StartsWith("user://", StringComparison.Ordinal) || SavePath.Length <= 7)
            return Error.FileBadPath;

        string path = backup ? SavePath + ".bak" : SavePath;
        if (!IO.File.Exists(ProjectSettings.GlobalizePath(path)))
            return Error.FileNotFound;

        using var data = new ConfigFile();
        Error error = data.Load(path);
        if (error != Error.Ok)
            return error;

        using Variant format = data.GetValue("save", "format", 0);
        if (format.VariantType != Variant.Type.Int || format.AsInt64() != 1)
            return Error.InvalidData;
        if (!data.HasSectionKey("player", "position"))
            return Error.InvalidData;

        using Variant value = data.GetValue("player", "position");
        if (value.VariantType != Variant.Type.Vector3)
            return Error.InvalidData;

        Vector3 position = value.AsVector3();
        if (!position.IsFinite())
            return Error.InvalidData;

        Target.GlobalPosition = position;
        Target.ResetPhysicsInterpolation();
        if (Target is CharacterBody3D character)
            character.Velocity = Vector3.Zero;

        return Error.Ok;
    }

    public override void _UnhandledInput(InputEvent @event)
    {
        if (@event.IsActionPressed("save_game"))
            ReckfellApi.Print($"Save: {SaveProgress()}");
        else if (@event.IsActionPressed("load_game"))
            ReckfellApi.Print($"Load: {LoadProgress()}");
        else
            return;

        GetViewport().SetInputAsHandled();
    }
}

F2 saves the position; F3 restores it. Open the save directory through Project → Open User Data Folder.

File replacement

  1. New data is written completely to .tmp and flushed to disk.
  2. Replacing an existing save moves its previous contents to .bak. The first save has no backup yet.
  3. LoadProgress(backup: true) explicitly loads the backup. The example does not automatically restore a backup or autosave.

This deliberately uses System.IO.File.Replace(). Do not substitute DirAccess.Rename() or RenameAbsolute(): Windows 1.0.0 deletes an existing destination before attempting the move. With a missing source the destination can disappear even though the method returns an error. This was reproduced on disposable files; see DirAccess limitations.

For a missing .tmp and an exclusively locked slot, the tested File.Replace() preserved the primary file and .bak; the locked-slot case also retained the new .tmp. Do not automatically load staging files after a failure: they may be stale or incomplete. Resolve the cause and save again, or explicitly load a validated backup. The example does not support multiple simultaneous writers.

Flush(true) and a successful replacement do not prove power-loss recovery. Normal operations and the stated failure cases were tested on Windows; crashes, other filesystems and platforms remain untested here.

format = 1 identifies the save schema. Loading checks it, the required field, the Vector3 type and finite coordinates before changing the character.

For multiple levels, also store a level identifier. Do not apply one level's coordinates to another without checking. See changing levels.

Errors

ResultAction
OkOperation completed
FileNotFoundThe slot or selected backup does not exist yet
UnconfiguredAssign a valid Target
InvalidDataCheck the schema, required field and type; leave current progress unchanged
FileBadPathProvide a nonempty valid file path in user://
FileNoPermissionChoose a writable path in user://
CantCreateCheck the path, available space and file access

Writing is synchronous and intended for a small save slot. Large world snapshots need serialization and I/O organized separately. Backup replacement is verified on Windows x64.

Reference: ConfigFile, ProjectSettings.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.