+2
-1
.gitignore
+2
-1
.gitignore
+22
Atproto/AtProtoSaveFactory.cs
+22
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("save_file")
17
+
.Matching(TransformationPatternFactory.CreateGdSnippetPattern(
18
+
"\"locked_refs\": PlayerData.locked_refs, \n\t}\n", 2))
19
+
.Do(Operation.Append)
20
+
.With("var atproto = $\"/root/Atproto\"\nif atproto.AtProtoClient.connected() && atproto.save_loaded != null:\n\tatproto.AtProtoClient.save_file()\n", 1)).Build();
21
+
}
22
+
}
+1
-1
Atproto/Atproto.csproj
+1
-1
Atproto/Atproto.csproj
···
4
4
<ImplicitUsings>enable</ImplicitUsings>
5
5
<Nullable>enable</Nullable>
6
6
<AssemblySearchPaths>$(AssemblySearchPaths);$(GDWeavePath)/core</AssemblySearchPaths>
7
-
<Version>1.0.0.0</Version>
7
+
<Version>1.0.1.0</Version>
8
8
<RootNamespace>Atproto</RootNamespace>
9
9
</PropertyGroup>
10
10
+3
Atproto/Config.cs
+3
Atproto/Config.cs
+1
Atproto/Mod.cs
+1
Atproto/Mod.cs
···
9
9
Config = modInterface.ReadConfig<Config>();
10
10
modInterface.RegisterScriptMod(CatchFishFactory.Create(modInterface));
11
11
modInterface.RegisterScriptMod(CatptureFishFactory.Create(modInterface));
12
+
modInterface.RegisterScriptMod(AtProtoSaveFactory.Create(modInterface));
12
13
}
13
14
14
15
public void Dispose() {
+1
-1
Atproto/manifest.json
+1
-1
Atproto/manifest.json
+11
-5
README.md
+11
-5
README.md
···
1
-
# ATProto Webfishing
1
+
# AtProto Webfishing
2
2
3
3
A Webfishing mod that send data to your PDS
4
-
5
4
[Download Link](https://fb.dreamteam.boo/api/public/dl/oMcpvwyP/Webfishing-Atproto.zip)
6
5
6
+
# Features
7
+
- Remote save files
8
+
- Caught fish are saved to your PDS
9
+
7
10
## Requirements
8
11
- [GDWeave](https://thunderstore.io/c/webfishing/p/NotNet/GDWeave/)
9
12
- [TackleBox](https://thunderstore.io/c/webfishing/p/PuppyGirl/TackleBox/)
10
13
11
14
## Configuration
12
-
In game using the TackleBox mod menu, you can input your AtProto Handle as well as an App Password to login
13
-
After it's done, simply save and quit the game, you'll be connected on the next startup
15
+
In game using the AtProto menu, you can input your AtProto Handle as well as an App Password to login.
16
+
17
+
After it's done, you can select a save file or create one, then load it !
18
+
19
+
When a file is loaded, the savedata will be synchronized to your PDS each time the game saves.
14
20
15
21
## Credits
16
22
17
-
- Thanks to [Tiany Ma](https://github.com/tma02) for his utils
23
+
- Thanks to [Tianyi Ma](https://github.com/tma02) for his utils
+1
gdscript/.import/.gdignore
+1
gdscript/.import/.gdignore
···
1
+
+264
gdscript/mods/Atproto/assets/at_proto.tres
+264
gdscript/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 )
+302
-16
gdscript/mods/Atproto/atproto_client.gd
+302
-16
gdscript/mods/Atproto/atproto_client.gd
···
1
1
extends Node
2
2
3
+
# Signals
4
+
signal connection(suceeded)
5
+
3
6
# AtProto
4
7
var did
5
8
var pds
6
9
var accessJwt
7
10
var refreshJwt
8
-
11
+
var Atproto
9
12
10
13
# HTTP
11
14
var requester: HTTPRequest
12
15
13
16
func _enter_tree():
14
17
self.requester = HTTPRequest.new()
18
+
Atproto = self.get_parent()
15
19
self.add_child(self.requester, true)
20
+
21
+
func connected() -> bool:
22
+
return accessJwt != null
16
23
17
24
func is_token_expired() -> bool:
18
25
var json = Marshalls.base64_to_utf8(self.accessJwt.split(".")[1])
···
21
28
var unix = floor(Time.get_unix_time_from_system())
22
29
return expires < unix
23
30
24
-
func create_record(record):
31
+
func get_header():
32
+
return [
33
+
"Authorization: Bearer " + self.accessJwt,
34
+
"Content-Type: application/json"
35
+
]
36
+
37
+
func create_record(record, callback : FuncRef = null):
25
38
26
39
if is_token_expired():
27
-
refresh_token("create_record", record)
40
+
refresh_token("create_record", [record])
28
41
return
29
42
30
43
var payload = {
···
36
49
var json_payload = JSON.print(payload)
37
50
json_payload = json_payload.replace("at_type", "$type")
38
51
52
+
var req = self.requester
53
+
req.request(pds + "/xrpc/com.atproto.repo.createRecord", get_header(), true, HTTPClient.METHOD_POST, json_payload)
54
+
req.connect("request_completed", self, "_create_record_handler", [callback])
55
+
56
+
func _create_record_handler(_result, code, _headers, body: PoolByteArray, callback: FuncRef):
39
57
var req = self.requester
58
+
req.disconnect("request_completed", self, "_create_record_handler")
59
+
if callback == null:
60
+
return
61
+
62
+
var res = parse_json(body.get_string_from_utf8())
63
+
callback.call_func(res)
64
+
65
+
func list_records(callback: FuncRef, collection: String, limit: int = 50, cursor = ""):
66
+
var req = self.requester
67
+
var query_string = "repo=" + did.http_escape()
68
+
query_string += "&collection=" + collection.http_escape()
69
+
query_string += "&limit=" + str(limit).http_escape()
70
+
query_string += "&cursor=" + str(limit).http_escape()
71
+
req.request(pds + "/xrpc/com.atproto.repo.listRecords?" + query_string, get_header(), true, HTTPClient.METHOD_GET)
72
+
req.connect("request_completed", self, "_list_record_handler", [callback])
40
73
41
-
var header = [
42
-
"Authorization: Bearer " + self.accessJwt,
43
-
"Content-Type: application/json"
44
-
]
74
+
75
+
func _list_record_handler(_result, code, _headers, body: PoolByteArray, callback: FuncRef):
76
+
var req = self.requester
77
+
req.disconnect("request_completed", self, "_list_record_handler")
78
+
var res = parse_json(body.get_string_from_utf8())
79
+
callback.call_func(res.records)
80
+
81
+
func get_record(callback: FuncRef, did: String, collection: String, rkey: String):
82
+
var req = self.requester
83
+
var query_string = "repo=" + did.http_escape()
84
+
query_string += "&collection=" + collection.http_escape()
85
+
query_string += "&rkey=" + rkey.http_escape()
86
+
req.request(pds + "/xrpc/com.atproto.repo.getRecord?" + query_string, get_header(), true, HTTPClient.METHOD_GET)
87
+
req.connect("request_completed", self, "_get_record_handle", [callback])
88
+
89
+
func _get_record_handle(_result, code, _headers, body: PoolByteArray, callback: FuncRef):
90
+
var req = self.requester
91
+
req.disconnect("request_completed", self, "_get_record_handle")
92
+
var res = parse_json(body.get_string_from_utf8())
93
+
callback.call_func(res)
94
+
95
+
func put_record(uri, record):
96
+
if is_token_expired():
97
+
refresh_token("put_record", [uri, record])
98
+
return
99
+
100
+
var splitted_uri = uri.split("/")
101
+
102
+
103
+
var payload = {
104
+
repo = splitted_uri[2],
105
+
collection = splitted_uri[3],
106
+
rkey = splitted_uri[4],
107
+
record = record
108
+
}
109
+
110
+
var json_payload = JSON.print(payload)
111
+
json_payload = json_payload.replace("at_type", "$type")
45
112
46
-
req.request(pds + "/xrpc/com.atproto.repo.createRecord", header, true, HTTPClient.METHOD_POST, json_payload)
47
-
113
+
var req = self.requester
114
+
req.request(pds + "/xrpc/com.atproto.repo.putRecord", get_header(), true, HTTPClient.METHOD_POST, json_payload)
48
115
49
116
################
50
117
# LOGIN #
···
57
124
req.request("https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=" + handle)
58
125
59
126
60
-
func after_handle_resolver(_result, _response_code, _headers, body: PoolByteArray, password):
127
+
func after_handle_resolver(_result, code, _headers, body: PoolByteArray, password):
61
128
var req = self.requester
62
129
req.disconnect("request_completed", self, "after_handle_resolver")
130
+
131
+
if code != 200:
132
+
emit_signal("connection", false)
133
+
return
134
+
63
135
var res = parse_json(body.get_string_from_utf8())
64
136
self.did = res.did
65
137
···
67
139
req.request("https://plc.directory/" + self.did)
68
140
69
141
70
-
func after_get_pds(_result, _response_code, _headers, body: PoolByteArray, password):
142
+
func after_get_pds(_result, code, _headers, body: PoolByteArray, password):
71
143
var req = self.requester
72
144
req.disconnect("request_completed", self, "after_get_pds")
73
145
146
+
if code != 200:
147
+
emit_signal("connection", false)
148
+
return
149
+
74
150
var res = parse_json(body.get_string_from_utf8())
75
151
for x in res.service:
76
152
if x.id == "#atproto_pds":
···
84
160
req.connect("request_completed", self, "after_create_session")
85
161
req.request(pds + "/xrpc/com.atproto.server.createSession", ["Content-Type: application/json"], true, HTTPClient.METHOD_POST, JSON.print(payload))
86
162
87
-
88
-
89
-
func after_create_session(_result, _response_code, _headers, body: PoolByteArray):
163
+
func after_create_session(_result, code, _headers, body: PoolByteArray):
90
164
var req = self.requester
91
165
req.disconnect("request_completed", self, "after_create_session")
92
166
167
+
if code != 200:
168
+
emit_signal("connection", false)
169
+
return
170
+
93
171
var res = parse_json(body.get_string_from_utf8())
94
172
self.accessJwt = res.accessJwt
95
173
self.refreshJwt = res.refreshJwt
174
+
emit_signal("connection", true)
96
175
97
176
#######################
98
177
# REFRESH TOKEN #
99
178
#######################
100
-
func refresh_token(method = "", payload = ""):
179
+
func refresh_token(method = "", payload = []):
101
180
var req = self.requester
102
181
103
182
var headers = [
···
116
195
self.refreshJwt = res.refreshJwt
117
196
118
197
if method != "":
119
-
self.call(method, payload)
198
+
self.callv(method, payload)
120
199
121
200
122
201
######################
···
133
212
quality = quality
134
213
}
135
214
create_record(record)
215
+
216
+
217
+
# SAVES
218
+
219
+
func create_save_file(uri: String, filename: String, callback: FuncRef = null):
220
+
var record = {
221
+
at_type = "dev.regnault.webfishing.savefile",
222
+
uri = uri,
223
+
name = filename,
224
+
}
225
+
create_record(record, callback)
226
+
pass
227
+
228
+
func get_saves(callback: FuncRef):
229
+
list_records(callback, "dev.regnault.webfishing.savefile")
230
+
231
+
func save_file(callback: FuncRef = null):
232
+
if !connected(): return
233
+
var save_data = {
234
+
"inventory": PlayerData.inventory,
235
+
"hotbar": PlayerData.hotbar,
236
+
"cosmetics_unlocked": PlayerData.cosmetics_unlocked,
237
+
"cosmetics_equipped": PlayerData.cosmetics_equipped,
238
+
"new_cosmetics": [],
239
+
"version": Globals.GAME_VERSION,
240
+
"muted_players": PlayerData.players_muted,
241
+
"hidden_players": PlayerData.players_hidden,
242
+
"recorded_time": PlayerData.last_recorded_time,
243
+
"money": PlayerData.money,
244
+
"bait_inv": PlayerData.bait_inv,
245
+
"bait_selected": PlayerData.bait_selected,
246
+
"bait_unlocked": PlayerData.bait_unlocked,
247
+
"shop": PlayerData.current_shop,
248
+
"journal": PlayerData.journal_logs,
249
+
"quests": PlayerData.current_quests,
250
+
"completed_quests": PlayerData.completed_quests,
251
+
"level": PlayerData.badge_level,
252
+
"xp": PlayerData.badge_xp,
253
+
"max_bait": PlayerData.max_bait,
254
+
"lure_unlocked": PlayerData.lure_unlocked,
255
+
"lure_selected": PlayerData.lure_selected,
256
+
"saved_aqua_fish": PlayerData.saved_aqua_fish,
257
+
"inbound_mail": PlayerData.inbound_mail,
258
+
"rod_power": PlayerData.rod_power_level,
259
+
"rod_speed": PlayerData.rod_speed_level,
260
+
"rod_chance": PlayerData.rod_chance_level,
261
+
"rod_luck": PlayerData.rod_luck_level,
262
+
"saved_tags": PlayerData.saved_tags,
263
+
"loan_level": PlayerData.loan_level,
264
+
"loan_left": PlayerData.loan_left,
265
+
"buddy_level": PlayerData.buddy_level,
266
+
"buddy_speed": PlayerData.buddy_speed,
267
+
"guitar_shapes": PlayerData.guitar_shapes,
268
+
"fish_caught": PlayerData.fish_caught,
269
+
"cash_total": PlayerData.cash_total,
270
+
"voice_pitch": PlayerData.voice_pitch,
271
+
"voice_speed": PlayerData.voice_speed,
272
+
"locked_refs": PlayerData.locked_refs,
273
+
}
274
+
save_data = save_data.duplicate(true)
275
+
276
+
# JOURNAL
277
+
var modified_journal = []
278
+
for area in save_data.journal:
279
+
var area_entry = {
280
+
name = area,
281
+
entries = []
282
+
}
283
+
for entry_name in save_data.journal[area]:
284
+
var entry = save_data.journal[area][entry_name]
285
+
area_entry.entries.append({
286
+
name = entry_name,
287
+
count = entry.count,
288
+
record = str(entry.record),
289
+
quality = entry.quality
290
+
})
291
+
modified_journal.append(area_entry)
292
+
save_data.journal = modified_journal
293
+
294
+
# Quests
295
+
var modified_quests = []
296
+
for quest_id in save_data.quests:
297
+
var entry = save_data.quests[quest_id].duplicate(true)
298
+
entry.id = quest_id
299
+
modified_quests.append(entry)
300
+
save_data.quests = modified_quests
301
+
302
+
# Inventory
303
+
for item in save_data.inventory:
304
+
item.size = str(item.size)
305
+
306
+
307
+
# Version
308
+
save_data.version = str(save_data.version)
309
+
310
+
# Voice Pitch
311
+
save_data.voice_pitch = str(save_data.voice_pitch)
312
+
313
+
# Aqua Fish
314
+
save_data.saved_aqua_fish.size = str(save_data.saved_aqua_fish.size)
315
+
316
+
# Letters
317
+
for letter in save_data.inbound_mail:
318
+
for item in letter.items:
319
+
item.size = str(item.size)
320
+
save_data.at_type = "dev.regnault.webfishing.save"
321
+
322
+
if Atproto.save_loaded:
323
+
put_record(Atproto.save_loaded, save_data)
324
+
else:
325
+
create_record(save_data, callback)
326
+
327
+
func load_save(uri: String):
328
+
var splitted_uri = uri.split("/")
329
+
var did = splitted_uri[2]
330
+
var collection = splitted_uri[3]
331
+
var rkey = splitted_uri[4]
332
+
get_record(funcref(self, "_after_get_save"), did, collection, rkey)
333
+
pass
334
+
335
+
func _after_get_save(save_record):
336
+
var save = save_record.value
337
+
338
+
var modified_journal: Dictionary = {}
339
+
for area in save.journal:
340
+
var area_entries = {}
341
+
for entry in area.entries:
342
+
area_entries[entry.name] = {
343
+
count = entry.count,
344
+
record = float(entry.record),
345
+
quality = entry.quality
346
+
}
347
+
348
+
modified_journal[area.name] = area_entries
349
+
save.journal = modified_journal
350
+
351
+
var modified_quests = {}
352
+
for quest in save.quests:
353
+
var id = quest.id
354
+
modified_quests[quest.id] = quest
355
+
modified_quests[quest.id].erase("id")
356
+
save.quests = modified_quests
357
+
358
+
# Inventory
359
+
for item in save.inventory:
360
+
item.size = float(item.size)
361
+
item.quality = int(item.quality)
362
+
var x = PlayerData.QUALITY_DATA[item.quality]
363
+
364
+
save.version = float(save.version)
365
+
save.saved_aqua_fish.size = float(save.saved_aqua_fish.size)
366
+
for letter in save.inbound_mail:
367
+
for item in letter.items:
368
+
item.size = float(item.size)
369
+
item.quality = int(item.quality)
370
+
var x = PlayerData.QUALITY_DATA[item.quality]
371
+
372
+
var modified_hotbar = {}
373
+
for item in save.hotbar:
374
+
modified_hotbar[int(item)] = save.hotbar[item]
375
+
save.hotbar = modified_hotbar
376
+
377
+
PlayerData.inventory = save.inventory
378
+
PlayerData.hotbar = save.hotbar
379
+
PlayerData.cosmetics_unlocked = save.cosmetics_unlocked
380
+
PlayerData.cosmetics_equipped = save.cosmetics_equipped
381
+
PlayerData.money = save.money
382
+
PlayerData.players_muted = save.muted_players
383
+
PlayerData.players_hidden = save.hidden_players
384
+
PlayerData.bait_inv = save.bait_inv
385
+
PlayerData.bait_selected = save.bait_selected
386
+
PlayerData.bait_unlocked = save.bait_unlocked
387
+
PlayerData.max_bait = save.max_bait
388
+
PlayerData.lure_unlocked = save.lure_unlocked
389
+
PlayerData.lure_selected = save.lure_selected
390
+
PlayerData.journal_logs = save.journal
391
+
PlayerData.current_quests = save.quests
392
+
PlayerData.completed_quests = save.completed_quests
393
+
PlayerData.badge_level = int(save.level)
394
+
PlayerData.badge_xp = int(save.xp)
395
+
PlayerData.saved_aqua_fish = save.saved_aqua_fish
396
+
PlayerData.inbound_mail = save.inbound_mail
397
+
PlayerData.saved_tags = save.saved_tags
398
+
PlayerData.loan_level = int(save.loan_level)
399
+
PlayerData.loan_left = save.loan_left
400
+
PlayerData.rod_power_level = save.rod_power
401
+
PlayerData.rod_speed_level = save.rod_speed
402
+
PlayerData.rod_chance_level = save.rod_chance
403
+
PlayerData.rod_luck_level = save.rod_luck
404
+
PlayerData.buddy_level = save.buddy_level
405
+
PlayerData.buddy_speed = save.buddy_speed
406
+
PlayerData.guitar_shapes = save.guitar_shapes
407
+
PlayerData.fish_caught = save.fish_caught
408
+
PlayerData.cash_total = save.cash_total
409
+
PlayerData.voice_pitch = float(save.voice_pitch)
410
+
PlayerData.voice_speed = save.voice_speed
411
+
PlayerData.locked_refs = save.locked_refs
412
+
PlayerData._validate_guitar_shapes()
413
+
PlayerData._validate_inventory()
414
+
PlayerData._journal_check()
415
+
PlayerData._missing_quest_check()
416
+
PlayerData._unlock_defaults()
417
+
418
+
Atproto.config.Save = save_record.uri
419
+
Atproto.save_loaded = save_record.uri
420
+
PopupMessage._show_popup("AtProto save file loaded")
421
+
+37
-1
gdscript/mods/Atproto/main.gd
+37
-1
gdscript/mods/Atproto/main.gd
···
1
1
extends Node
2
2
3
3
var config: Dictionary
4
+
var save_loaded: String
4
5
var default_config: Dictionary = {}
5
6
6
7
onready var TackleBox := $"/root/TackleBox"
7
8
const AtProtoClient_t := preload("res://mods/Atproto/atproto_client.gd")
8
9
var AtProtoClient: AtProtoClient_t
9
10
11
+
# UI
12
+
const AtProtoMenu := preload("res://mods/Atproto/ui/menus/atproto_config.tscn")
13
+
const AtProtoButton := preload("res://mods/Atproto/ui/buttons/atproto.tscn")
14
+
15
+
var setuped = false
16
+
10
17
func _enter_tree():
11
18
AtProtoClient = AtProtoClient_t.new()
12
19
add_child(AtProtoClient)
20
+
get_tree().connect("node_added", self, "_add_atproto_menu")
21
+
13
22
14
23
func _ready() -> void:
24
+
print("QUOTA:", Steam.getQuota())
15
25
TackleBox.connect("mod_config_updated", self, "_on_config_update")
26
+
16
27
_init_config()
17
28
18
29
func _init_config():
···
22
33
saved_config[key] = default_config[key]
23
34
config = saved_config
24
35
TackleBox.set_mod_config(name, config)
25
-
if config.Handle != "" and config.Password != "":
36
+
if config.Autoconnect == true:
26
37
AtProtoClient.login(config.Handle, config.Password)
27
38
28
39
40
+
func _save_config():
41
+
TackleBox.set_mod_config(name, config)
42
+
29
43
func _on_config_update(mod_id: String, new_config: Dictionary) -> void:
30
44
if mod_id != name:
31
45
return
···
34
48
config = new_config
35
49
if config.Handle != "" and config.Password != "":
36
50
AtProtoClient.login(config.Handle, config.Password)
51
+
52
+
func _add_atproto_menu(node: Node):
53
+
if node.name == "main_menu":
54
+
var atproto_menu: Node = AtProtoMenu.instance()
55
+
atproto_menu.visible = false
56
+
node.add_child(atproto_menu)
57
+
58
+
var button = AtProtoButton.instance()
59
+
var menu_list: Node = node.get_node("VBoxContainer")
60
+
var settings_button: Node = menu_list.get_node("settings")
61
+
menu_list.add_child(button)
62
+
menu_list.move_child(button, settings_button.get_index() + 1)
63
+
atproto_menu.connect("setup_done", self, "_after_setup")
64
+
pass
65
+
66
+
func _after_setup():
67
+
if setuped:
68
+
return
69
+
setuped = true
70
+
71
+
if config.Save != "" and config.Autoload and AtProtoClient.connected():
72
+
AtProtoClient.load_save(config.Save)
+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
+
}