+66
.tangled/workflows/build-publish.yaml
+66
.tangled/workflows/build-publish.yaml
···
1
+
when:
2
+
- event: ["push", "manual"]
3
+
branch: ["master"]
4
+
- event: ["push", "manual"]
5
+
branch: ["testing"]
6
+
7
+
engine: "nixery"
8
+
9
+
# using the default values
10
+
clone:
11
+
skip: false
12
+
depth: 1
13
+
submodules: false
14
+
15
+
dependencies:
16
+
# nixpkgs
17
+
nixpkgs:
18
+
- cargo
19
+
- dotnet-sdk
20
+
- mono
21
+
- wget
22
+
- unzip
23
+
- git
24
+
- gcc
25
+
26
+
steps:
27
+
# SETUP passwd
28
+
- name: "Create /etc/passwd"
29
+
command: "echo root:x:0:0::$PWD/home:/usr/bin/bash > /etc/passwd"
30
+
- name: "Create fake home"
31
+
command: "mkdir $PWD/home"
32
+
33
+
# GODOTSTEAM
34
+
- name: "Get GodotSteam"
35
+
command: "wget -nv https://codeberg.org/godotsteam/godotsteam/releases/download/v3.21/linux64-g352-s158-gs321.zip -O godotsteam.zip"
36
+
- name: "Unzip GodotSteam"
37
+
command: "unzip godotsteam.zip -d godotsteam"
38
+
- name: "Make GodotSteam executable"
39
+
command: "chmod +x godotsteam/linux-352-editor.64"
40
+
41
+
# GDWEAVE
42
+
- name: "Get GDWeave"
43
+
command: "wget -nv https://github.com/NotNite/GDWeave/releases/download/v2.0.14/GDWeave.zip -O gdweave.zip"
44
+
- name: "Unzip GDWeave"
45
+
command: "unzip gdweave.zip"
46
+
47
+
# Setup home dir
48
+
49
+
50
+
# MANIFESTATION
51
+
- name: "Install Manifestation"
52
+
command: "cargo install --git https://github.com/NotNite/manifestation.git"
53
+
54
+
- name: "Create Manifestation config dir"
55
+
command: "mkdir manifestation_config"
56
+
57
+
- name: "Add GodotSteam to manifestation config"
58
+
command: "echo godot_path = \\\"${PWD}/godotsteam/linux-352-editor.64\\\" > manifestation_config/config.toml"
59
+
- name: "Add GDWeave to manifestation config"
60
+
command: "echo gdweave_path = \\\"${PWD}/GDWeave\\\" >> manifestation_config/config.toml"
61
+
62
+
- name: "LOG"
63
+
command: "cat manifestation_config/config.toml"
64
+
65
+
- name: "Run manifestation"
66
+
command: "MANIFESTATION_CONFIG_DIR=$PWD/manifestation_config ~/.cargo/bin/manifestation ./manifestation.toml"
+37
Atproto/AtProtoSaveFactory.cs
+37
Atproto/AtProtoSaveFactory.cs
···
1
+
using GDWeave;
2
+
using GDWeave.Modding;
3
+
using Teemaw.Calico.LexicalTransformer;
4
+
5
+
namespace Atproto;
6
+
7
+
public class AtProtoSaveFactory
8
+
{
9
+
public static IScriptMod Create(IModInterface mod)
10
+
{
11
+
return new TransformationRuleScriptModBuilder()
12
+
.ForMod(mod)
13
+
.Named("AtProtoSave")
14
+
.Patching("res://Scenes/Singletons/UserSave/usersave.gdc")
15
+
.AddRule(new TransformationRuleBuilder()
16
+
.Named("ready_slot_condition")
17
+
.Matching(TransformationPatternFactory.CreateGdSnippetPattern("_load_save(last_loaded_slot)", 2))
18
+
.Do(Operation.ReplaceAll)
19
+
.With("""
20
+
var Atproto = $"/root/Atproto"
21
+
if last_loaded_slot != Atproto.ATPROTO_SLOT or Atproto.config.Autoload:
22
+
_load_save(last_loaded_slot)
23
+
else:
24
+
last_loaded_slot = -1
25
+
""", 2)
26
+
)
27
+
.AddRule(new TransformationRuleBuilder()
28
+
.Named("save_file")
29
+
.Matching(TransformationPatternFactory.CreateGdSnippetPattern(
30
+
"\"locked_refs\": PlayerData.locked_refs, \n\t}\n", 2))
31
+
.Do(Operation.Append)
32
+
.With("var atproto = $\"/root/Atproto\"\nif atproto.can_save_to_atproto():\n\tatproto.AtProtoClient.save_file()\n", 1)
33
+
)
34
+
35
+
.Build();
36
+
}
37
+
}
+26
Atproto/Atproto.csproj
+26
Atproto/Atproto.csproj
···
1
+
๏ปฟ<Project Sdk="Microsoft.NET.Sdk">
2
+
<PropertyGroup>
3
+
<TargetFramework>net8.0</TargetFramework>
4
+
<ImplicitUsings>enable</ImplicitUsings>
5
+
<Nullable>enable</Nullable>
6
+
<AssemblySearchPaths>$(AssemblySearchPaths);$(GDWeavePath)/core</AssemblySearchPaths>
7
+
<Version>1.0.2.0</Version>
8
+
<RootNamespace>Atproto</RootNamespace>
9
+
</PropertyGroup>
10
+
11
+
<ItemGroup>
12
+
<Reference Include="GDWeave" Private="false"/>
13
+
<Reference Include="Serilog" Private="false"/>
14
+
</ItemGroup>
15
+
16
+
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(GDWeavePath)' != ''">
17
+
<ItemGroup>
18
+
<_SourceFiles Include="$(OutDir)\**\*.*"/>
19
+
</ItemGroup>
20
+
21
+
<Copy
22
+
SourceFiles="@(_SourceFiles)"
23
+
DestinationFolder="$(GDWeavePath)/mods/$(AssemblyName)"
24
+
/>
25
+
</Target>
26
+
</Project>
+87
Atproto/CalicoUtils/LexicalTransformer/TransformationPatternFactory.cs
+87
Atproto/CalicoUtils/LexicalTransformer/TransformationPatternFactory.cs
···
1
+
๏ปฟusing GDWeave.Godot;
2
+
using Teemaw.Calico.Util;
3
+
using static GDWeave.Godot.TokenType;
4
+
5
+
namespace Teemaw.Calico.LexicalTransformer;
6
+
7
+
using MultiTokenPattern = Func<Token, bool>[];
8
+
9
+
public static class TransformationPatternFactory
10
+
{
11
+
/// <summary>
12
+
/// Creates a new pattern array which matches `extends <Identifier>\n`. Useful for patching into the global
13
+
/// scope.
14
+
/// </summary>
15
+
/// <returns></returns>
16
+
public static MultiTokenPattern CreateGlobalsPattern()
17
+
{
18
+
return
19
+
[
20
+
t => t.Type is PrExtends,
21
+
t => t.Type is Identifier,
22
+
t => t.Type is Newline
23
+
];
24
+
}
25
+
26
+
/// <summary>
27
+
/// Creates a new pattern array which matches a function definition. This will not match the preceding or trailing
28
+
/// Newline tokens.
29
+
/// </summary>
30
+
/// <param name="name">The name of the function to match.</param>
31
+
/// <param name="args">
32
+
/// An array of the names of arguments of the function to match. If null or empty, the returned checks will only
33
+
/// match a function which does not accept arguments.
34
+
/// </param>
35
+
/// <returns></returns>
36
+
public static MultiTokenPattern CreateFunctionDefinitionPattern(string name, string[]? args = null)
37
+
{
38
+
var checks = new List<Func<Token, bool>>();
39
+
40
+
checks.Add(t => t.Type is PrFunction);
41
+
checks.Add(t => t is IdentifierToken token && token.Name == name);
42
+
checks.Add(t => t.Type is ParenthesisOpen);
43
+
if (args is { Length: > 0 })
44
+
{
45
+
foreach (var arg in args)
46
+
{
47
+
checks.Add(t => t is IdentifierToken token && token.Name == arg);
48
+
checks.Add(t => t.Type is Comma);
49
+
}
50
+
51
+
checks.RemoveAt(checks.Count - 1);
52
+
}
53
+
54
+
checks.Add(t => t.Type is ParenthesisClose);
55
+
checks.Add(t => t.Type is Colon);
56
+
57
+
return checks.ToArray();
58
+
}
59
+
60
+
/// <summary>
61
+
/// Creates a new pattern array which matches the provided GDScript snippet.
62
+
/// </summary>
63
+
/// <param name="snippet">A GDScript snippet to match.</param>
64
+
/// <param name="indent">The base indent of the snippet.</param>
65
+
/// <returns></returns>
66
+
public static MultiTokenPattern CreateGdSnippetPattern(string snippet, uint indent = 0)
67
+
{
68
+
var tokens = ScriptTokenizer.Tokenize(snippet, indent);
69
+
70
+
return tokens.Select(snippetToken => (Func<Token, bool>)(t =>
71
+
{
72
+
if (t.Type == Identifier)
73
+
{
74
+
return snippetToken is IdentifierToken snippetIdentifier && t is IdentifierToken identifier &&
75
+
snippetIdentifier.Name == identifier.Name;
76
+
}
77
+
78
+
if (t.Type == Constant)
79
+
{
80
+
return snippetToken is ConstantToken snippetConstant && t is ConstantToken constant &&
81
+
snippetConstant.Value.Equals(constant.Value);
82
+
}
83
+
84
+
return t.Type == snippetToken.Type;
85
+
})).ToArray();
86
+
}
87
+
}
+231
Atproto/CalicoUtils/LexicalTransformer/TransformationRule.cs
+231
Atproto/CalicoUtils/LexicalTransformer/TransformationRule.cs
···
1
+
๏ปฟusing GDWeave.Godot;
2
+
using GDWeave.Modding;
3
+
using ScriptTokenizer = Teemaw.Calico.Util.ScriptTokenizer;
4
+
5
+
namespace Teemaw.Calico.LexicalTransformer;
6
+
7
+
using MultiTokenPattern = Func<Token, bool>[];
8
+
9
+
public enum Operation
10
+
{
11
+
/// <summary>
12
+
/// Do not patch.
13
+
/// </summary>
14
+
None,
15
+
16
+
/// <summary>
17
+
/// Replace all tokens of the waiter. This is a buffered operation. Buffered rules do not support overlapping token
18
+
/// patterns with other buffered rules.
19
+
/// </summary>
20
+
ReplaceAll,
21
+
22
+
/// <summary>
23
+
/// Replace the final token of the waiter.
24
+
/// </summary>
25
+
ReplaceLast,
26
+
27
+
/// <summary>
28
+
/// Appends after the final token of the waiter.
29
+
/// </summary>
30
+
Append,
31
+
32
+
/// <summary>
33
+
/// Prepends before the first token of the waiter. This is a buffered operation. Buffered rules do not support
34
+
/// overlapping token patterns with other buffered rules.
35
+
/// </summary>
36
+
Prepend,
37
+
}
38
+
39
+
public static class OperationExtensions
40
+
{
41
+
public static bool RequiresBuffer(this Operation operation)
42
+
{
43
+
return operation is Operation.ReplaceAll or Operation.Prepend;
44
+
}
45
+
46
+
public static bool YieldTokenBeforeOperation(this Operation operation)
47
+
{
48
+
return operation is Operation.Append or Operation.None;
49
+
}
50
+
51
+
public static bool YieldTokenAfterOperation(this Operation operation)
52
+
{
53
+
return !operation.RequiresBuffer() && !operation.YieldTokenBeforeOperation() &&
54
+
operation != Operation.ReplaceLast;
55
+
}
56
+
}
57
+
58
+
/// <summary>
59
+
/// This holds the information required to perform a patch at a single locus.
60
+
/// </summary>
61
+
/// <param name="Name">The name of this descriptor. Used for logging.</param>
62
+
/// <param name="Pattern">A list of checks to be used in a MultiTokenWaiter.</param>
63
+
/// <param name="ScopePattern">A list of checks to be used in a MultiTokenWaiter marking the .</param>
64
+
/// <param name="Tokens">A list of GDScript tokens which will be patched in.</param>
65
+
/// <param name="Operation">The type of patch.</param>
66
+
/// <param name="Times">The number of times this rule is expected to match.</param>
67
+
/// <param name="Predicate">A predicate which must return true for the rule to match.</param>
68
+
public record TransformationRule(
69
+
string Name,
70
+
MultiTokenPattern Pattern,
71
+
MultiTokenPattern ScopePattern,
72
+
IEnumerable<Token> Tokens,
73
+
Operation Operation,
74
+
uint Times,
75
+
Func<bool> Predicate)
76
+
{
77
+
78
+
public MultiTokenWaiter CreateMultiTokenWaiter() => new(Pattern);
79
+
80
+
public MultiTokenWaiter CreateMultiTokenWaiterForScope() => new(ScopePattern);
81
+
}
82
+
83
+
/// <summary>
84
+
/// Builder for TransformationRule. Times defaults to 1. Operation defaults to <see cref="Operation.Append"/>.
85
+
/// </summary>
86
+
public class TransformationRuleBuilder
87
+
{
88
+
private string? _name;
89
+
private MultiTokenPattern? _pattern;
90
+
private MultiTokenPattern _scopePattern = [];
91
+
private IEnumerable<Token>? _tokens;
92
+
private uint _times = 1;
93
+
private Operation _operation = Operation.Append;
94
+
private Func<bool> _predicate = () => true;
95
+
96
+
/// <summary>
97
+
/// Sets the name for the TransformationRule. Used for logging.
98
+
/// </summary>
99
+
/// <param name="name"></param>
100
+
/// <returns></returns>
101
+
public TransformationRuleBuilder Named(string name)
102
+
{
103
+
_name = name;
104
+
return this;
105
+
}
106
+
107
+
/// <summary>
108
+
/// Sets the token pattern which will be matched by the TransformationRule.
109
+
/// </summary>
110
+
/// <param name="pattern"></param>
111
+
/// <returns></returns>
112
+
public TransformationRuleBuilder Matching(MultiTokenPattern pattern)
113
+
{
114
+
_pattern = pattern;
115
+
return this;
116
+
}
117
+
118
+
/// <summary>
119
+
/// Sets the token content which will be patched in for the TransformationRule.
120
+
/// </summary>
121
+
/// <param name="tokens"></param>
122
+
/// <returns></returns>
123
+
public TransformationRuleBuilder With(IEnumerable<Token> tokens)
124
+
{
125
+
_tokens = tokens;
126
+
return this;
127
+
}
128
+
129
+
/// <summary>
130
+
/// Sets the token content which will be patched in for the TransformationRule.
131
+
/// </summary>
132
+
/// <param name="token"></param>
133
+
/// <returns></returns>
134
+
public TransformationRuleBuilder With(Token token)
135
+
{
136
+
_tokens = [token];
137
+
return this;
138
+
}
139
+
140
+
/// <summary>
141
+
/// Sets the token content which will be patched in for the TransformationRule with a GDScript snippet.
142
+
/// </summary>
143
+
/// <param name="snippet"></param>
144
+
/// <param name="indent">The base indentation level for the tokenizer.</param>
145
+
/// <returns></returns>
146
+
public TransformationRuleBuilder With(string snippet, uint indent = 0)
147
+
{
148
+
_tokens = ScriptTokenizer.Tokenize(snippet, indent);
149
+
return this;
150
+
}
151
+
152
+
/// <summary>
153
+
/// Sets the <see cref="Operation"/> of the TransformationRule.
154
+
/// </summary>
155
+
/// <param name="operation"></param>
156
+
/// <returns></returns>
157
+
public TransformationRuleBuilder Do(Operation operation)
158
+
{
159
+
_operation = operation;
160
+
return this;
161
+
}
162
+
163
+
/// <summary>
164
+
/// Sets the number of times the rule is expected to match.
165
+
/// </summary>
166
+
/// <param name="times"></param>
167
+
/// <returns></returns>
168
+
public TransformationRuleBuilder ExpectTimes(uint times)
169
+
{
170
+
_times = times;
171
+
return this;
172
+
}
173
+
174
+
/// <summary>
175
+
/// Sets the scope of this rule.
176
+
/// </summary>
177
+
/// <param name="scopePattern"></param>
178
+
/// <returns></returns>
179
+
public TransformationRuleBuilder ScopedTo(MultiTokenPattern scopePattern)
180
+
{
181
+
_scopePattern = scopePattern;
182
+
return this;
183
+
}
184
+
185
+
/// <summary>
186
+
/// Sets the predicate function whose return value will decide if this rule will be checked.
187
+
/// </summary>
188
+
/// <param name="predicate"></param>
189
+
/// <returns></returns>
190
+
public TransformationRuleBuilder When(Func<bool> predicate)
191
+
{
192
+
_predicate = predicate;
193
+
return this;
194
+
}
195
+
196
+
/// <summary>
197
+
/// Sets a value which will decide if this rule will be checked.
198
+
/// </summary>
199
+
/// <param name="eligible">If true, this rule will be checked.</param>
200
+
/// <returns></returns>
201
+
public TransformationRuleBuilder When(bool eligible)
202
+
{
203
+
_predicate = () => eligible;
204
+
return this;
205
+
}
206
+
207
+
/// <summary>
208
+
/// Builds the TransformationRule.
209
+
/// </summary>
210
+
/// <returns></returns>
211
+
/// <exception cref="ArgumentNullException">Thrown if any required fields were not set.</exception>
212
+
public TransformationRule Build()
213
+
{
214
+
if (string.IsNullOrEmpty(_name))
215
+
{
216
+
throw new ArgumentNullException(nameof(_name), "Name cannot be null or empty");
217
+
}
218
+
219
+
if (_pattern == null)
220
+
{
221
+
throw new ArgumentNullException(nameof(_pattern), "Pattern cannot be null");
222
+
}
223
+
224
+
if (_tokens == null)
225
+
{
226
+
throw new ArgumentNullException(nameof(_tokens), "Tokens cannot be null");
227
+
}
228
+
229
+
return new TransformationRule(_name, _pattern, _scopePattern, _tokens, _operation, _times, _predicate);
230
+
}
231
+
}
+179
Atproto/CalicoUtils/LexicalTransformer/TransformationRuleScriptMod.cs
+179
Atproto/CalicoUtils/LexicalTransformer/TransformationRuleScriptMod.cs
···
1
+
๏ปฟusing GDWeave;
2
+
using GDWeave.Godot;
3
+
using GDWeave.Modding;
4
+
using static Teemaw.Calico.LexicalTransformer.Operation;
5
+
6
+
namespace Teemaw.Calico.LexicalTransformer;
7
+
8
+
/// <summary>
9
+
/// An IScriptMod implementation that handles patching through the provided list of TransformationRules.
10
+
/// </summary>
11
+
/// <param name="mod">IModInterface of the current mod.</param>
12
+
/// <param name="name">The name of this script mod. Used for logging.</param>
13
+
/// <param name="scriptPath">The GD res:// path of the script which will be patched.</param>
14
+
/// <param name="rules">A list of patches to perform. Multiple descriptors with overlapping checks is not supported.</param>
15
+
public class TransformationRuleScriptMod(
16
+
IModInterface mod,
17
+
string name,
18
+
string scriptPath,
19
+
Func<bool> predicate,
20
+
TransformationRule[] rules)
21
+
: IScriptMod
22
+
{
23
+
public bool ShouldRun(string path)
24
+
{
25
+
if (path != scriptPath) return false;
26
+
if (!predicate.Invoke())
27
+
{
28
+
mod.Logger.Information($"[{name}] Predicate failed, not patching.");
29
+
return false;
30
+
}
31
+
32
+
return true;
33
+
}
34
+
35
+
public IEnumerable<Token> Modify(string path, IEnumerable<Token> tokens)
36
+
{
37
+
var eligibleRules = rules.Where(rule =>
38
+
{
39
+
var eligible = rule.Predicate();
40
+
if (!eligible)
41
+
{
42
+
mod.Logger.Information($"[{name}] Skipping patch {rule.Name}...");
43
+
}
44
+
45
+
return eligible;
46
+
}).ToList();
47
+
var transformers = eligibleRules.Select(rule =>
48
+
(Rule: rule, Waiter: rule.CreateMultiTokenWaiter(), Buffer: new List<Token>()))
49
+
.ToList();
50
+
mod.Logger.Information($"[{name}] Patching {path}");
51
+
52
+
var patchOccurrences =
53
+
eligibleRules.ToDictionary(r => r.Name, r => (Occurred: 0, Expected: r.Times));
54
+
var bufferAfterChecks = true;
55
+
var stagingBuffer = new List<Token>();
56
+
stagingBuffer.AddRange(tokens);
57
+
var transformedBuffer = new List<Token>();
58
+
59
+
foreach (var transformer in transformers)
60
+
{
61
+
var hasScopePattern = transformer.Rule.ScopePattern.Length > 0;
62
+
var inScope = !hasScopePattern;
63
+
uint? scopeIndent = null;
64
+
var scopeWaiter = transformer.Rule.CreateMultiTokenWaiterForScope();
65
+
66
+
foreach (var token in stagingBuffer)
67
+
{
68
+
if (inScope && hasScopePattern)
69
+
{
70
+
// Try to find the scope's base indentation
71
+
if (scopeIndent == null && token.Type == TokenType.Newline)
72
+
{
73
+
scopeIndent = token.AssociatedData ?? 0;
74
+
}
75
+
// Check if we should leave the scope
76
+
else if (scopeIndent != null && token.Type == TokenType.Newline)
77
+
{
78
+
inScope = (token.AssociatedData ?? 0) >= scopeIndent;
79
+
}
80
+
}
81
+
else if (hasScopePattern)
82
+
{
83
+
// We should never reach this case if the scope pattern has Length = 0.
84
+
if (scopeWaiter.Check(token))
85
+
{
86
+
scopeWaiter.Reset();
87
+
// Latch inScope to true if we match the scope pattern.
88
+
inScope = true;
89
+
}
90
+
}
91
+
92
+
if (!inScope)
93
+
{
94
+
transformedBuffer.Add(token);
95
+
continue;
96
+
}
97
+
98
+
transformer.Waiter.Check(token);
99
+
if (transformer.Waiter.Step == 0)
100
+
{
101
+
transformedBuffer.AddRange(transformer.Buffer);
102
+
transformer.Buffer.Clear();
103
+
}
104
+
else
105
+
{
106
+
if (transformer.Rule.Operation.RequiresBuffer())
107
+
{
108
+
transformer.Buffer.Add(token);
109
+
bufferAfterChecks = false;
110
+
}
111
+
112
+
if (transformer.Waiter.Matched)
113
+
{
114
+
transformer.Waiter.Reset();
115
+
116
+
if (transformer.Rule.Operation.YieldTokenBeforeOperation())
117
+
{
118
+
transformedBuffer.Add(token);
119
+
bufferAfterChecks = false;
120
+
}
121
+
else
122
+
{
123
+
bufferAfterChecks =
124
+
transformer.Rule.Operation.YieldTokenAfterOperation();
125
+
}
126
+
127
+
switch (transformer.Rule.Operation)
128
+
{
129
+
case Prepend:
130
+
transformedBuffer.AddRange(transformer.Rule.Tokens);
131
+
transformedBuffer.AddRange(transformer.Buffer);
132
+
transformer.Buffer.Clear();
133
+
break;
134
+
case ReplaceLast:
135
+
case Append:
136
+
case ReplaceAll:
137
+
transformer.Buffer.Clear();
138
+
transformedBuffer.AddRange(transformer.Rule.Tokens);
139
+
break;
140
+
case None:
141
+
default:
142
+
break;
143
+
}
144
+
145
+
mod.Logger.Information(
146
+
$"[{name}] Patch {transformer.Rule.Name} OK!");
147
+
patchOccurrences[transformer.Rule.Name] =
148
+
patchOccurrences[transformer.Rule.Name] with
149
+
{
150
+
Occurred = patchOccurrences[transformer.Rule.Name].Occurred + 1
151
+
};
152
+
}
153
+
}
154
+
155
+
if (bufferAfterChecks)
156
+
{
157
+
transformedBuffer.Add(token);
158
+
}
159
+
else
160
+
{
161
+
bufferAfterChecks = true;
162
+
}
163
+
}
164
+
165
+
stagingBuffer.Clear();
166
+
stagingBuffer.AddRange(transformedBuffer);
167
+
transformedBuffer.Clear();
168
+
}
169
+
170
+
foreach (var result in patchOccurrences.Where(result =>
171
+
result.Value.Occurred != result.Value.Expected))
172
+
{
173
+
mod.Logger.Error(
174
+
$"[{name}] Patch {result.Key} FAILED! Times expected={result.Value.Expected}, actual={result.Value.Occurred}");
175
+
}
176
+
177
+
return stagingBuffer;
178
+
}
179
+
}
+112
Atproto/CalicoUtils/LexicalTransformer/TransformationRuleScriptModBuilder.cs
+112
Atproto/CalicoUtils/LexicalTransformer/TransformationRuleScriptModBuilder.cs
···
1
+
๏ปฟusing GDWeave;
2
+
3
+
namespace Teemaw.Calico.LexicalTransformer;
4
+
5
+
public class TransformationRuleScriptModBuilder
6
+
{
7
+
private IModInterface? _mod;
8
+
private string? _name;
9
+
private string? _scriptPath;
10
+
private Func<bool> _predicate = () => true;
11
+
private List<TransformationRule> _rules = [];
12
+
13
+
/// <summary>
14
+
/// Sets the IModInterface to be used by the ScriptMod.
15
+
/// </summary>
16
+
/// <param name="mod"></param>
17
+
/// <returns></returns>
18
+
public TransformationRuleScriptModBuilder ForMod(IModInterface mod)
19
+
{
20
+
_mod = mod;
21
+
return this;
22
+
}
23
+
24
+
/// <summary>
25
+
/// Sets the predicate which must return true for this ScriptMod to run. Config options MUST be
26
+
/// evaluated at patch-time since the environment is not fully known at load-time. Currently,
27
+
/// this affects mod conflict detection, but there may be other affected features in the future.
28
+
/// </summary>
29
+
/// <param name="predicate"></param>
30
+
/// <returns></returns>
31
+
public TransformationRuleScriptModBuilder When(Func<bool> predicate)
32
+
{
33
+
_predicate = predicate;
34
+
return this;
35
+
}
36
+
37
+
/// <summary>
38
+
/// Sets the name of the ScriptMod. Used for logging.
39
+
/// </summary>
40
+
/// <param name="name"></param>
41
+
/// <returns></returns>
42
+
public TransformationRuleScriptModBuilder Named(string name)
43
+
{
44
+
_name = name;
45
+
return this;
46
+
}
47
+
48
+
/// <summary>
49
+
/// Sets the Godot resource path of the script to be patched.
50
+
/// </summary>
51
+
/// <param name="scriptPath"></param>
52
+
/// <returns></returns>
53
+
public TransformationRuleScriptModBuilder Patching(string scriptPath)
54
+
{
55
+
_scriptPath = scriptPath;
56
+
return this;
57
+
}
58
+
59
+
/// <summary>
60
+
/// Adds a TransformationRule to the TransformationRuleScriptMod.
61
+
/// </summary>
62
+
/// <param name="rule"></param>
63
+
/// <returns></returns>
64
+
public TransformationRuleScriptModBuilder AddRule(TransformationRule rule)
65
+
{
66
+
if (_rules.Select(r => r.Name).Contains(rule.Name))
67
+
{
68
+
throw new InvalidOperationException(
69
+
$"Another rule with the name '{rule.Name}' already exists!");
70
+
}
71
+
72
+
_rules.Add(rule);
73
+
return this;
74
+
}
75
+
76
+
/// <summary>
77
+
/// Adds the TransformationRule built by calling Build() on the provided builder to the TransformationRuleScriptMod.
78
+
/// </summary>
79
+
/// <param name="rule"></param>
80
+
/// <returns></returns>
81
+
public TransformationRuleScriptModBuilder AddRule(TransformationRuleBuilder rule)
82
+
{
83
+
return AddRule(rule.Build());
84
+
}
85
+
86
+
/// <summary>
87
+
/// Build the TransformationRuleScriptMod.
88
+
/// </summary>
89
+
/// <returns></returns>
90
+
/// <exception cref="ArgumentNullException">Thrown if any required fields were not set.</exception>
91
+
public TransformationRuleScriptMod Build()
92
+
{
93
+
if (_mod == null)
94
+
{
95
+
throw new ArgumentNullException(nameof(_mod), "Mod cannot be null");
96
+
}
97
+
98
+
if (string.IsNullOrEmpty(_name))
99
+
{
100
+
throw new ArgumentNullException(nameof(_name), "Name cannot be null or empty");
101
+
}
102
+
103
+
if (string.IsNullOrEmpty(_scriptPath))
104
+
{
105
+
throw new ArgumentNullException(nameof(_scriptPath),
106
+
"Script path cannot be null or empty");
107
+
}
108
+
109
+
return new TransformationRuleScriptMod(_mod, _name, _scriptPath, _predicate,
110
+
_rules.ToArray());
111
+
}
112
+
}
+507
Atproto/CalicoUtils/Util/ScriptTokenizer.cs
+507
Atproto/CalicoUtils/Util/ScriptTokenizer.cs
···
1
+
๏ปฟ// MIT License
2
+
//
3
+
// Copyright (c) 2024 NotNite
4
+
//
5
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6
+
// of this software and associated documentation files (the "Software"), to deal
7
+
// in the Software without restriction, including without limitation the rights
8
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+
// copies of the Software, and to permit persons to whom the Software is
10
+
// furnished to do so, subject to the following conditions:
11
+
//
12
+
// The above copyright notice and this permission notice shall be included in all
13
+
// copies or substantial portions of the Software.
14
+
//
15
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+
// SOFTWARE.
22
+
23
+
using System.Text;
24
+
using GDWeave.Godot;
25
+
using GDWeave.Godot.Variants;
26
+
27
+
namespace Teemaw.Calico.Util;
28
+
29
+
/**
30
+
* This is copied from https://github.com/NotNite/GDWeave/blob/main/GDWeave/Script/ScriptTokenizer.cs since this class
31
+
* was not made visible. Minor modifications have been made to make it more ergonomic to tokenize snippets.
32
+
*/
33
+
public static class ScriptTokenizer {
34
+
private static readonly Dictionary<string, TokenType> Tokens = new() {
35
+
{"continue", TokenType.CfContinue},
36
+
{"return", TokenType.CfReturn},
37
+
{"break", TokenType.CfBreak},
38
+
{"match", TokenType.CfMatch},
39
+
{"while", TokenType.CfWhile},
40
+
{"elif", TokenType.CfElif},
41
+
{"else", TokenType.CfElse},
42
+
{"pass", TokenType.CfPass},
43
+
{"for", TokenType.CfFor},
44
+
{"if", TokenType.CfIf},
45
+
{"const", TokenType.PrConst},
46
+
{"var", TokenType.PrVar},
47
+
{"func", TokenType.PrFunction},
48
+
{"class", TokenType.PrClass},
49
+
{"extends", TokenType.PrExtends},
50
+
{"is", TokenType.PrIs},
51
+
{"as", TokenType.PrAs},
52
+
{"@onready", TokenType.PrOnready},
53
+
{"@tool", TokenType.PrTool},
54
+
{"@export", TokenType.PrExport},
55
+
// CALICO:
56
+
{"yield", TokenType.PrYield},
57
+
58
+
{"setget", TokenType.PrSetget},
59
+
{"static", TokenType.PrStatic},
60
+
61
+
{"void", TokenType.PrVoid},
62
+
{"enum", TokenType.PrEnum},
63
+
{"preload", TokenType.PrPreload},
64
+
{"assert", TokenType.PrAssert},
65
+
66
+
{"signal", TokenType.PrSignal},
67
+
{"breakpoint", TokenType.PrBreakpoint},
68
+
69
+
{"sync", TokenType.PrSync},
70
+
{"remote", TokenType.PrRemote},
71
+
{"master", TokenType.PrMaster},
72
+
{"slave", TokenType.PrSlave},
73
+
{"puppet", TokenType.PrPuppet},
74
+
75
+
{"remotesync", TokenType.PrRemotesync},
76
+
{"mastersync", TokenType.PrMastersync},
77
+
{"puppetsync", TokenType.PrPuppetsync},
78
+
79
+
{"\n", TokenType.Newline},
80
+
81
+
{"PI", TokenType.ConstPi},
82
+
{"TAU", TokenType.ConstTau},
83
+
{"INF", TokenType.ConstInf},
84
+
{"NAN", TokenType.ConstNan},
85
+
86
+
{"error", TokenType.Error},
87
+
{"cursor", TokenType.Cursor},
88
+
89
+
{"self", TokenType.Self},
90
+
91
+
{"in", TokenType.OpIn},
92
+
93
+
{"_", TokenType.Wildcard},
94
+
95
+
{"[", TokenType.BracketOpen},
96
+
{"]", TokenType.BracketClose},
97
+
{"{", TokenType.CurlyBracketOpen},
98
+
{"}", TokenType.CurlyBracketClose},
99
+
100
+
{"(", TokenType.ParenthesisOpen},
101
+
{")", TokenType.ParenthesisClose},
102
+
103
+
{",", TokenType.Comma},
104
+
{";", TokenType.Semicolon},
105
+
{".", TokenType.Period},
106
+
{"?", TokenType.QuestionMark},
107
+
{":", TokenType.Colon},
108
+
{"$", TokenType.Dollar},
109
+
{"->", TokenType.ForwardArrow},
110
+
111
+
{">>=", TokenType.OpAssignShiftRight},
112
+
{"<<=", TokenType.OpAssignShiftLeft},
113
+
114
+
{">>", TokenType.OpShiftRight},
115
+
{"<<", TokenType.OpShiftLeft},
116
+
117
+
{"==", TokenType.OpEqual},
118
+
{"!=", TokenType.OpNotEqual},
119
+
{"&&", TokenType.OpAnd},
120
+
{"||", TokenType.OpOr},
121
+
{"!", TokenType.OpNot},
122
+
123
+
{"+=", TokenType.OpAssignAdd},
124
+
{"-=", TokenType.OpAssignSub},
125
+
{"*=", TokenType.OpAssignMul},
126
+
{"/=", TokenType.OpAssignDiv},
127
+
{"%=", TokenType.OpAssignMod},
128
+
{"&=", TokenType.OpAssignBitAnd},
129
+
{"|=", TokenType.OpAssignBitOr},
130
+
{"^=", TokenType.OpAssignBitXor},
131
+
132
+
{"+", TokenType.OpAdd},
133
+
{"-", TokenType.OpSub},
134
+
{"*", TokenType.OpMul},
135
+
{"/", TokenType.OpDiv},
136
+
{"%", TokenType.OpMod},
137
+
138
+
{"~", TokenType.OpBitInvert},
139
+
{"&", TokenType.OpBitAnd},
140
+
{"|", TokenType.OpBitOr},
141
+
{"^", TokenType.OpBitXor},
142
+
143
+
{"<=", TokenType.OpLessEqual},
144
+
{">=", TokenType.OpGreaterEqual},
145
+
{"<", TokenType.OpLess},
146
+
{">", TokenType.OpGreater},
147
+
148
+
{"=", TokenType.OpAssign},
149
+
};
150
+
151
+
private static readonly HashSet<string> Symbols = new() {
152
+
"->",
153
+
154
+
">>=",
155
+
"<<=",
156
+
157
+
">>",
158
+
"<<",
159
+
160
+
"==",
161
+
"!=",
162
+
"&&",
163
+
"||",
164
+
"!",
165
+
166
+
"+=",
167
+
"-=",
168
+
"*=",
169
+
"/=",
170
+
"%=",
171
+
"&=",
172
+
"|=",
173
+
"^=",
174
+
175
+
"_",
176
+
177
+
"[",
178
+
"]",
179
+
180
+
"{",
181
+
"}",
182
+
183
+
"(",
184
+
")",
185
+
186
+
",",
187
+
";",
188
+
".",
189
+
"?",
190
+
":",
191
+
"$",
192
+
"+",
193
+
"-",
194
+
"*",
195
+
"/",
196
+
"%",
197
+
198
+
"~",
199
+
"&",
200
+
"|",
201
+
"^",
202
+
203
+
"<=",
204
+
">=",
205
+
"<",
206
+
">",
207
+
208
+
"=",
209
+
};
210
+
211
+
private static readonly List<string> BuiltinFunctions = Enum.GetNames<BuiltinFunction>().ToList();
212
+
213
+
private static void InsertNewLine(IEnumerator<string> enumerator, uint baseIndent, List<Token> toFlush) {
214
+
if (!enumerator.MoveNext()) {
215
+
return;
216
+
}
217
+
218
+
var tabCount = uint.Parse(enumerator.Current);
219
+
toFlush.Add(new Token(TokenType.Newline, tabCount + baseIndent));
220
+
}
221
+
222
+
private static void BuildIdentifierName(IEnumerator<string> enumerator, List<Token> toFlush, out string? found) {
223
+
found = string.Empty;
224
+
if (!enumerator.MoveNext()) {
225
+
return;
226
+
}
227
+
228
+
if (enumerator.Current == ":") {
229
+
toFlush.Add(new Token(TokenType.Wildcard));
230
+
toFlush.Add(new Token(TokenType.Semicolon));
231
+
return;
232
+
}
233
+
234
+
found = "_" + enumerator.Current;
235
+
}
236
+
237
+
private static void BuildNumber(IEnumerator<string> enumerator, List<Token> toFlush, out bool foundFull) {
238
+
foundFull = true;
239
+
int sign = 1;
240
+
241
+
if (enumerator.Current == "-") {
242
+
sign = -1;
243
+
if (!enumerator.MoveNext()) return;
244
+
}
245
+
246
+
if (!long.TryParse(enumerator.Current, out long upper)) {
247
+
toFlush.Add(new Token(TokenType.OpSub));
248
+
foundFull = false;
249
+
return;
250
+
}
251
+
252
+
if (!enumerator.MoveNext()) return;
253
+
254
+
if (enumerator.Current != ".") {
255
+
toFlush.Add(new ConstantToken(new IntVariant(upper * sign)));
256
+
foundFull = false;
257
+
return;
258
+
}
259
+
260
+
if (!enumerator.MoveNext()) return;
261
+
262
+
if (!long.TryParse(enumerator.Current, out long lower)) {
263
+
// I dont think there is really a proper return for here.
264
+
// You'd have a number that looks like this "1000."
265
+
// No following decimal
266
+
// Comment if you had ideas
267
+
return;
268
+
}
269
+
270
+
var result = upper + (lower / Math.Pow(10, lower.ToString().Length));
271
+
toFlush.Add(new ConstantToken(new RealVariant(result * sign)));
272
+
}
273
+
274
+
public static IEnumerable<Token> Tokenize(string gdScript, uint baseIndent = 0) {
275
+
var finalTokens = new List<Token>();
276
+
var tokens = SanitizeInput(TokenizeString(gdScript + " "));
277
+
278
+
var previous = string.Empty;
279
+
var idName = string.Empty;
280
+
281
+
var toFlush = new List<Token>(2);
282
+
// CALICO: We don't need this since we're dealing with snippets
283
+
//finalTokens.Add(new Token(TokenType.Newline, baseIndent));
284
+
var enumerator = tokens.GetEnumerator();
285
+
var reparse = false;
286
+
while (reparse ? true : enumerator.MoveNext()) {
287
+
reparse = false;
288
+
289
+
if (enumerator.Current == "\n") {
290
+
InsertNewLine(enumerator, baseIndent, toFlush);
291
+
endAndFlushId();
292
+
continue;
293
+
}
294
+
295
+
if (enumerator.Current == "_") {
296
+
BuildIdentifierName(enumerator, toFlush, out string? found);
297
+
if (found == string.Empty) {
298
+
endAndFlushId();
299
+
continue;
300
+
}
301
+
302
+
idName += found;
303
+
304
+
end();
305
+
continue;
306
+
}
307
+
308
+
//if (enumerator.Current == "-" || char.IsDigit(enumerator.Current[0])) {
309
+
if (char.IsDigit(enumerator.Current[0])) {
310
+
BuildNumber(enumerator, toFlush, out bool foundFull);
311
+
reparse = !foundFull;
312
+
endAndFlushId();
313
+
continue;
314
+
}
315
+
316
+
if (BuiltinFunctions.Contains(enumerator.Current)) {
317
+
toFlush.Add(new Token(TokenType.BuiltInFunc, (uint?) BuiltinFunctions.IndexOf(enumerator.Current)));
318
+
endAndFlushId();
319
+
continue;
320
+
}
321
+
322
+
if (Tokens.TryGetValue(enumerator.Current, out var type)) {
323
+
toFlush.Add(new Token(type));
324
+
endAndFlushId();
325
+
continue;
326
+
}
327
+
328
+
if (enumerator.Current.StartsWith('"')) {
329
+
var current = enumerator.Current;
330
+
toFlush.Add(new ConstantToken(new StringVariant(current.Substring(1, current.Length - 2))));
331
+
endAndFlushId();
332
+
continue;
333
+
}
334
+
335
+
if (bool.TryParse(enumerator.Current, out var resultB)) {
336
+
toFlush.Add(new ConstantToken(new BoolVariant(resultB)));
337
+
endAndFlushId();
338
+
continue;
339
+
}
340
+
341
+
idName += enumerator.Current;
342
+
343
+
end();
344
+
345
+
void end() {
346
+
previous = enumerator.Current;
347
+
finalTokens.AddRange(toFlush);
348
+
toFlush.Clear();
349
+
}
350
+
351
+
void endAndFlushId() {
352
+
if (idName != string.Empty) {
353
+
if (idName.Trim() == "return")
354
+
{
355
+
// CALICO: Hack to handle `return` being the last token of a line
356
+
finalTokens.Add(new Token(TokenType.CfReturn));
357
+
}
358
+
else if (idName.Trim() == "self")
359
+
{
360
+
// CALICO: Hack to handle `self` being the last token of a line
361
+
finalTokens.Add(new Token(TokenType.Self));
362
+
}
363
+
else switch (idName.Trim())
364
+
{
365
+
// TODO: CALICO: clean up this hack
366
+
case "print":
367
+
// CALICO: Without this, `print` is tokenized as an identifier.
368
+
finalTokens.Add(new Token(TokenType.BuiltInFunc, (uint?) BuiltinFunction.TextPrint));
369
+
break;
370
+
case "min":
371
+
// CALICO: Without this, `min` is tokenized as an identifier.
372
+
finalTokens.Add(new Token(TokenType.BuiltInFunc, (uint?) BuiltinFunction.LogicMin));
373
+
break;
374
+
case "null":
375
+
// CALICO: Without this, `null` is tokenized as an identifier.
376
+
finalTokens.Add(new ConstantToken(new NilVariant()));
377
+
break;
378
+
case "break":
379
+
// CALICO: Without this, `break` is tokenized as an identifier.
380
+
finalTokens.Add(new Token(TokenType.CfBreak));
381
+
break;
382
+
case "match":
383
+
// CALICO: Without this, `match` is tokenized as an identifier.
384
+
finalTokens.Add(new Token(TokenType.CfMatch));
385
+
break;
386
+
case "Color":
387
+
// CALICO: Without this, `Color` is tokenized as an identifier.
388
+
finalTokens.Add(new Token(TokenType.BuiltInType, 14));
389
+
break;
390
+
case "Vector3":
391
+
// CALICO: Without this, `Vector3` is tokenized as an identifier.
392
+
finalTokens.Add(new Token(TokenType.BuiltInType, 7));
393
+
break;
394
+
case "lerp_angle":
395
+
// CALICO: Without this, `lerp_angle` is tokenized as an identifier.
396
+
finalTokens.Add(new Token(TokenType.BuiltInFunc, (uint?) BuiltinFunction.MathLerpAngle));
397
+
break;
398
+
case "int":
399
+
// CALICO: https://docs.godotengine.org/en/3.5/tutorials/io/binary_serialization_api.html
400
+
finalTokens.Add(new Token(TokenType.BuiltInType, 2));
401
+
break;
402
+
case "pow":
403
+
// CALICO:
404
+
finalTokens.Add(new Token(TokenType.BuiltInFunc, (uint?) BuiltinFunction.MathPow));
405
+
break;
406
+
case "abs":
407
+
// CALICO:
408
+
finalTokens.Add(new Token(TokenType.BuiltInFunc, (uint?) BuiltinFunction.MathAbs));
409
+
break;
410
+
default:
411
+
// CALICO: We change this to trim the idName, otherwise the whitespace messes with the token
412
+
finalTokens.Add(new IdentifierToken(idName.Trim()));
413
+
break;
414
+
}
415
+
idName = string.Empty;
416
+
}
417
+
418
+
end();
419
+
}
420
+
}
421
+
422
+
// CALICO: We don't need this since we're dealing with snippets
423
+
//finalTokens.Add(new(TokenType.Newline, baseIndent));
424
+
425
+
foreach (var t in finalTokens) yield return t;
426
+
}
427
+
428
+
private static IEnumerable<string> SanitizeInput(IEnumerable<string> tokens) {
429
+
foreach (var token in tokens) {
430
+
if (token != "\n" && string.IsNullOrWhiteSpace(token)) {
431
+
continue;
432
+
}
433
+
434
+
yield return token;
435
+
}
436
+
}
437
+
438
+
private static IEnumerable<string> TokenizeString(string text) {
439
+
StringBuilder builder = new(20);
440
+
for (var i = 0; i < text.Length; i++) {
441
+
switch (text[i]) {
442
+
case '"': {
443
+
yield return ClearBuilder();
444
+
builder.Append('"');
445
+
i++;
446
+
for (; i < text.Length; i++) {
447
+
builder.Append(text[i]);
448
+
if (text[i] == '"') {
449
+
break;
450
+
}
451
+
}
452
+
453
+
yield return ClearBuilder();
454
+
continue;
455
+
}
456
+
457
+
// This is stupid and awful
458
+
case '\n': {
459
+
yield return ClearBuilder();
460
+
var start = i;
461
+
i++;
462
+
for (; i < text.Length && text[i] == '\t'; i++) ;
463
+
i--;
464
+
yield return "\n";
465
+
yield return $"{i - start}";
466
+
continue;
467
+
}
468
+
}
469
+
470
+
var matched = false;
471
+
foreach (var delimiter in Symbols) {
472
+
if (Match(text, i, delimiter)) {
473
+
yield return ClearBuilder();
474
+
yield return delimiter;
475
+
i += delimiter.Length - 1;
476
+
matched = true;
477
+
break;
478
+
}
479
+
}
480
+
481
+
if (matched) continue;
482
+
483
+
if (text[i] == ' ') {
484
+
yield return ClearBuilder();
485
+
continue;
486
+
}
487
+
488
+
builder.Append(text[i]);
489
+
}
490
+
491
+
yield return "\n";
492
+
493
+
string ClearBuilder() {
494
+
var built = builder.ToString();
495
+
builder.Clear();
496
+
return built;
497
+
}
498
+
}
499
+
500
+
private static bool Match(string text, int index, string match) {
501
+
if (index + match.Length > text.Length) return false;
502
+
for (var i = 0; i < match.Length; i++) {
503
+
if (text[index + i] != match[i]) return false;
504
+
}
505
+
return true;
506
+
}
507
+
}
+131
Atproto/CalicoUtils/Util/TokenUtil.cs
+131
Atproto/CalicoUtils/Util/TokenUtil.cs
···
1
+
๏ปฟusing GDWeave.Godot;
2
+
using GDWeave.Godot.Variants;
3
+
using static GDWeave.Godot.TokenType;
4
+
5
+
namespace Teemaw.Calico.Util;
6
+
7
+
public static class TokenUtil
8
+
{
9
+
public static IEnumerable<Token> ReplaceAssignmentsAsDeferred(IEnumerable<Token> tokens,
10
+
HashSet<string>? ignoredIdentifiers = null)
11
+
{
12
+
Token? lastToken = null;
13
+
var inAssignmentStatement = false;
14
+
var skipLine = false;
15
+
foreach (var t in tokens)
16
+
{
17
+
switch (t)
18
+
{
19
+
case { Type: Newline }:
20
+
skipLine = false;
21
+
break;
22
+
case { Type: PrVar }:
23
+
skipLine = true;
24
+
break;
25
+
default:
26
+
{
27
+
if (lastToken is IdentifierToken identifier &&
28
+
(ignoredIdentifiers == null || !ignoredIdentifiers.Contains(identifier.Name)) &&
29
+
t is { Type: OpAssign } && !skipLine)
30
+
{
31
+
inAssignmentStatement = true;
32
+
yield return new IdentifierToken("set_deferred");
33
+
yield return new Token(ParenthesisOpen);
34
+
yield return new ConstantToken(new StringVariant(identifier.Name));
35
+
yield return new Token(Comma);
36
+
// Don't return the last token or current token
37
+
lastToken = null;
38
+
continue;
39
+
}
40
+
41
+
break;
42
+
}
43
+
}
44
+
45
+
if (t.Type == Newline && inAssignmentStatement)
46
+
{
47
+
inAssignmentStatement = false;
48
+
if (lastToken != null)
49
+
yield return lastToken;
50
+
// Close out the set_deferred call...
51
+
yield return new Token(ParenthesisClose);
52
+
// ...before the Newline token.
53
+
yield return t;
54
+
}
55
+
else if (lastToken != null)
56
+
yield return lastToken;
57
+
58
+
lastToken = inAssignmentStatement && t is { Type: Identifier } ? StripAssociatedData(t) : t;
59
+
}
60
+
61
+
if (lastToken != null)
62
+
yield return lastToken;
63
+
}
64
+
65
+
public static Token ReplaceToken(Token cursor, Token target, Token replacement)
66
+
{
67
+
return TokenEquals(cursor, target) ? replacement : cursor;
68
+
}
69
+
70
+
public static IEnumerable<Token> ReplaceTokens(IEnumerable<Token> haystack, IEnumerable<Token> needle,
71
+
IEnumerable<Token> replacements)
72
+
{
73
+
var haystackList = haystack.ToList();
74
+
var needleList = needle.ToList();
75
+
var replacementsList = replacements.ToList();
76
+
77
+
if (needleList.Count == 0) return haystackList;
78
+
79
+
var result = new List<Token>();
80
+
var i = 0;
81
+
82
+
while (i < haystackList.Count)
83
+
{
84
+
if (IsMatch(haystackList, needleList, i))
85
+
{
86
+
result.AddRange(replacementsList);
87
+
i += needleList.Count;
88
+
}
89
+
else
90
+
{
91
+
result.Add(haystackList[i]);
92
+
i++;
93
+
}
94
+
}
95
+
96
+
return result;
97
+
}
98
+
99
+
private static bool IsMatch(List<Token> haystack, List<Token> needle, int startIndex)
100
+
{
101
+
if (startIndex + needle.Count > haystack.Count)
102
+
return false;
103
+
104
+
for (int i = 0; i < needle.Count; i++)
105
+
{
106
+
if (!TokenEquals(haystack[startIndex + i], needle[i]))
107
+
return false;
108
+
}
109
+
110
+
return true;
111
+
}
112
+
113
+
private static bool TokenEquals(Token token, Token token1)
114
+
{
115
+
if (token is IdentifierToken id && token1 is IdentifierToken id1)
116
+
{
117
+
return id.Name == id1.Name;
118
+
}
119
+
if (token is ConstantToken constant && token1 is ConstantToken constant1)
120
+
{
121
+
return constant.Value.Equals(constant1.Value);
122
+
}
123
+
return token.Type == token1.Type && token.AssociatedData == token1.AssociatedData;
124
+
}
125
+
126
+
private static Token StripAssociatedData(Token token)
127
+
{
128
+
token.AssociatedData = null;
129
+
return token;
130
+
}
131
+
}
+9
Atproto/CalicoUtils/Util/WeaveUtil.cs
+9
Atproto/CalicoUtils/Util/WeaveUtil.cs
+22
Atproto/CatchFishFactory.cs
+22
Atproto/CatchFishFactory.cs
···
1
+
using GDWeave;
2
+
using GDWeave.Modding;
3
+
using Teemaw.Calico.LexicalTransformer;
4
+
5
+
namespace Atproto;
6
+
7
+
public static class CatchFishFactory
8
+
{
9
+
public static IScriptMod Create(IModInterface mod)
10
+
{
11
+
return new TransformationRuleScriptModBuilder()
12
+
.ForMod(mod)
13
+
.Named("CatchFish")
14
+
.Patching("res://Scenes/Entities/Player/player.gdc")
15
+
.AddRule(new TransformationRuleBuilder()
16
+
.Named("create_fish_record")
17
+
.Matching(TransformationPatternFactory.CreateGdSnippetPattern(
18
+
"PlayerData._log_item(fish_roll, size, quality)\n", 3))
19
+
.Do(Operation.Append)
20
+
.With("$\"/root/Atproto\".AtProtoClient.catch_fish(fish_roll, size, quality)\n", 3)).Build();
21
+
}
22
+
}
+22
Atproto/CatptureFishFactory.cs
+22
Atproto/CatptureFishFactory.cs
···
1
+
using GDWeave;
2
+
using GDWeave.Modding;
3
+
using Teemaw.Calico.LexicalTransformer;
4
+
5
+
namespace Atproto;
6
+
7
+
public static class CatptureFishFactory
8
+
{
9
+
public static IScriptMod Create(IModInterface mod)
10
+
{
11
+
return new TransformationRuleScriptModBuilder()
12
+
.ForMod(mod)
13
+
.Named("CaptureFish")
14
+
.Patching("res://Scenes/Entities/Props/fish_trap.gdc")
15
+
.AddRule(new TransformationRuleBuilder()
16
+
.Named("create_fish_record")
17
+
.Matching(TransformationPatternFactory.CreateGdSnippetPattern(
18
+
"PlayerData._log_item(fish_roll, size, quality)\n", 1))
19
+
.Do(Operation.Append)
20
+
.With("$\"/root/Atproto\".AtProtoClient.catch_fish(fish_roll, size, quality)\n", 1)).Build();
21
+
}
22
+
}
+11
Atproto/Config.cs
+11
Atproto/Config.cs
···
1
+
๏ปฟusing System.Text.Json.Serialization;
2
+
3
+
namespace Atproto;
4
+
5
+
public class Config {
6
+
[JsonInclude] public string Handle = "";
7
+
[JsonInclude] public string Password = "";
8
+
[JsonInclude] public string Save = "";
9
+
[JsonInclude] public bool Autoconnect;
10
+
[JsonInclude] public bool Autoload;
11
+
}
+18
Atproto/Mod.cs
+18
Atproto/Mod.cs
···
1
+
๏ปฟusing GDWeave;
2
+
3
+
namespace Atproto;
4
+
5
+
public class Mod : IMod {
6
+
public Config Config;
7
+
8
+
public Mod(IModInterface modInterface) {
9
+
Config = modInterface.ReadConfig<Config>();
10
+
modInterface.RegisterScriptMod(CatchFishFactory.Create(modInterface));
11
+
modInterface.RegisterScriptMod(CatptureFishFactory.Create(modInterface));
12
+
modInterface.RegisterScriptMod(AtProtoSaveFactory.Create(modInterface));
13
+
}
14
+
15
+
public void Dispose() {
16
+
// Cleanup anything you do here
17
+
}
18
+
}
+16
Atproto.sln
+16
Atproto.sln
···
1
+
๏ปฟ
2
+
Microsoft Visual Studio Solution File, Format Version 12.00
3
+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atproto", "Atproto\Atproto.csproj", "{1A273855-C2D0-40BB-B22C-CE17AA4D0A9B}"
4
+
EndProject
5
+
Global
6
+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7
+
Debug|Any CPU = Debug|Any CPU
8
+
Release|Any CPU = Release|Any CPU
9
+
EndGlobalSection
10
+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
11
+
{1A273855-C2D0-40BB-B22C-CE17AA4D0A9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12
+
{1A273855-C2D0-40BB-B22C-CE17AA4D0A9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
13
+
{1A273855-C2D0-40BB-B22C-CE17AA4D0A9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
14
+
{1A273855-C2D0-40BB-B22C-CE17AA4D0A9B}.Release|Any CPU.Build.0 = Release|Any CPU
15
+
EndGlobalSection
16
+
EndGlobal
+29
README.md
+29
README.md
···
1
+
# AtProto Webfishing
2
+
3
+
A Webfishing mod that send data to your PDS
4
+
5
+
[ThunderStore Link](https://thunderstore.io/c/webfishing/p/EstymMods/Webfishing_Atproto/)
6
+
7
+
# Features
8
+
- Remote save files
9
+
- Caught fish are saved to your PDS
10
+
11
+
## Requirements
12
+
- [GDWeave](https://thunderstore.io/c/webfishing/p/NotNet/GDWeave/)
13
+
- [TackleBox](https://thunderstore.io/c/webfishing/p/PuppyGirl/TackleBox/)
14
+
15
+
## Configuration
16
+
In game using the AtProto menu, you can input your AtProto Handle as well as an App Password to login.
17
+
18
+
After it's done, you can select a save file or create one, then load it !
19
+
20
+
When a file is loaded, the savedata will be synchronized to your PDS each time the game saves.
21
+
22
+
## How to send your save online
23
+
24
+
To send your save on your PDS, simply create a new save and click on "Duplicate Save", it will create a new save on the PDS with the local data you have.
25
+
26
+
27
+
## Credits
28
+
29
+
- Thanks to [Tianyi Ma](https://github.com/tma02) for his utils
-1
gdscript/.import/.gdignore
-1
gdscript/.import/.gdignore
···
1
-
-143
gdscript/mods/AtProto/atproto.gd
-143
gdscript/mods/AtProto/atproto.gd
···
1
-
extends Node
2
-
class_name AtProto
3
-
4
-
# AtProto
5
-
var did
6
-
var pds
7
-
8
-
var accessJwt
9
-
var refreshJwt
10
-
11
-
12
-
# HTTP
13
-
var requester: HTTPRequest
14
-
15
-
func _enter_tree():
16
-
var root = get_tree().get_root()
17
-
var instance = root.get_node("AtProto")
18
-
self.name = "AtProto"
19
-
if instance == null:
20
-
root.add_child(self, true)
21
-
self.requester = HTTPRequest.new()
22
-
self.add_child(self.requester, true)
23
-
24
-
func is_token_expired() -> bool:
25
-
var json = Marshalls.base64_to_utf8(self.accessJwt.split(".")[1])
26
-
var data = parse_json(json)
27
-
var expires = data.exp
28
-
var unix = floor(Time.get_unix_time_from_system())
29
-
return expires < unix
30
-
31
-
func create_record(record):
32
-
33
-
if is_token_expired():
34
-
refresh_token("create_record", record)
35
-
return
36
-
37
-
var payload = {
38
-
repo = did,
39
-
collection = record.at_type,
40
-
record = record
41
-
}
42
-
43
-
var json_payload = JSON.print(payload)
44
-
json_payload = json_payload.replace("at_type", "$type")
45
-
46
-
var req = self.requester
47
-
48
-
var header = [
49
-
"Authorization: Bearer " + self.accessJwt,
50
-
"Content-Type: application/json"
51
-
]
52
-
53
-
req.request(pds + "/xrpc/com.atproto.repo.createRecord", header, true, HTTPClient.METHOD_POST, json_payload)
54
-
55
-
56
-
################
57
-
# LOGIN #
58
-
################
59
-
60
-
func login(handle, password):
61
-
var req = self.requester
62
-
63
-
req.connect("request_completed", self, "after_handle_resolver", [password])
64
-
req.request("https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=" + handle)
65
-
66
-
67
-
func after_handle_resolver(_result, _response_code, _headers, body: PoolByteArray, password):
68
-
var req = self.requester
69
-
req.disconnect("request_completed", self, "after_handle_resolver")
70
-
var res = parse_json(body.get_string_from_utf8())
71
-
self.did = res.did
72
-
73
-
req.connect("request_completed", self, "after_get_pds", [password])
74
-
req.request("https://plc.directory/" + self.did)
75
-
76
-
77
-
func after_get_pds(_result, _response_code, _headers, body: PoolByteArray, password):
78
-
var req = self.requester
79
-
req.disconnect("request_completed", self, "after_get_pds")
80
-
81
-
var res = parse_json(body.get_string_from_utf8())
82
-
for x in res.service:
83
-
if x.id == "#atproto_pds":
84
-
self.pds = x.serviceEndpoint
85
-
86
-
var payload = {
87
-
identifier = self.did,
88
-
password = password
89
-
}
90
-
91
-
req.connect("request_completed", self, "after_create_session")
92
-
req.request(pds + "/xrpc/com.atproto.server.createSession", ["Content-Type: application/json"], true, HTTPClient.METHOD_POST, JSON.print(payload))
93
-
94
-
95
-
96
-
func after_create_session(_result, _response_code, _headers, body: PoolByteArray):
97
-
var req = self.requester
98
-
req.disconnect("request_completed", self, "after_create_session")
99
-
100
-
var res = parse_json(body.get_string_from_utf8())
101
-
self.accessJwt = res.accessJwt
102
-
self.refreshJwt = res.refreshJwt
103
-
104
-
#######################
105
-
# REFRESH TOKEN #
106
-
#######################
107
-
func refresh_token(method = "", payload = ""):
108
-
var req = self.requester
109
-
110
-
var headers = [
111
-
"Authorization: Bearer " + self.refreshJwt,
112
-
"Content-Type: application/json"
113
-
]
114
-
req.connect("request_completed", self, "after_refresh_token", [method, payload])
115
-
req.request(pds + "/xrpc/com.atproto.server.refreshSession", headers, true, HTTPClient.METHOD_POST)
116
-
117
-
func after_refresh_token(_result, _response_code, _headers, body: PoolByteArray, method, payload):
118
-
var req = self.requester
119
-
req.disconnect("request_completed", self, "after_refresh_token")
120
-
121
-
var res = parse_json(body.get_string_from_utf8())
122
-
self.accessJwt = res.accessJwt
123
-
self.refreshJwt = res.refreshJwt
124
-
125
-
if method != "":
126
-
self.call(method, payload)
127
-
128
-
129
-
######################
130
-
# STATIC CALLS #
131
-
######################
132
-
133
-
static func catch_fish(node, fish, size, quality):
134
-
# var fish_data = Globals.item_data[fish]["file"]
135
-
# var record = {
136
-
# at_type = "dev.regnault.webfishing.fish",
137
-
# id = fish,
138
-
# name = fish_data.item_name,
139
-
# size = str(size),
140
-
# quality = quality
141
-
# }
142
-
# node.get_tree().get_root().get_node("AtProto").create_record(record)
143
-
pass
-5
gdscript/mods/AtProto/main.gd
-5
gdscript/mods/AtProto/main.gd
-31
gdscript/project.godot
-31
gdscript/project.godot
···
1
-
; Engine configuration file.
2
-
; It's best edited using the editor UI and not directly,
3
-
; since the parameters that go here are not all obvious.
4
-
;
5
-
; Format:
6
-
; [section] ; section goes between []
7
-
; param=value ; assign values to parameters
8
-
9
-
config_version=4
10
-
11
-
_global_script_classes=[ {
12
-
"base": "Node",
13
-
"class": "AtProto",
14
-
"language": "GDScript",
15
-
"path": "res://mods/AtProto/atproto.gd"
16
-
} ]
17
-
_global_script_class_icons={
18
-
"AtProto": ""
19
-
}
20
-
21
-
[application]
22
-
23
-
config/name="Webfishing AtProto"
24
-
25
-
[gui]
26
-
27
-
common/drop_mouse_on_gui_input_disabled=true
28
-
29
-
[physics]
30
-
31
-
common/enable_pause_aware_picking=true
icon.png
icon.png
This is a binary file and will not be displayed.
+36
lexicon/fish.json
+36
lexicon/fish.json
···
1
+
{
2
+
"id": "dev.regnault.webfishing.fish",
3
+
"defs": {
4
+
"main": {
5
+
"key": "tid",
6
+
"type": "record",
7
+
"output": {
8
+
"schema": {
9
+
"type": "object",
10
+
"required": [
11
+
"id",
12
+
"name",
13
+
"quality"
14
+
],
15
+
"properties": {
16
+
"id": {
17
+
"type": "string"
18
+
},
19
+
"name": {
20
+
"type": "string"
21
+
},
22
+
"size": {
23
+
"type": "string"
24
+
},
25
+
"quality": {
26
+
"type": "integer"
27
+
}
28
+
}
29
+
},
30
+
"encoding": "application/json"
31
+
}
32
+
}
33
+
},
34
+
"$type": "com.atproto.lexicon.schema",
35
+
"lexicon": 1
36
+
}
+448
lexicon/save.json
+448
lexicon/save.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "dev.regnault.webfishing.save",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Record declaring a save data of the game webfishing",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": [
12
+
"inventory"
13
+
],
14
+
"properties": {
15
+
"inventory": {
16
+
"type": "array",
17
+
"items": {
18
+
"type": "ref",
19
+
"ref": "#item"
20
+
}
21
+
},
22
+
"hotbar": {
23
+
"type": "ref",
24
+
"ref": "#hotbar"
25
+
},
26
+
"cosmetics_unlocked": {
27
+
"type": "array",
28
+
"items": {
29
+
"type": "string"
30
+
}
31
+
},
32
+
"cosmetics_equipped": {
33
+
"type": "ref",
34
+
"ref": "#cosmetics"
35
+
},
36
+
"new_cosmetics": {
37
+
"type": "array",
38
+
"items": {
39
+
"type": "string"
40
+
}
41
+
},
42
+
"version": {
43
+
"type": "string"
44
+
},
45
+
"money": {
46
+
"type": "integer"
47
+
},
48
+
"bait_inv": {
49
+
"type": "ref",
50
+
"ref": "#bait_inv"
51
+
},
52
+
"bait_selected": {
53
+
"type": "string"
54
+
},
55
+
"bait_unlocked": {
56
+
"type": "array",
57
+
"items": {
58
+
"type": "string"
59
+
}
60
+
},
61
+
"journal": {
62
+
"type": "array",
63
+
"items": {
64
+
"type": "ref",
65
+
"ref": "#journal_category"
66
+
}
67
+
},
68
+
"quests": {
69
+
"type": "array",
70
+
"items": {
71
+
"type": "ref",
72
+
"ref": "#quest_entry"
73
+
}
74
+
},
75
+
"completed_quests": {
76
+
"type": "array",
77
+
"items": {
78
+
"type": "string"
79
+
}
80
+
},
81
+
"level": {
82
+
"type": "integer"
83
+
},
84
+
"xp": {
85
+
"type": "integer"
86
+
},
87
+
"max_bait": {
88
+
"type": "integer"
89
+
},
90
+
"lure_unlocked": {
91
+
"type": "array",
92
+
"items": {
93
+
"type": "string"
94
+
}
95
+
},
96
+
"lure_selected": {
97
+
"type": "string"
98
+
},
99
+
"saved_aqua_fish": {
100
+
"type": "ref",
101
+
"ref": "#aqua_fish"
102
+
},
103
+
"inbound_mail": {
104
+
"type": "array",
105
+
"items": {
106
+
"type": "ref",
107
+
"ref": "#letter"
108
+
}
109
+
},
110
+
"rod_power": {
111
+
"type": "integer"
112
+
},
113
+
"rod_speed": {
114
+
"type": "integer"
115
+
},
116
+
"rod_chance": {
117
+
"type": "integer"
118
+
},
119
+
"rod_luck": {
120
+
"type": "integer"
121
+
},
122
+
"saved_tags": {
123
+
"type": "array",
124
+
"items": {
125
+
"type": "string"
126
+
}
127
+
},
128
+
"loan_level": {
129
+
"type": "integer"
130
+
},
131
+
"loan_left": {
132
+
"type": "integer"
133
+
},
134
+
"buddy_level": {
135
+
"type": "integer"
136
+
},
137
+
"buddy_speed": {
138
+
"type": "integer"
139
+
},
140
+
"guitar_shapes": {
141
+
"type": "array",
142
+
"items": {
143
+
"type": "ref",
144
+
"ref": "#guitar_shapes"
145
+
}
146
+
},
147
+
"fish_caught": {
148
+
"type": "integer"
149
+
},
150
+
"cash_total": {
151
+
"type": "integer"
152
+
},
153
+
"voice_pitch": {
154
+
"type": "string"
155
+
},
156
+
"voice_speed": {
157
+
"type": "integer"
158
+
},
159
+
160
+
"locked_refs": {
161
+
"type": "array",
162
+
"items": {
163
+
"type": "integer"
164
+
}
165
+
}
166
+
}
167
+
}
168
+
},
169
+
"item": {
170
+
"type": "object",
171
+
"properties": {
172
+
"id": {
173
+
"type": "string"
174
+
},
175
+
"ref": {
176
+
"type": "integer"
177
+
},
178
+
"size": {
179
+
"type": "string"
180
+
},
181
+
"quality": {
182
+
"type": "integer"
183
+
},
184
+
"tags": {
185
+
"type": "array",
186
+
"items": {
187
+
"type": "string"
188
+
}
189
+
},
190
+
"custom_name": {
191
+
"type": "string"
192
+
},
193
+
"count": {
194
+
"type": "integer"
195
+
}
196
+
}
197
+
},
198
+
"hotbar": {
199
+
"type": "object",
200
+
"properties": {
201
+
"0":{
202
+
"type": "integer"
203
+
},
204
+
"1":{
205
+
"type": "integer"
206
+
},
207
+
"2":{
208
+
"type": "integer"
209
+
},
210
+
"3":{
211
+
"type": "integer"
212
+
},
213
+
"4":{
214
+
"type": "integer"
215
+
}
216
+
}
217
+
},
218
+
"cosmetics": {
219
+
"type": "object",
220
+
"properties": {
221
+
"species": {
222
+
"type": "string"
223
+
},
224
+
"pattern": {
225
+
"type": "string"
226
+
},
227
+
"primary_color": {
228
+
"type": "string"
229
+
},
230
+
"secondary_color": {
231
+
"type": "string"
232
+
},
233
+
"hat": {
234
+
"type": "string"
235
+
},
236
+
"undershirt": {
237
+
"type": "string"
238
+
},
239
+
"overshirt":{
240
+
"type": "string"
241
+
},
242
+
"title": {
243
+
"type": "string"
244
+
},
245
+
"bobber": {
246
+
"type": "string"
247
+
},
248
+
"eye": {
249
+
"type": "string"
250
+
},
251
+
"nose": {
252
+
"type": "string"
253
+
},
254
+
"mouth": {
255
+
"type": "string"
256
+
},
257
+
"accessory": {
258
+
"type": "array",
259
+
"items": {
260
+
"type": "string"
261
+
}
262
+
},
263
+
"tail": {
264
+
"type": "string"
265
+
},
266
+
"legs": {
267
+
"type": "string"
268
+
}
269
+
}
270
+
},
271
+
"bait_inv": {
272
+
"type": "object",
273
+
"properties": {
274
+
"": {
275
+
"type": "integer"
276
+
},
277
+
"worms": {
278
+
"type": "integer"
279
+
},
280
+
"cricket": {
281
+
"type": "integer"
282
+
},
283
+
"leech": {
284
+
"type": "integer"
285
+
},
286
+
"minnow": {
287
+
"type": "integer"
288
+
},
289
+
"squid": {
290
+
"type": "integer"
291
+
},
292
+
"nautilus": {
293
+
"type": "integer"
294
+
}
295
+
}
296
+
},
297
+
"journal_category": {
298
+
"type": "object",
299
+
"properties": {
300
+
"name": {
301
+
"type": "string"
302
+
},
303
+
"entries": {
304
+
"type": "array",
305
+
"items": {
306
+
"type": "ref",
307
+
"ref": "#journal_entry"
308
+
}
309
+
}
310
+
}
311
+
},
312
+
"journal_entry": {
313
+
"type": "object",
314
+
"properties": {
315
+
"name": {
316
+
"type": "string"
317
+
},
318
+
"count": {
319
+
"type": "integer"
320
+
},
321
+
"record": {
322
+
"type": "string"
323
+
},
324
+
"quality": {
325
+
"type": "array",
326
+
"items": {
327
+
"type": "integer"
328
+
}
329
+
}
330
+
}
331
+
},
332
+
"quest_entry": {
333
+
"type": "object",
334
+
"properties": {
335
+
"id": {
336
+
"type": "integer"
337
+
},
338
+
"title": {
339
+
"type": "string"
340
+
},
341
+
"tier": {
342
+
"type": "integer"
343
+
},
344
+
"action": {
345
+
"type": "string"
346
+
},
347
+
"gold_reward": {
348
+
"type": "integer"
349
+
},
350
+
"xp_reward": {
351
+
"type": "integer"
352
+
},
353
+
"rewards": {
354
+
"type": "array",
355
+
"items": {
356
+
"type": "string"
357
+
}
358
+
},
359
+
"goal_id": {
360
+
"type": "string"
361
+
},
362
+
"icon": {
363
+
"type": "string"
364
+
},
365
+
"progress": {
366
+
"type": "integer"
367
+
},
368
+
"max_level": {
369
+
"type": "integer"
370
+
},
371
+
"hidden": {
372
+
"type": "boolean"
373
+
},
374
+
"goal_amt": {
375
+
"type": "integer"
376
+
},
377
+
"goal_array": {
378
+
"type": "array",
379
+
"items": {
380
+
"type": "integer"
381
+
}
382
+
}
383
+
}
384
+
},
385
+
"aqua_fish": {
386
+
"type": "object",
387
+
"properties": {
388
+
"id": {
389
+
"type": "string"
390
+
},
391
+
"size": {
392
+
"type": "string"
393
+
},
394
+
"ref": {
395
+
"type": "integer"
396
+
},
397
+
"quality": {
398
+
"type": "integer"
399
+
}
400
+
}
401
+
},
402
+
"guitar_shapes": {
403
+
"type": "array",
404
+
"items": {
405
+
"type": "ref",
406
+
"ref": "#guitar_shape"
407
+
}
408
+
},
409
+
"guitar_shape": {
410
+
"type": "array",
411
+
"maxLength": 6,
412
+
"minLength": 6,
413
+
"items": {
414
+
"type": "integer"
415
+
}
416
+
},
417
+
"letter": {
418
+
"type": "object",
419
+
"properties": {
420
+
"letter_id": {
421
+
"type": "integer"
422
+
},
423
+
"header": {
424
+
"type": "string"
425
+
},
426
+
"closing": {
427
+
"type": "string"
428
+
},
429
+
"body": {
430
+
"type": "string"
431
+
},
432
+
"items": {
433
+
"type": "array",
434
+
"items": {
435
+
"type": "ref",
436
+
"ref": "#item"
437
+
}
438
+
},
439
+
"to": {
440
+
"type": "string"
441
+
},
442
+
"from": {
443
+
"type": "string"
444
+
}
445
+
}
446
+
}
447
+
}
448
+
}
+27
lexicon/savefile.json
+27
lexicon/savefile.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "dev.regnault.webfishing.savefile",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Record declaring a savefile of Webfishing.",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": [
12
+
"name",
13
+
"uri"
14
+
],
15
+
"properties": {
16
+
"name": {
17
+
"type": "string"
18
+
},
19
+
"uri": {
20
+
"type": "string",
21
+
"format": "at-uri"
22
+
}
23
+
}
24
+
}
25
+
}
26
+
}
27
+
}
+20
manifestation.toml
+20
manifestation.toml
···
1
+
id = "Atproto"
2
+
name = "Atproto Webfishing"
3
+
description = "A mod that sends data to your AtProto PDS."
4
+
version = "1.0.3"
5
+
homepage = "https://tangled.org/@regnault.dev/webfishing-atproto"
6
+
author = "Estym"
7
+
8
+
icon = "icon.png"
9
+
readme = "README.md"
10
+
11
+
[project]
12
+
csharp = "./Atproto/Atproto.csproj"
13
+
godot = "./project/project.godot"
14
+
15
+
[[dependencies]]
16
+
thunderstore_version = "NotNet-GDWeave-2.0.12"
17
+
18
+
[[dependencies]]
19
+
id = "TackleBox"
20
+
thunderstore_version = "PuppyGirl-TackleBox-0.5.2"
+264
project/mods/Atproto/assets/at_proto.tres
+264
project/mods/Atproto/assets/at_proto.tres
···
1
+
[gd_resource type="Theme" load_steps=28 format=2]
2
+
3
+
[ext_resource path="res://Assets/Textures/UI/scrollbar.png" type="Texture" id=1]
4
+
[ext_resource path="res://Assets/Themes/main_font.tres" type="DynamicFont" id=2]
5
+
6
+
[sub_resource type="StyleBoxFlat" id=1]
7
+
content_margin_left = 12.0
8
+
content_margin_right = 12.0
9
+
bg_color = Color( 0.415686, 0.266667, 0.12549, 1 )
10
+
border_color = Color( 0.415686, 0.266667, 0.12549, 1 )
11
+
corner_radius_top_left = 12
12
+
corner_radius_top_right = 12
13
+
corner_radius_bottom_right = 12
14
+
corner_radius_bottom_left = 12
15
+
corner_detail = 5
16
+
17
+
[sub_resource type="StyleBoxEmpty" id=34]
18
+
19
+
[sub_resource type="StyleBoxFlat" id=30]
20
+
content_margin_left = 12.0
21
+
content_margin_right = 12.0
22
+
bg_color = Color( 0.611765, 0.568627, 0.290196, 1 )
23
+
border_color = Color( 0.835294, 0.666667, 0.45098, 1 )
24
+
corner_radius_top_left = 12
25
+
corner_radius_top_right = 12
26
+
corner_radius_bottom_right = 12
27
+
corner_radius_bottom_left = 12
28
+
corner_detail = 5
29
+
expand_margin_left = 1.0
30
+
expand_margin_right = 1.0
31
+
expand_margin_top = 1.0
32
+
expand_margin_bottom = 1.0
33
+
34
+
[sub_resource type="StyleBoxFlat" id=31]
35
+
content_margin_left = 12.0
36
+
content_margin_right = 12.0
37
+
bg_color = Color( 0.352941, 0.458824, 0.352941, 1 )
38
+
border_color = Color( 0.415686, 0.266667, 0.12549, 1 )
39
+
corner_radius_top_left = 12
40
+
corner_radius_top_right = 12
41
+
corner_radius_bottom_right = 12
42
+
corner_radius_bottom_left = 12
43
+
44
+
[sub_resource type="StyleBoxFlat" id=32]
45
+
content_margin_left = 12.0
46
+
content_margin_right = 12.0
47
+
bg_color = Color( 0.352941, 0.458824, 0.352941, 1 )
48
+
border_color = Color( 0.415686, 0.266667, 0.12549, 1 )
49
+
corner_radius_top_left = 12
50
+
corner_radius_top_right = 12
51
+
corner_radius_bottom_right = 12
52
+
corner_radius_bottom_left = 12
53
+
corner_detail = 5
54
+
55
+
[sub_resource type="StyleBoxFlat" id=18]
56
+
bg_color = Color( 0.352941, 0.458824, 0.352941, 1 )
57
+
border_width_left = 6
58
+
border_color = Color( 1, 1, 1, 0 )
59
+
corner_radius_top_left = 4
60
+
corner_radius_top_right = 4
61
+
corner_radius_bottom_right = 4
62
+
corner_radius_bottom_left = 4
63
+
64
+
[sub_resource type="StyleBoxFlat" id=15]
65
+
bg_color = Color( 0.611765, 0.568627, 0.290196, 1 )
66
+
border_color = Color( 0.835294, 0.666667, 0.45098, 1 )
67
+
corner_radius_top_left = 12
68
+
corner_radius_top_right = 12
69
+
corner_radius_bottom_right = 12
70
+
corner_radius_bottom_left = 12
71
+
corner_detail = 5
72
+
73
+
[sub_resource type="StyleBoxFlat" id=21]
74
+
bg_color = Color( 0.0627451, 0.109804, 0.192157, 1 )
75
+
draw_center = false
76
+
border_width_left = 16
77
+
border_color = Color( 1, 1, 1, 0 )
78
+
corner_radius_top_left = 4
79
+
corner_radius_top_right = 4
80
+
corner_radius_bottom_right = 4
81
+
corner_radius_bottom_left = 4
82
+
83
+
[sub_resource type="StyleBoxEmpty" id=20]
84
+
85
+
[sub_resource type="StyleBoxFlat" id=35]
86
+
87
+
[sub_resource type="StyleBoxFlat" id=36]
88
+
89
+
[sub_resource type="StyleBoxFlat" id=37]
90
+
bg_color = Color( 0.0627451, 0.109804, 0.192157, 1 )
91
+
corner_radius_top_left = 8
92
+
corner_radius_top_right = 8
93
+
corner_radius_bottom_right = 8
94
+
corner_radius_bottom_left = 8
95
+
expand_margin_top = 3.0
96
+
expand_margin_bottom = 3.0
97
+
98
+
[sub_resource type="StyleBoxFlat" id=33]
99
+
content_margin_left = 12.0
100
+
content_margin_right = 12.0
101
+
bg_color = Color( 0.572549, 0.45098, 0.290196, 1 )
102
+
border_color = Color( 0.415686, 0.266667, 0.12549, 1 )
103
+
corner_radius_top_left = 12
104
+
corner_radius_top_right = 12
105
+
corner_radius_bottom_right = 12
106
+
corner_radius_bottom_left = 12
107
+
108
+
[sub_resource type="StyleBoxFlat" id=9]
109
+
bg_color = Color( 1, 0.933333, 0.835294, 1 )
110
+
border_color = Color( 0.415686, 0.266667, 0.12549, 1 )
111
+
corner_radius_top_left = 32
112
+
corner_radius_top_right = 32
113
+
corner_radius_bottom_right = 32
114
+
corner_radius_bottom_left = 32
115
+
116
+
[sub_resource type="ImageTexture" id=24]
117
+
118
+
[sub_resource type="StyleBoxFlat" id=25]
119
+
content_margin_left = 4.0
120
+
content_margin_right = 4.0
121
+
bg_color = Color( 0.611765, 0.568627, 0.290196, 1 )
122
+
corner_radius_top_left = 8
123
+
corner_radius_top_right = 8
124
+
corner_radius_bottom_right = 8
125
+
corner_radius_bottom_left = 8
126
+
expand_margin_left = 2.0
127
+
expand_margin_right = 2.0
128
+
expand_margin_top = 2.0
129
+
130
+
[sub_resource type="StyleBoxEmpty" id=26]
131
+
132
+
[sub_resource type="StyleBoxEmpty" id=27]
133
+
134
+
[sub_resource type="StyleBoxFlat" id=28]
135
+
content_margin_left = 8.0
136
+
content_margin_right = 8.0
137
+
bg_color = Color( 0.352941, 0.458824, 0.352941, 1 )
138
+
corner_radius_bottom_right = 12
139
+
corner_radius_bottom_left = 12
140
+
expand_margin_top = 10.0
141
+
expand_margin_bottom = 8.0
142
+
143
+
[sub_resource type="StyleBoxEmpty" id=29]
144
+
145
+
[sub_resource type="StyleBoxFlat" id=4]
146
+
bg_color = Color( 0.352941, 0.458824, 0.352941, 1 )
147
+
border_color = Color( 0.415686, 0.266667, 0.12549, 1 )
148
+
corner_radius_top_left = 12
149
+
corner_radius_top_right = 12
150
+
corner_radius_bottom_right = 12
151
+
corner_radius_bottom_left = 12
152
+
expand_margin_left = 2.0
153
+
expand_margin_right = 2.0
154
+
155
+
[sub_resource type="StyleBoxFlat" id=8]
156
+
bg_color = Color( 0.835294, 0.666667, 0.45098, 1 )
157
+
border_width_left = 2
158
+
border_width_top = 2
159
+
border_width_right = 2
160
+
border_width_bottom = 2
161
+
border_color = Color( 0.415686, 0.266667, 0.12549, 1 )
162
+
corner_radius_top_left = 6
163
+
corner_radius_top_right = 6
164
+
corner_radius_bottom_right = 6
165
+
corner_radius_bottom_left = 6
166
+
corner_detail = 5
167
+
anti_aliasing = false
168
+
169
+
[sub_resource type="StyleBoxFlat" id=6]
170
+
bg_color = Color( 0.835294, 0.666667, 0.45098, 1 )
171
+
border_width_left = 2
172
+
border_width_top = 2
173
+
border_width_right = 2
174
+
border_color = Color( 0.415686, 0.266667, 0.12549, 1 )
175
+
corner_radius_top_left = 6
176
+
corner_radius_top_right = 6
177
+
corner_detail = 5
178
+
anti_aliasing = false
179
+
180
+
[sub_resource type="StyleBoxFlat" id=7]
181
+
bg_color = Color( 0.835294, 0.666667, 0.45098, 1 )
182
+
border_width_left = 2
183
+
border_width_top = 2
184
+
border_width_right = 2
185
+
border_color = Color( 0.415686, 0.266667, 0.12549, 1 )
186
+
corner_radius_top_left = 6
187
+
corner_radius_top_right = 6
188
+
corner_detail = 5
189
+
expand_margin_bottom = 2.0
190
+
anti_aliasing = false
191
+
192
+
[sub_resource type="StyleBoxEmpty" id=23]
193
+
194
+
[resource]
195
+
default_font = ExtResource( 2 )
196
+
Button/colors/font_color = Color( 1, 0.933333, 0.835294, 1 )
197
+
Button/colors/font_color_disabled = Color( 0.835294, 0.666667, 0.45098, 1 )
198
+
Button/colors/font_color_focus = Color( 1, 0.933333, 0.835294, 1 )
199
+
Button/colors/font_color_hover = Color( 1, 0.933333, 0.835294, 1 )
200
+
Button/colors/font_color_pressed = Color( 0.835294, 0.666667, 0.45098, 1 )
201
+
Button/styles/disabled = SubResource( 1 )
202
+
Button/styles/focus = SubResource( 34 )
203
+
Button/styles/hover = SubResource( 30 )
204
+
Button/styles/normal = SubResource( 31 )
205
+
Button/styles/pressed = SubResource( 32 )
206
+
HScrollBar/styles/grabber = SubResource( 18 )
207
+
HScrollBar/styles/grabber_highlight = SubResource( 15 )
208
+
HScrollBar/styles/grabber_pressed = SubResource( 15 )
209
+
HScrollBar/styles/scroll = SubResource( 21 )
210
+
HScrollBar/styles/scroll_focus = SubResource( 20 )
211
+
HSlider/icons/grabber = ExtResource( 1 )
212
+
HSlider/icons/grabber_disabled = ExtResource( 1 )
213
+
HSlider/icons/grabber_highlight = ExtResource( 1 )
214
+
HSlider/styles/grabber_area = SubResource( 35 )
215
+
HSlider/styles/grabber_area_highlight = SubResource( 36 )
216
+
HSlider/styles/slider = SubResource( 37 )
217
+
Label/colors/font_color = Color( 0.352941, 0.458824, 0.352941, 1 )
218
+
LineEdit/colors/cursor_color = Color( 1, 0.933333, 0.835294, 1 )
219
+
LineEdit/colors/font_color = Color( 1, 0.933333, 0.835294, 1 )
220
+
LineEdit/colors/font_color_selected = Color( 1, 0.933333, 0.835294, 1 )
221
+
LineEdit/colors/font_color_uneditable = Color( 0.835294, 0.666667, 0.45098, 1 )
222
+
LineEdit/styles/focus = SubResource( 33 )
223
+
LineEdit/styles/normal = SubResource( 33 )
224
+
LineEdit/styles/read_only = SubResource( 33 )
225
+
Panel/styles/panel = SubResource( 9 )
226
+
PopupMenu/colors/font_color = Color( 1, 0.933333, 0.835294, 1 )
227
+
PopupMenu/colors/font_color_hover = Color( 1, 0.933333, 0.835294, 1 )
228
+
PopupMenu/icons/checked = SubResource( 24 )
229
+
PopupMenu/icons/radio_checked = SubResource( 24 )
230
+
PopupMenu/icons/radio_unchecked = SubResource( 24 )
231
+
PopupMenu/icons/submenu = SubResource( 24 )
232
+
PopupMenu/icons/unchecked = SubResource( 24 )
233
+
PopupMenu/styles/hover = SubResource( 25 )
234
+
PopupMenu/styles/labeled_separator_left = SubResource( 26 )
235
+
PopupMenu/styles/labeled_separator_right = SubResource( 27 )
236
+
PopupMenu/styles/panel = SubResource( 28 )
237
+
PopupMenu/styles/panel_disabled = SubResource( 28 )
238
+
PopupMenu/styles/separator = SubResource( 29 )
239
+
ProgressBar/colors/font_color = Color( 1, 0.933333, 0.835294, 1 )
240
+
ProgressBar/styles/bg = SubResource( 1 )
241
+
ProgressBar/styles/fg = SubResource( 4 )
242
+
RichTextLabel/colors/default_color = Color( 0.835294, 0.666667, 0.45098, 1 )
243
+
TabContainer/colors/font_color_bg = Color( 0.415686, 0.266667, 0.12549, 1 )
244
+
TabContainer/colors/font_color_disabled = Color( 0.611765, 0.0784314, 0.0784314, 1 )
245
+
TabContainer/colors/font_color_fg = Color( 1, 0.933333, 0.835294, 1 )
246
+
TabContainer/styles/panel = SubResource( 8 )
247
+
TabContainer/styles/tab_bg = SubResource( 6 )
248
+
TabContainer/styles/tab_disabled = SubResource( 1 )
249
+
TabContainer/styles/tab_fg = SubResource( 7 )
250
+
Tabs/colors/font_color_bg = Color( 0.835294, 0.666667, 0.45098, 1 )
251
+
Tabs/colors/font_color_fg = Color( 0.835294, 0.666667, 0.45098, 1 )
252
+
TextEdit/colors/caret_color = Color( 1, 0.933333, 0.835294, 1 )
253
+
TextEdit/colors/font_color = Color( 0.415686, 0.266667, 0.12549, 1 )
254
+
TextEdit/colors/font_color_readonly = Color( 0.415686, 0.266667, 0.12549, 1 )
255
+
TextEdit/colors/selection_color = Color( 0.352941, 0.458824, 0.352941, 1 )
256
+
TextEdit/styles/completion = SubResource( 23 )
257
+
TextEdit/styles/focus = SubResource( 23 )
258
+
TextEdit/styles/normal = SubResource( 23 )
259
+
TextEdit/styles/read_only = SubResource( 23 )
260
+
VScrollBar/styles/grabber = SubResource( 18 )
261
+
VScrollBar/styles/grabber_highlight = SubResource( 15 )
262
+
VScrollBar/styles/grabber_pressed = SubResource( 15 )
263
+
VScrollBar/styles/scroll = SubResource( 21 )
264
+
VScrollBar/styles/scroll_focus = SubResource( 20 )
+462
project/mods/Atproto/atproto_client.gd
+462
project/mods/Atproto/atproto_client.gd
···
1
+
extends Node
2
+
3
+
# Signals
4
+
signal connection(suceeded)
5
+
signal savefile_loaded(uri)
6
+
7
+
# State
8
+
var can_save = true
9
+
10
+
# AtProto
11
+
var did
12
+
var pds
13
+
var accessJwt
14
+
var refreshJwt
15
+
var Atproto
16
+
17
+
func _enter_tree():
18
+
Atproto = self.get_parent()
19
+
20
+
func connected() -> bool:
21
+
return accessJwt != null
22
+
23
+
func is_token_expired() -> bool:
24
+
var token_data = self.accessJwt.split(".")[1]
25
+
var data = null
26
+
for x in ["", "=", "=="] :
27
+
var json = Marshalls.base64_to_utf8(token_data + x)
28
+
data = parse_json(json)
29
+
if data != null: break
30
+
var expires = data.exp
31
+
var unix = floor(Time.get_unix_time_from_system())
32
+
return expires < unix
33
+
34
+
func get_header():
35
+
return [
36
+
"Authorization: Bearer " + self.accessJwt,
37
+
"Content-Type: application/json"
38
+
]
39
+
40
+
func create_record(record, callback : FuncRef = null):
41
+
42
+
if is_token_expired():
43
+
refresh_token("create_record", [record])
44
+
return
45
+
46
+
var payload = {
47
+
repo = did,
48
+
collection = record.at_type,
49
+
record = record
50
+
}
51
+
52
+
var json_payload = JSON.print(payload)
53
+
json_payload = json_payload.replace("at_type", "$type")
54
+
55
+
var req = HTTPRequest.new()
56
+
self.add_child(req)
57
+
req.connect("request_completed", self, "_create_record_handler", [req, callback])
58
+
req.request(pds + "/xrpc/com.atproto.repo.createRecord", get_header(), true, HTTPClient.METHOD_POST, json_payload)
59
+
60
+
61
+
func _create_record_handler(_result, code, _headers, body: PoolByteArray, req: HTTPRequest, callback: FuncRef):
62
+
req.queue_free()
63
+
if callback == null:
64
+
return
65
+
66
+
var res = parse_json(body.get_string_from_utf8())
67
+
callback.call_func(res)
68
+
69
+
## LIST RECORDS
70
+
71
+
func list_records(callback: FuncRef, collection: String, limit: int = 50, cursor = ""):
72
+
var query_string = "repo=" + did
73
+
query_string += "&collection=" + collection.http_escape()
74
+
query_string += "&limit=" + str(limit).http_escape()
75
+
query_string += "&cursor=" + str(limit).http_escape()
76
+
var req_str = pds + "/xrpc/com.atproto.repo.listRecords?" + query_string
77
+
78
+
var req = HTTPRequest.new()
79
+
self.add_child(req)
80
+
req.connect("request_completed", self, "_list_record_handler", [req, callback])
81
+
req.request(req_str, get_header(), true, HTTPClient.METHOD_GET)
82
+
83
+
func _list_record_handler(_result, code, _headers, body: PoolByteArray, req: HTTPRequest, callback: FuncRef):
84
+
req.queue_free()
85
+
var b = body.get_string_from_utf8()
86
+
var res = parse_json(b)
87
+
callback.call_func(res.records)
88
+
89
+
90
+
## GET RECORD
91
+
92
+
func get_record(callback: FuncRef, did: String, collection: String, rkey: String):
93
+
var query_string = "repo=" + did.http_escape()
94
+
query_string += "&collection=" + collection.http_escape()
95
+
query_string += "&rkey=" + rkey.http_escape()
96
+
97
+
var req = HTTPRequest.new()
98
+
self.add_child(req)
99
+
req.connect("request_completed", self, "_get_record_handler", [req, callback])
100
+
req.request(pds + "/xrpc/com.atproto.repo.getRecord?" + query_string, get_header(), true, HTTPClient.METHOD_GET)
101
+
102
+
func _get_record_handler(_result, code, _headers, body: PoolByteArray, req: HTTPRequest, callback: FuncRef):
103
+
req.queue_free()
104
+
var res = parse_json(body.get_string_from_utf8())
105
+
callback.call_func(res)
106
+
107
+
108
+
## PUT RECORD
109
+
110
+
func put_record(uri, record, callback: FuncRef = null):
111
+
if is_token_expired():
112
+
refresh_token("put_record", [uri, record])
113
+
return
114
+
115
+
var splitted_uri = uri.split("/")
116
+
117
+
118
+
var payload = {
119
+
repo = splitted_uri[2],
120
+
collection = splitted_uri[3],
121
+
rkey = splitted_uri[4],
122
+
record = record
123
+
}
124
+
125
+
var json_payload = JSON.print(payload)
126
+
json_payload = json_payload.replace("at_type", "$type")
127
+
128
+
var req = HTTPRequest.new()
129
+
self.add_child(req)
130
+
req.connect("request_completed", self, "_put_record_handler", [req, callback])
131
+
req.request(pds + "/xrpc/com.atproto.repo.putRecord", get_header(), true, HTTPClient.METHOD_POST, json_payload)
132
+
133
+
func _put_record_handler(_result, code, _headers, body: PoolByteArray, req: HTTPRequest, callback: FuncRef):
134
+
req.queue_free()
135
+
if callback == null:
136
+
return
137
+
var res = parse_json(body.get_string_from_utf8())
138
+
callback.call_func(res)
139
+
140
+
################
141
+
# LOGIN #
142
+
################
143
+
144
+
func login(handle, password):
145
+
var req = HTTPRequest.new()
146
+
self.add_child(req)
147
+
148
+
req.connect("request_completed", self, "after_handle_resolver", [req, password])
149
+
req.request("https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=" + handle)
150
+
151
+
152
+
func after_handle_resolver(_result, code, _headers, body: PoolByteArray, req: HTTPRequest, password):
153
+
req.disconnect("request_completed", self, "after_handle_resolver")
154
+
155
+
if code != 200:
156
+
emit_signal("connection", false)
157
+
return
158
+
159
+
var res = parse_json(body.get_string_from_utf8())
160
+
self.did = res.did
161
+
162
+
req.connect("request_completed", self, "after_get_pds", [req, password])
163
+
req.request("https://plc.directory/" + self.did)
164
+
165
+
166
+
func after_get_pds(_result, code, _headers, body: PoolByteArray,req: HTTPRequest, password):
167
+
req.disconnect("request_completed", self, "after_get_pds")
168
+
169
+
if code != 200:
170
+
emit_signal("connection", false)
171
+
return
172
+
173
+
var res = parse_json(body.get_string_from_utf8())
174
+
for x in res.service:
175
+
if x.id == "#atproto_pds":
176
+
self.pds = x.serviceEndpoint
177
+
178
+
var payload = {
179
+
identifier = self.did,
180
+
password = password
181
+
}
182
+
183
+
req.connect("request_completed", self, "after_create_session", [req])
184
+
req.request(pds + "/xrpc/com.atproto.server.createSession", ["Content-Type: application/json"], true, HTTPClient.METHOD_POST, JSON.print(payload))
185
+
186
+
187
+
func after_create_session(_result, code, _headers, body: PoolByteArray, req: HTTPRequest):
188
+
req.queue_free()
189
+
190
+
if code != 200:
191
+
emit_signal("connection", false)
192
+
return
193
+
194
+
var res = parse_json(body.get_string_from_utf8())
195
+
self.accessJwt = res.accessJwt
196
+
self.refreshJwt = res.refreshJwt
197
+
emit_signal("connection", true)
198
+
199
+
#######################
200
+
# REFRESH TOKEN #
201
+
#######################
202
+
func refresh_token(method = "", payload = []):
203
+
var req = self.requester
204
+
205
+
var headers = [
206
+
"Authorization: Bearer " + self.refreshJwt,
207
+
"Content-Type: application/json"
208
+
]
209
+
req.connect("request_completed", self, "after_refresh_token", [method, payload])
210
+
req.request(pds + "/xrpc/com.atproto.server.refreshSession", headers, true, HTTPClient.METHOD_POST)
211
+
212
+
func after_refresh_token(_result, _response_code, _headers, body: PoolByteArray, method, payload):
213
+
var req = self.requester
214
+
req.disconnect("request_completed", self, "after_refresh_token")
215
+
216
+
var res = parse_json(body.get_string_from_utf8())
217
+
self.accessJwt = res.accessJwt
218
+
self.refreshJwt = res.refreshJwt
219
+
220
+
if method != "":
221
+
self.callv(method, payload)
222
+
223
+
224
+
######################
225
+
# Method Calls #
226
+
######################
227
+
228
+
func catch_fish(fish, size, quality):
229
+
var fish_data = Globals.item_data[fish]["file"]
230
+
var record = {
231
+
at_type = "dev.regnault.webfishing.fish",
232
+
id = fish,
233
+
name = fish_data.item_name,
234
+
size = str(size),
235
+
quality = quality
236
+
}
237
+
create_record(record)
238
+
239
+
240
+
# SAVES
241
+
242
+
func create_save_file(uri: String, filename: String, callback: FuncRef = null):
243
+
var record = {
244
+
at_type = "dev.regnault.webfishing.savefile",
245
+
uri = uri,
246
+
name = filename,
247
+
}
248
+
create_record(record, callback)
249
+
pass
250
+
251
+
func get_saves(callback: FuncRef):
252
+
list_records(callback, "dev.regnault.webfishing.savefile")
253
+
254
+
255
+
func get_save_data():
256
+
var save_data = {
257
+
"inventory": PlayerData.inventory,
258
+
"hotbar": PlayerData.hotbar,
259
+
"cosmetics_unlocked": PlayerData.cosmetics_unlocked,
260
+
"cosmetics_equipped": PlayerData.cosmetics_equipped,
261
+
"new_cosmetics": [],
262
+
"version": Globals.GAME_VERSION,
263
+
"muted_players": PlayerData.players_muted,
264
+
"hidden_players": PlayerData.players_hidden,
265
+
"recorded_time": PlayerData.last_recorded_time,
266
+
"money": PlayerData.money,
267
+
"bait_inv": PlayerData.bait_inv,
268
+
"bait_selected": PlayerData.bait_selected,
269
+
"bait_unlocked": PlayerData.bait_unlocked,
270
+
"shop": PlayerData.current_shop,
271
+
"journal": PlayerData.journal_logs,
272
+
"quests": PlayerData.current_quests,
273
+
"completed_quests": PlayerData.completed_quests,
274
+
"level": PlayerData.badge_level,
275
+
"xp": PlayerData.badge_xp,
276
+
"max_bait": PlayerData.max_bait,
277
+
"lure_unlocked": PlayerData.lure_unlocked,
278
+
"lure_selected": PlayerData.lure_selected,
279
+
"saved_aqua_fish": PlayerData.saved_aqua_fish,
280
+
"inbound_mail": PlayerData.inbound_mail,
281
+
"rod_power": PlayerData.rod_power_level,
282
+
"rod_speed": PlayerData.rod_speed_level,
283
+
"rod_chance": PlayerData.rod_chance_level,
284
+
"rod_luck": PlayerData.rod_luck_level,
285
+
"saved_tags": PlayerData.saved_tags,
286
+
"loan_level": PlayerData.loan_level,
287
+
"loan_left": PlayerData.loan_left,
288
+
"buddy_level": PlayerData.buddy_level,
289
+
"buddy_speed": PlayerData.buddy_speed,
290
+
"guitar_shapes": PlayerData.guitar_shapes,
291
+
"fish_caught": PlayerData.fish_caught,
292
+
"cash_total": PlayerData.cash_total,
293
+
"voice_pitch": PlayerData.voice_pitch,
294
+
"voice_speed": PlayerData.voice_speed,
295
+
"locked_refs": PlayerData.locked_refs,
296
+
}
297
+
save_data = save_data.duplicate(true)
298
+
299
+
# JOURNAL
300
+
var modified_journal = []
301
+
for area in save_data.journal:
302
+
var area_entry = {
303
+
name = area,
304
+
entries = []
305
+
}
306
+
for entry_name in save_data.journal[area]:
307
+
var entry = save_data.journal[area][entry_name]
308
+
area_entry.entries.append({
309
+
name = entry_name,
310
+
count = entry.count,
311
+
record = str(entry.record),
312
+
quality = entry.quality
313
+
})
314
+
modified_journal.append(area_entry)
315
+
save_data.journal = modified_journal
316
+
317
+
# Quests
318
+
var modified_quests = []
319
+
for quest_id in save_data.quests:
320
+
var entry = save_data.quests[quest_id].duplicate(true)
321
+
entry.id = quest_id
322
+
modified_quests.append(entry)
323
+
save_data.quests = modified_quests
324
+
325
+
# Inventory
326
+
for item in save_data.inventory:
327
+
item.size = str(item.size)
328
+
329
+
330
+
# Version
331
+
save_data.version = str(save_data.version)
332
+
333
+
# Voice Pitch
334
+
save_data.voice_pitch = str(save_data.voice_pitch)
335
+
336
+
# Aqua Fish
337
+
save_data.saved_aqua_fish.size = str(save_data.saved_aqua_fish.size)
338
+
339
+
# Letters
340
+
for letter in save_data.inbound_mail:
341
+
for item in letter.items:
342
+
item.size = str(item.size)
343
+
344
+
return save_data
345
+
346
+
func save_file(callback: FuncRef = null, creation = false):
347
+
if UserSave.current_loaded_slot != Atproto.ATPROTO_SLOT:
348
+
return
349
+
if !connected(): return
350
+
351
+
var save_data = get_save_data()
352
+
save_data.at_type = "dev.regnault.webfishing.save"
353
+
354
+
if Atproto.save_loaded != "":
355
+
put_record(Atproto.save_loaded, save_data)
356
+
357
+
func create_save(callback: FuncRef = null):
358
+
if !connected(): return
359
+
var save_data = get_save_data()
360
+
save_data.at_type = "dev.regnault.webfishing.save"
361
+
create_record(save_data, callback)
362
+
363
+
func load_save(uri: String):
364
+
var splitted_uri = uri.split("/")
365
+
var did = splitted_uri[2]
366
+
var collection = splitted_uri[3]
367
+
var rkey = splitted_uri[4]
368
+
get_record(funcref(self, "_after_get_save"), did, collection, rkey)
369
+
pass
370
+
371
+
func _after_get_save(save_record):
372
+
var save = save_record.value
373
+
374
+
UserSave._load_save(Atproto.ATPROTO_SLOT)
375
+
376
+
var modified_journal: Dictionary = {}
377
+
for area in save.journal:
378
+
var area_entries = {}
379
+
for entry in area.entries:
380
+
area_entries[entry.name] = {
381
+
count = entry.count,
382
+
record = float(entry.record),
383
+
quality = entry.quality
384
+
}
385
+
386
+
modified_journal[area.name] = area_entries
387
+
save.journal = modified_journal
388
+
389
+
var modified_quests = {}
390
+
for quest in save.quests:
391
+
var id = quest.id
392
+
modified_quests[quest.id] = quest
393
+
modified_quests[quest.id].erase("id")
394
+
save.quests = modified_quests
395
+
396
+
# Inventory
397
+
for item in save.inventory:
398
+
item.size = float(item.size)
399
+
item.quality = int(item.quality)
400
+
var x = PlayerData.QUALITY_DATA[item.quality]
401
+
402
+
save.version = float(save.version)
403
+
save.saved_aqua_fish.size = float(save.saved_aqua_fish.size)
404
+
for letter in save.inbound_mail:
405
+
for item in letter.items:
406
+
item.size = float(item.size)
407
+
item.quality = int(item.quality)
408
+
var x = PlayerData.QUALITY_DATA[item.quality]
409
+
410
+
var modified_hotbar = {}
411
+
for item in save.hotbar:
412
+
modified_hotbar[int(item)] = save.hotbar[item]
413
+
save.hotbar = modified_hotbar
414
+
415
+
PlayerData.inventory = save.inventory
416
+
PlayerData.hotbar = save.hotbar
417
+
PlayerData.cosmetics_unlocked = save.cosmetics_unlocked
418
+
PlayerData.cosmetics_equipped = save.cosmetics_equipped
419
+
PlayerData.money = save.money
420
+
PlayerData.players_muted = save.muted_players
421
+
PlayerData.players_hidden = save.hidden_players
422
+
PlayerData.bait_inv = save.bait_inv
423
+
PlayerData.bait_selected = save.bait_selected
424
+
PlayerData.bait_unlocked = save.bait_unlocked
425
+
PlayerData.max_bait = save.max_bait
426
+
PlayerData.lure_unlocked = save.lure_unlocked
427
+
PlayerData.lure_selected = save.lure_selected
428
+
PlayerData.journal_logs = save.journal
429
+
PlayerData.current_quests = save.quests
430
+
PlayerData.completed_quests = save.completed_quests
431
+
PlayerData.badge_level = int(save.level)
432
+
PlayerData.badge_xp = int(save.xp)
433
+
PlayerData.saved_aqua_fish = save.saved_aqua_fish
434
+
PlayerData.inbound_mail = save.inbound_mail
435
+
PlayerData.saved_tags = save.saved_tags
436
+
PlayerData.loan_level = int(save.loan_level)
437
+
PlayerData.loan_left = save.loan_left
438
+
PlayerData.rod_power_level = save.rod_power
439
+
PlayerData.rod_speed_level = save.rod_speed
440
+
PlayerData.rod_chance_level = save.rod_chance
441
+
PlayerData.rod_luck_level = save.rod_luck
442
+
PlayerData.buddy_level = save.buddy_level
443
+
PlayerData.buddy_speed = save.buddy_speed
444
+
PlayerData.guitar_shapes = save.guitar_shapes
445
+
PlayerData.fish_caught = save.fish_caught
446
+
PlayerData.cash_total = save.cash_total
447
+
PlayerData.voice_pitch = float(save.voice_pitch)
448
+
PlayerData.voice_speed = save.voice_speed
449
+
PlayerData.locked_refs = save.locked_refs
450
+
PlayerData._validate_guitar_shapes()
451
+
PlayerData._validate_inventory()
452
+
PlayerData._journal_check()
453
+
PlayerData._missing_quest_check()
454
+
PlayerData._unlock_defaults()
455
+
456
+
can_save = false
457
+
UserSave._save_slot(Atproto.ATPROTO_SLOT)
458
+
can_save = true
459
+
460
+
emit_signal("savefile_loaded", save_record.uri)
461
+
462
+
+68
project/mods/Atproto/main.gd
+68
project/mods/Atproto/main.gd
···
1
+
extends Node
2
+
3
+
var config: Dictionary
4
+
5
+
const ATPROTO_SLOT = 99
6
+
var save_loaded: String
7
+
var default_config: Dictionary = {}
8
+
9
+
onready var TackleBox := $"/root/TackleBox"
10
+
const AtProtoClient_t := preload("res://mods/Atproto/atproto_client.gd")
11
+
var AtProtoClient: AtProtoClient_t
12
+
13
+
# UI
14
+
const AtProtoMenu := preload("res://mods/Atproto/ui/menus/atproto_config.tscn")
15
+
const AtProtoButton := preload("res://mods/Atproto/ui/buttons/atproto.tscn")
16
+
17
+
var setuped = false
18
+
19
+
func _enter_tree():
20
+
AtProtoClient = AtProtoClient_t.new()
21
+
add_child(AtProtoClient)
22
+
AtProtoClient.connect("savefile_loaded", self, "set_save_file")
23
+
get_tree().connect("node_added", self, "_add_atproto_menu")
24
+
25
+
26
+
func _ready() -> void:
27
+
_init_config()
28
+
29
+
func _init_config():
30
+
var saved_config = TackleBox.get_mod_config(name)
31
+
for key in default_config.keys():
32
+
if not saved_config[key]:
33
+
saved_config[key] = default_config[key]
34
+
config = saved_config
35
+
TackleBox.set_mod_config(name, config)
36
+
if config.Autoconnect == true:
37
+
AtProtoClient.login(config.Handle, config.Password)
38
+
39
+
func _save_config():
40
+
TackleBox.set_mod_config(name, config)
41
+
42
+
func _add_atproto_menu(node: Node):
43
+
if node.name == "main_menu":
44
+
var atproto_menu: Node = AtProtoMenu.instance()
45
+
atproto_menu.visible = false
46
+
node.add_child(atproto_menu)
47
+
48
+
var button = AtProtoButton.instance()
49
+
var menu_list: Node = node.get_node("VBoxContainer")
50
+
var settings_button: Node = menu_list.get_node("settings")
51
+
menu_list.add_child(button)
52
+
menu_list.move_child(button, settings_button.get_index() + 1)
53
+
atproto_menu.connect("setup_done", self, "_after_setup")
54
+
pass
55
+
56
+
func _after_setup():
57
+
if setuped:
58
+
return
59
+
setuped = true
60
+
61
+
if config.Save != "" and config.Autoload and AtProtoClient.connected():
62
+
AtProtoClient.load_save(config.Save)
63
+
64
+
func can_save_to_atproto():
65
+
return AtProtoClient.can_save && UserSave.current_loaded_slot == ATPROTO_SLOT && AtProtoClient.connected()
66
+
67
+
func set_save_file(save_uri):
68
+
save_loaded = save_uri
+21
project/project.godot
+21
project/project.godot
···
1
+
; Engine configuration file.
2
+
; It's best edited using the editor UI and not directly,
3
+
; since the parameters that go here are not all obvious.
4
+
;
5
+
; Format:
6
+
; [section] ; section goes between []
7
+
; param=value ; assign values to parameters
8
+
9
+
config_version=4
10
+
11
+
[application]
12
+
13
+
config/name="AtProto Webfishing"
14
+
15
+
[gui]
16
+
17
+
common/drop_mouse_on_gui_input_disabled=true
18
+
19
+
[physics]
20
+
21
+
common/enable_pause_aware_picking=true