Saving progress
Character position, data validation and a backup file.
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".
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.
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
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
- New data is written completely to
.tmpand flushed to disk. - Replacing an existing save moves its previous contents to
.bak. The first save has no backup yet. 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
| Result | Action |
|---|---|
Ok | Operation completed |
FileNotFound | The slot or selected backup does not exist yet |
Unconfigured | Assign a valid Target |
InvalidData | Check the schema, required field and type; leave current progress unchanged |
FileBadPath | Provide a nonempty valid file path in user:// |
FileNoPermission | Choose a writable path in user:// |
CantCreate | Check 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.