-
Notifications
You must be signed in to change notification settings - Fork 8
/
StorageSQLite.lua
1130 lines (889 loc) · 29.1 KB
/
StorageSQLite.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- StorageSQLite.lua
-- Implements the SQLite-backed database storage
--[[
Usage: Call SQLite_CreateStorage() to get an object that has all the functions implemented below.
g_DB = SQLite_CreateStorage(config)
...
local areas = g_DB:LoadAllPlayerAreas()
--]]
--- The SQLite backend namespace:
local StorageSQLite = {}
--- The columns definition for the Areas table
-- A lookup map of LowerCaseColumnName => {ColumnName, ColumnType} is added in the initialization
local g_AreasColumns =
{
{"ID", "INTEGER PRIMARY KEY AUTOINCREMENT"},
{"MinX", "INTEGER"}, -- The bounds of this area, including the non-buildable "sidewalk"
{"MaxX", "INTEGER"}, -- The bounds of this area, including the non-buildable "sidewalk"
{"MinZ", "INTEGER"}, -- The bounds of this area, including the non-buildable "sidewalk"
{"MaxZ", "INTEGER"}, -- The bounds of this area, including the non-buildable "sidewalk"
{"StartX", "INTEGER"}, -- The buildable bounds of this area
{"EndX", "INTEGER"}, -- The buildable bounds of this area
{"StartZ", "INTEGER"}, -- The buildable bounds of this area
{"EndZ", "INTEGER"}, -- The buildable bounds of this area
{"Name", "TEXT"}, -- The name given to the area
{"WorldName", "TEXT"}, -- Name of the world where the area belongs
{"PlayerName", "TEXT"}, -- Name of the owner
{"GalleryName", "TEXT"}, -- Name of the gallery from which the area has been claimed
{"GalleryIndex", "INTEGER"}, -- Index of the area in the gallery from which this area has been claimed
{"DateClaimed", "TEXT"}, -- ISO 8601 DateTime of the claiming
{"ForkedFromID", "INTEGER"}, -- The ID of the area from which this one has been forked
{"IsLocked", "INTEGER"}, -- If nonzero, the area is locked and cannot be edited unless the player has the "gallery.admin.overridelocked" permission
{"LockedBy", "TEXT"}, -- Name of the player who last locked / unlocked the area
{"DateLocked", "TEXT"}, -- ISO 8601 DateTime when the area was last locked / unlocked
{"DateLastChanged", "TEXT"}, -- ISO 8601 DateTime when the area was last changed
{"TickLastChanged", "INTEGER"}, -- World Age, in ticks, when the area was last changed
{"NumPlacedBlocks", "INTEGER"}, -- Total number of blocks that the players have placed in the area
{"NumBrokenBlocks", "INTEGER"}, -- Total number of blocks that the players have broken in the area
{"EditMaxX", "INTEGER"}, -- Maximum X coord of the edits within the area
{"EditMaxY", "INTEGER"}, -- Maximum Y coord of the edits within the area
{"EditMaxZ", "INTEGER"}, -- Maximum Z coord of the edits within the area
{"EditMinX", "INTEGER"}, -- Minimum X coord of the edits within the are
{"EditMinY", "INTEGER"}, -- Minimum Y coord of the edits within the are
{"EditMinZ", "INTEGER"}, -- Minimum Z coord of the edits within the are
}
--- Formats the datetime (as returned by os.time() ) into textual representation used in the DB
function FormatDateTime(a_DateTime)
assert(type(a_DateTime) == "number");
return os.date("%Y-%m-%dT%H:%M:%S", a_DateTime);
end
--- Fixes the area table after loading
-- Assigns the proper Gallery object to the area
-- Synthesizes area name if not present
-- Returns the input table on success, nil on failure
function StorageSQLite:FixupAreaAfterLoad(a_Area)
-- Check params:
assert(type(a_Area) == "table")
assert(a_Area.GalleryName ~= nil)
-- Assign the proper Gallery object to the area:
if (a_Area.Gallery == nil) then
a_Area.Gallery = FindGalleryByName(a_Area.GalleryName);
if (a_Area.Gallery == nil) then
return;
end
end
-- Fix area name, if not present:
if ((a_Area.Name == nil) or (a_Area.Name == "")) then
a_Area.Name = a_Area.GalleryName .. " " .. tostring(a_Area.GalleryIndex)
end
-- Convert IsLocked from "number or bool" to "bool":
a_Area.IsLocked = (a_Area.IsLocked ~= 0) and (a_Area.IsLocked ~= false) and (a_Area.IsLocked ~= nil)
-- Add some defaults:
a_Area.NumPlacedBlocks = a_Area.NumPlacedBlocks or 0
a_Area.NumBrokenBlocks = a_Area.NumBrokenBlocks or 0
return a_Area
end
--- Returns a table of top area counts per player, up to a_Limit rows (sorted by count desc)
--[[
If a_PlayerName is given, that player is added to the table as well (if not already there)
The table returned has the format:
{
{NumAreas = Count1, PlayerName = "PlayerName1"},
{NumAreas = Count2, PlayerName = "PlayerName2"},
...
}
--]]
function StorageSQLite:GetPlayerAreaCounts(a_Limit, a_PlayerName)
a_Limit = tonumber(a_Limit) or 5
-- Add the top N players:
local res = {}
self:ExecuteStatement(
"SELECT COUNT(*) AS NumAreas, PlayerName FROM Areas GROUP BY PlayerName ORDER BY NumAreas DESC LIMIT ?", -- .. a_Limit,
{ a_Limit },
function (a_Values)
local PlayerName = a_Values["PlayerName"]
if (a_Values["NumAreas"] and PlayerName) then
table.insert(res, {NumAreas = a_Values["NumAreas"], PlayerName = PlayerName})
if (PlayerName == a_PlayerName) then
a_PlayerName = nil -- Do not add the specified player, they're already present
end
end
end
)
-- Add a_Player, if not already added:
if (a_PlayerName) then
local HasFound = false
self:ExecuteStatement(
"SELECT COUNT(*) AS NumAreas FROM Areas WHERE PlayerName = ?",
{ a_PlayerName },
function (a_Values)
if (a_Values["NumAreas"] and a_Values["PlayerName"]) then
table.insert(res, {NumAreas = a_Values["NumAreas"], PlayerName = a_PlayerName})
HasFound = true
end
end
)
if not(HasFound) then
table.insert(res, {NumAreas = 0, PlayerName = a_PlayerName})
end
end
return res
end
--- Loads the areas for a single player in the specified world
-- Returns a table that has both an array of the area objects, as well as a map AreaName -> area object
-- Also deletes areas with invalid gallery from the DB (TODO: move this to a separate function?)
function StorageSQLite:LoadPlayerAreasInWorld(a_WorldName, a_PlayerName)
assert(a_WorldName ~= nil);
assert(a_PlayerName ~= nil);
local res = {};
local ToDelete = {}; -- List of IDs to delete because of missing Gallery
self:ExecuteStatement(
"SELECT * FROM Areas WHERE PlayerName = ? AND WorldName = ?",
{
a_PlayerName,
a_WorldName,
},
function (a_Values)
local area = self:FixupAreaAfterLoad(a_Values)
if (area == nil) then
-- The area is invalid (bad gallery etc), schedule it for removing:
table.insert(ToDelete, a_Values.ID);
else
table.insert(res, area);
res[area.Name] = area;
end
end
)
-- Remove areas that reference non-existent galleries:
if (ToDelete[1] ~= nil) then
local stmt = self.DB:prepare("DELETE FROM Areas WHERE ID = ?");
for idx, id in ipairs(ToDelete) do
stmt:bind_values(id);
stmt:step();
end
stmt:finalize();
end
return res;
end
--- Loads up to a_NumAreas most recently claimed areas
-- Returns an array-table of area descriptions, in recentness order (most recent first)
function StorageSQLite:LoadLatestClaimedAreas(a_NumAreas)
-- Check params:
assert(self)
assert(tonumber(a_NumAreas))
-- Query the DB:
local res = {}
self:ExecuteStatement(
"SELECT * FROM Areas ORDER BY DateClaimed DESC LIMIT " .. tonumber(a_NumAreas),
{},
function (a_Values)
local area = self:FixupAreaAfterLoad(a_Values)
if (area) then
table.insert(res, area)
end
end
)
return res
end
--- Loads up to a_NumAreas most recently changed areas
-- Returns an array-table of area descriptions, in recentness order (most recent first)
function StorageSQLite:LoadLatestChangedAreas(a_NumAreas)
-- Check params:
assert(self)
assert(tonumber(a_NumAreas))
-- Query the DB:
local res = {}
self:ExecuteStatement(
"SELECT * FROM Areas WHERE ((NumPlacedBlocks > 0) OR (NumBrokenBlocks > 0)) ORDER BY DateLastChanged DESC LIMIT " .. tonumber(a_NumAreas),
{},
function (a_Values)
local area = self:FixupAreaAfterLoad(a_Values)
if (area) then
table.insert(res, area)
end
end
)
return res
end
--- Loads all player allowances in the specified world
-- Returns a table that has both an array of the area objects, as well as a map AreaName -> area object
function StorageSQLite:LoadPlayerAllowancesInWorld(a_WorldName, a_PlayerName)
local res = {};
self:ExecuteStatement(
[[
SELECT Areas.MinX AS MinX, Areas.MinZ AS MinZ, Areas.MaxX AS MaxX, Areas.MaxZ as MaxZ,
Areas.StartX AS StartX, Areas.StartZ AS StartZ, Areas.EndX AS EndX, Areas.EndZ AS EndZ,
Areas.PlayerName AS PlayerName, Areas.Name AS Name, Areas.ID AS ID,
Areas.GalleryIndex AS GalleryIndex, Areas.GalleryName AS GalleryName,
Areas.IsLocked AS IsLocked, Areas.LockedBy AS LockedBy, Areas.DateLocked as DateLocked
FROM Areas INNER JOIN Allowances ON Areas.ID = Allowances.AreaID
WHERE Areas.WorldName = ? AND Allowances.FriendName = ?
]],
{
a_WorldName,
a_PlayerName,
},
function (a_Values)
local area = self:FixupAreaAfterLoad(a_Values)
if (area == nil) then
return;
end
table.insert(res, area)
res[area.Name] = area
end
);
return res;
end
--- Loads the areas for a single player in the specified gallery
-- Returns a table that has both an array of the area objects, as well as a map AreaName -> area object
function StorageSQLite:LoadPlayerAreasInGallery(a_GalleryName, a_PlayerName)
assert(a_GalleryName ~= nil);
assert(a_PlayerName ~= nil);
local Gallery = FindGalleryByName(a_GalleryName);
if (Gallery == nil) then
-- no such gallery
return {};
end
local res = {};
self:ExecuteStatement(
"SELECT * FROM Areas WHERE PlayerName = ? AND GalleryName = ?",
{
a_PlayerName,
a_GalleryName,
},
function (a_Values)
-- Assign the proper gallery object to the area:
local area = self:FixupAreaAfterLoad(a_Values)
if (area == nil) then
return;
end
table.insert(res, area);
res[area.Name] = area;
end
)
return res;
end
--- Loads all the areas in the DB
-- Returns a table that has both an array of the area objects, as well as a map AreaName -> area object
function StorageSQLite:LoadAllAreas()
local res = {};
self:ExecuteStatement(
"SELECT * FROM Areas",
{},
function (a_Values)
-- Assign the proper gallery:
local area = self:FixupAreaAfterLoad(a_Values)
if (area == nil) then
return
end
table.insert(res, area)
res[area.Name] = area
end
)
return res;
end
--- Loads all the areas for a single player
-- Returns a table that has both an array of the area objects, as well as a map AreaName -> area object
function StorageSQLite:LoadAllPlayerAreas(a_PlayerName)
assert(a_PlayerName ~= nil);
local res = {};
self:ExecuteStatement(
"SELECT * FROM Areas WHERE PlayerName = ?",
{
a_PlayerName
},
function (a_Values)
-- Assign the proper gallery:
local area = self:FixupAreaAfterLoad(a_Values)
if (area == nil) then
return
end
table.insert(res, area)
res[area.Name] = area
end
)
return res;
end
--- Loads an area of the specified name owned by the specified player
function StorageSQLite:LoadPlayerAreaByName(a_PlayerName, a_AreaName)
assert(a_PlayerName ~= nil);
assert(a_AreaName ~= nil);
assert(a_AreaName ~= "");
local res = nil;
self:ExecuteStatement(
"SELECT * FROM Areas WHERE PlayerName = ? AND Name = ?",
{a_PlayerName, a_AreaName},
function (a_Values)
-- Assign the proper gallery:
res = self:FixupAreaAfterLoad(a_Values)
end
);
return res;
end
--- Loads an area identified by its ID
-- Returns the loaded area, or nil if there's no such area
function StorageSQLite:LoadAreaByID(a_AreaID)
-- Check params:
a_AreaID = tonumber(a_AreaID)
assert(a_AreaID ~= nil)
local res = nil
self:ExecuteStatement(
"SELECT * FROM Areas WHERE ID = ?",
{
a_AreaID
},
function (a_Values)
res = self:FixupAreaAfterLoad(a_Values)
end
)
return res
end
--- Loads whatever area intersects the given block coords.
-- Returns the loaded area, or nil if there's no area
function StorageSQLite:LoadAreaByPos(a_WorldName, a_BlockX, a_BlockZ)
assert(a_WorldName ~= nil);
assert(a_BlockX ~= nil);
assert(a_BlockZ ~= nil);
a_BlockX = math.floor(a_BlockX);
a_BlockZ = math.floor(a_BlockZ);
local res = nil;
self:ExecuteStatement(
"SELECT * FROM Areas WHERE WorldName = ? AND MinX <= ? AND MaxX > ? AND MinZ <= ? AND MaxZ > ?",
{a_WorldName, a_BlockX, a_BlockX, a_BlockZ, a_BlockZ},
function (a_Values)
res = self:FixupAreaAfterLoad(a_Values)
end
);
return res;
end
--- Loads the next area index for each gallery
function StorageSQLite:LoadGalleries()
for idx, gallery in ipairs(g_Galleries) do
local SQL = "SELECT NextAreaIdx FROM GalleryEnd WHERE GalleryName = \"" .. gallery.Name .. "\"";
self:DBExec(SQL,
function (UserData, NumCols, Values, Names)
for i = 1, NumCols do
if (Names[i] == "NextAreaIdx") then
gallery.NextAreaIdx = tonumber(Values[i]);
return 0;
end
end
return 0;
end
);
if (gallery.NextAreaIdx == nil) then
self:ExecuteStatement(
"SELECT MAX(GalleryIndex) as mx FROM Areas WHERE GalleryName = ?",
{ gallery.Name },
function (a_Values)
gallery.NextAreaIdx = a_Values.mx;
end
);
if (gallery.NextAreaIdx == nil) then
-- This is normal for when the gallery was created for the very first time. Create the record in the DB.
gallery.NextAreaIdx = 0;
else
-- This is not normal, warn the admin about possible DB corruption:
LOGWARNING("Gallery \"" .. gallery.Name .. "\" doesn't have its NextAreaIdx set in the database, will be reset to " .. gallery.NextAreaIdx);
end
self:ExecuteStatement("INSERT INTO GalleryEnd (GalleryName, NextAreaIdx) VALUES (?, ?)", {gallery.Name, gallery.NextAreaIdx});
end
end
end
--- Returns an array Areas for the areas in the specified index range in the specified gallery
-- If an area is not claimed, the array entry for it will be {}
-- a_SortBy is the column on which to sort. It is checked against the list of columns and if it doesn't fit any, the default "GalleryIndex" is used instead
function StorageSQLite:LoadGalleryAreasRange(a_GalleryName, a_SortBy, a_StartIndex, a_EndIndex)
-- Check the a_SortBy column:
if not(g_AreasColumns[a_SortBy:lower()]) then
a_SortBy = "GalleryIndex"
end
if (g_AreasColumns[a_SortBy:lower()][2] == "TEXT") then
a_SortBy = a_SortBy .. " COLLATE NOCASE"
end
-- Get the results:
local res = {}
self:ExecuteStatement(
"SELECT * FROM Areas WHERE GalleryName = ? ORDER BY " .. a_SortBy .. " LIMIT ? OFFSET ?",
{
a_GalleryName,
a_EndIndex - a_StartIndex,
a_StartIndex
},
function (a_Values)
table.insert(res, self:FixupAreaAfterLoad(a_Values) or {})
end
)
return res
end
--- Stores a new area into the DB
function StorageSQLite:AddArea(a_Area)
-- Check params:
assert(type(a_Area) == "table");
-- Add in the DB:
local DateTimeNow = FormatDateTime(os.time())
self:ExecuteStatement(
"INSERT INTO Areas \
(MinX, MaxX, MinZ, MaxZ, StartX, EndX, StartZ, EndZ, GalleryName, GalleryIndex, WorldName, \
PlayerName, Name, DateClaimed, ForkedFromID, IsLocked, DateLastChanged, TickLastChanged, \
NumPlacedBlocks, NumBrokenBlocks) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
{
a_Area.MinX, a_Area.MaxX, a_Area.MinZ, a_Area.MaxZ,
a_Area.StartX, a_Area.EndX, a_Area.StartZ, a_Area.EndZ,
a_Area.Gallery.Name, a_Area.GalleryIndex,
a_Area.Gallery.WorldName,
a_Area.PlayerName,
a_Area.Name,
DateTimeNow,
(a_Area.ForkedFrom or {}).ID or -1,
a_Area.IsLocked or 0,
DateTimeNow,
a_Area.Gallery.World:GetWorldAge(),
a_Area.NumPlacedBlocks or 0,
a_Area.NumBrokenBlocks or 0,
}
);
a_Area.ID = self.DB:last_insert_rowid();
end
--- Checks the specified gallery's area indices against the Claimed and Removed area lists.
-- Areas that are in neither are added to the Removed list
function StorageSQLite:CheckAreaIndices(a_Gallery, a_RemovedBy)
-- Check params:
a_RemovedBy = tostring(a_RemovedBy)
assert(type(a_Gallery) == "table")
assert(a_Gallery.Name)
assert(a_RemovedBy)
-- Walk through all claimed areas, remember them in a map of GalleryIndex -> true:
local IsPresent = {}
local MaxIndex = 0
self:ExecuteStatement(
"SELECT GalleryIndex FROM Areas WHERE GalleryName = ?",
{
a_Gallery.Name
},
function (a_Values)
IsPresent[a_Values.GalleryIndex] = true
if (a_Values.GalleryIndex > MaxIndex) then
MaxIndex = a_Values.GalleryIndex
end
end
)
-- Walk through all removed areas, add them to the map:
self:ExecuteStatement(
"SELECT GalleryIndex FROM RemovedAreas WHERE GalleryName = ?",
{
a_Gallery.Name
},
function (a_Values)
IsPresent[a_Values.GalleryIndex] = true
end
)
-- Add all areas between index 0 and MaxIndex that are not present into the RemovedAreas table:
local now = FormatDateTime(os.time())
for idx = 0, MaxIndex do
if not(IsPresent[idx]) then
self:ExecuteStatement(
"INSERT INTO RemovedAreas(GalleryName, GalleryIndex, RemovedBy, DateRemoved) VALUES (?, ?, ?, ?)",
{
a_Gallery.Name,
idx,
a_RemovedBy,
now
}
)
end -- if not(IsPresent)
end -- for idx
-- Remove areas from RemovedAreas that are above the MaxIndex:
self:ExecuteStatement(
"DELETE FROM RemovedAreas WHERE GalleryName = ? AND GalleryIndex > ?",
{
a_Gallery.Name,
MaxIndex
}
)
end
--- Claims an area either from the list of removed areas, or a fresh new one
-- Returns the Area table, or nil and string description of the error
function StorageSQLite:ClaimArea(a_Gallery, a_PlayerName, a_ForkedFromArea)
-- Check params:
assert(type(a_Gallery) == "table")
assert(type(a_PlayerName) == "string")
assert((a_ForkedFromArea == nil) or (type(a_ForkedFromArea) == "table"))
-- Get the index for the new area:
local NextAreaIdx = self:PopRemovedArea(a_Gallery)
if (NextAreaIdx == -1) then
NextAreaIdx = a_Gallery.NextAreaIdx
end
if (NextAreaIdx >= a_Gallery.MaxAreaIdx) then
return nil, "The gallery is full";
end
local AreaX, AreaZ = AreaIndexToCoords(NextAreaIdx, a_Gallery)
local MinX, MaxX, MinZ, MaxZ = AreaCoordsToBlockCoords(a_Gallery, AreaX, AreaZ);
local Area = {
MinX = MinX,
MaxX = MaxX,
MinZ = MinZ,
MaxZ = MaxZ,
StartX = MinX + a_Gallery.AreaEdge,
EndX = MaxX - a_Gallery.AreaEdge,
StartZ = MinZ + a_Gallery.AreaEdge,
EndZ = MaxZ - a_Gallery.AreaEdge,
Gallery = a_Gallery,
GalleryIndex = NextAreaIdx,
PlayerName = a_PlayerName,
Name = a_Gallery.Name .. " " .. tostring(NextAreaIdx),
ForkedFrom = a_ForkedFromArea,
NumPlacedBlocks = 0,
NumBrokenBlocks = 0,
};
self:AddArea(Area);
-- Update the next area idx in the gallery object:
if (a_Gallery.NextAreaIdx == NextAreaIdx) then
a_Gallery.NextAreaIdx = NextAreaIdx + 1;
self:UpdateGallery(a_Gallery);
end
return Area
end
--- Marks the specified area as locked in the DB
-- a_LockedByName is the name of the player locking the area
function StorageSQLite:LockArea(a_Area, a_LockedByName)
-- Check params:
assert(type(a_Area) == "table")
assert(a_Area.ID ~= nil)
assert(type(a_LockedByName) == "string")
-- Set the area's properties:
a_Area.IsLocked = true
a_Area.LockedBy = a_LockedByName
a_Area.DateLocked = FormatDateTime(os.time())
-- Update the DB:
self:ExecuteStatement(
"UPDATE Areas SET IsLocked = ?, LockedBy = ?, DateLocked = ? WHERE ID = ?",
{
1,
a_LockedByName,
a_Area.DateLocked,
a_Area.ID
}
)
end
--- Removes an area from the RemovedAreas table in the specified gallery, and returns its GalleryIndex
-- Returns -1 if there's no suitable area in the RemovedAreas table
function StorageSQLite:PopRemovedArea(a_Gallery)
-- Check params:
assert(type(a_Gallery) == "table")
-- Get the lowest index stored in the DB:
local AreaIndex = -1
local AreaID = -1
self:ExecuteStatement(
"SELECT ID, GalleryIndex FROM RemovedAreas WHERE GalleryName = ? LIMIT 1",
{
a_Gallery.Name
},
function (a_Values)
AreaIndex = a_Values["GalleryIndex"]
AreaID = a_Values["ID"]
end
)
-- If the area is valid, remove it from the table:
if (AreaID < 0) then
return -1
end
self:ExecuteStatement(
"DELETE FROM RemovedAreas WHERE ID = ?",
{
AreaID
}
)
return AreaIndex
end
function StorageSQLite:IsAreaNameUsed(a_PlayerName, a_WorldName, a_AreaName)
assert(a_PlayerName ~= nil);
assert(a_WorldName ~= nil);
assert(a_AreaName ~= nil);
local IsNameUsed = false;
self:ExecuteStatement(
"SELECT ID FROM Areas WHERE WorldName = ? AND PlayerName = ? AND Name = ?",
{
a_WorldName,
a_PlayerName,
a_AreaName,
},
function (a_Values)
IsNameUsed = true;
end
);
return IsNameUsed;
end
--- Removes the claim on the specified area
-- The area is recycled into the RemovedAreas table which then serves as source of new areas for claiming
-- a_RemovedBy is the name of the player removing the area
function StorageSQLite:RemoveArea(a_Area, a_RemovedBy)
-- Check params:
assert(type(a_Area) == "table")
assert(type(a_RemovedBy) == "string")
-- TODO: Check that the area really exists
-- Add the area to the RemovedAreas table:
self:ExecuteStatement(
"INSERT INTO RemovedAreas (GalleryIndex, GalleryName, DateRemoved, RemovedBy) VALUES (?, ?, ?, ?)",
{
a_Area.GalleryIndex,
a_Area.Gallery.Name,
FormatDateTime(os.time()),
a_RemovedBy
}
)
-- Remove the area from the Areas table:
self:ExecuteStatement(
"DELETE FROM Areas WHERE ID = ?",
{
a_Area.ID
}
)
-- Remove any allowances on the area:
self:ExecuteStatement(
"DELETE FROM Allowances WHERE AreaID = ?",
{
a_Area.ID
}
)
end
--- Modifies an existing area's name, if it doesn't collide with any other existing area names
-- If the name is already used, returns false; returns true if renamed successfully
function StorageSQLite:RenameArea(a_PlayerName, a_AreaName, a_NewName)
assert(a_PlayerName ~= nil);
assert(a_AreaName ~= nil);
assert(a_NewName ~= nil);
-- Load the area:
local Area = self:LoadPlayerAreaByName(a_PlayerName, a_AreaName);
if (Area == nil) then
return false, "Area doesn't exist";
end
-- Check if the name is already used:
if (self:IsAreaNameUsed(a_PlayerName, Area.Gallery.WorldName, a_NewName)) then
return false
end
-- Rename the area:
self:ExecuteStatement(
"UPDATE Areas SET Name = ? WHERE GalleryName = ? AND ID = ?",
{
a_NewName,
Area.Gallery.Name,
Area.ID,
}
);
return true;
end
--- Marks the specified area as unlocked in the DB
-- a_UnlockedByName is the name of the player unlocking the area
function StorageSQLite:UnlockArea(a_Area, a_UnlockedByName)
-- Check params:
assert(type(a_Area) == "table")
assert(a_Area.ID ~= nil)
assert(type(a_UnlockedByName) == "string")
-- Set the area's properties:
a_Area.IsLocked = false
a_Area.LockedBy = a_UnlockedByName
a_Area.DateLocked = FormatDateTime(os.time())
-- Update the DB:
self:ExecuteStatement(
"UPDATE Areas SET IsLocked = ?, LockedBy = ?, DateLocked = ? WHERE ID = ?",
{
0,
a_UnlockedByName,
a_Area.DateLocked,
a_Area.ID
}
)
end
--- Updates the NumPlacedBlocks and NumBrokenBlocks values in the DB for the specified area
function StorageSQLite:UpdateAreaBlockStats(a_Area)
-- Check params:
assert(type(a_Area) == "table")
assert(a_Area.ID ~= nil)
-- Update the DB:
self:ExecuteStatement(
"UPDATE Areas SET NumPlacedBlocks = ?, NumBrokenBlocks = ? WHERE ID = ?",
{
a_Area.NumPlacedBlocks,
a_Area.NumBrokenBlocks,
a_Area.ID
}
)
end
--- Updates the DateLastChanged, TickLastChanged, NumPlacedBlocks, NumBrokenBlocks and edit range values in the DB for the specified area
function StorageSQLite:UpdateAreaBlockStatsAndEditRange(a_Area)
-- Check params:
assert(type(a_Area) == "table")
assert(a_Area.ID)
-- Update the DB:
self:ExecuteStatement(
"UPDATE Areas SET \
NumPlacedBlocks = ?, NumBrokenBlocks = ?, \
DateLastChanged = ?, TickLastChanged = ?, \
EditMinX = ?, EditMinY = ?, EditMinZ = ?, \
EditMaxX = ?, EditMaxY = ?, EditMaxZ = ? \
WHERE ID = ?",
{
a_Area.NumPlacedBlocks,
a_Area.NumBrokenBlocks,
FormatDateTime(os.time()), a_Area.Gallery.World:GetWorldAge(),
a_Area.EditMinX, a_Area.EditMinY, a_Area.EditMinZ,
a_Area.EditMaxX, a_Area.EditMaxY, a_Area.EditMaxZ,
a_Area.ID
}
)
end
--- Updates the edit range values (MaxEditX etc.) in the DB for the specified area
function StorageSQLite:UpdateAreaEditRange(a_Area)
-- Check params:
assert(type(a_Area) == "table")
assert(a_Area.ID)
-- Update the DB:
self:ExecuteStatement(
"UPDATE Areas SET EditMaxX = ?, EditMaxY = ?, EditMaxZ = ?, EditMinX = ?, EditMinY = ?, EditMinZ = ? WHERE ID = ?",
{
a_Area.EditMaxX, a_Area.EditMaxY, a_Area.EditMaxZ,
a_Area.EditMinX, a_Area.EditMinY, a_Area.EditMinZ,
a_Area.ID
}
)
end
--- Updates the DateLastChanged, TickLastChanged, NumPlacedBlocks and NumBrokenBlocks values in the DB for the specified area
function StorageSQLite:UpdateAreaStats(a_Area)
-- Check params:
assert(type(a_Area) == "table")
assert(a_Area.ID ~= nil)
-- Update the DB:
self:ExecuteStatement(
"UPDATE Areas SET DateLastChanged = ?, TickLastChanged = ?, NumPlacedBlocks = ?, NumBrokenBlocks = ? WHERE ID = ?",
{
FormatDateTime(os.time()),
a_Area.Gallery.World:GetWorldAge(),
a_Area.NumPlacedBlocks,
a_Area.NumBrokenBlocks,
a_Area.ID
}
)
end
function StorageSQLite:UpdateGallery(a_Gallery)
self:ExecuteStatement(
"UPDATE GalleryEnd SET NextAreaIdx = ? WHERE GalleryName = ?",
{
a_Gallery.NextAreaIdx,
a_Gallery.Name
}
);
end
--- Adds the playername to the list of allowed players in the specified area
-- Returns success state and an error message in case of failure
function StorageSQLite:AllowPlayerInArea(a_Area, a_PlayerName)
assert(a_Area ~= nil)
assert(a_Area.ID ~= nil)
assert(type(a_PlayerName) == "string")
-- First try if the pairing is already there:
local IsThere = false
local IsSuccess, Msg = self:ExecuteStatement(
"SELECT * FROM Allowances WHERE AreaID = ? AND FriendName = ?",
{
a_Area.ID,
a_PlayerName
},
function (a_Values)
IsThere = true