finish migration to in-memory structs

This commit is contained in:
Sky Johnson 2025-08-14 11:22:42 -05:00
parent 21acb38157
commit 869464944a
26 changed files with 3166 additions and 4584 deletions

4
.gitignore vendored
View File

@ -1,6 +1,4 @@
# Dragon Knight test/build files
/dk
/dk.db
/dk.db-*
/sessions.json
/tmp
/data/users.json

1
data/babble.json Normal file
View File

@ -0,0 +1 @@
[]

8
data/control.json Normal file
View File

@ -0,0 +1,8 @@
{
"world_size": 200,
"open": 1,
"admin_email": "",
"class_1_name": "Mage",
"class_2_name": "Warrior",
"class_3_name": "Paladin"
}

View File

@ -1,226 +1,226 @@
[
{
"id": 1,
"name": "Life Pebble",
"level": 1,
"type": 1,
"att": "maxhp,10"
},
{
"id": 2,
"name": "Life Stone",
"level": 10,
"type": 1,
"att": "maxhp,25"
},
{
"id": 3,
"name": "Life Rock",
"level": 25,
"type": 1,
"att": "maxhp,50"
},
{
"id": 4,
"name": "Magic Pebble",
"level": 1,
"type": 1,
"att": "maxmp,10"
},
{
"id": 5,
"name": "Magic Stone",
"level": 10,
"type": 1,
"att": "maxmp,25"
},
{
"id": 6,
"name": "Magic Rock",
"level": 25,
"type": 1,
"att": "maxmp,50"
},
{
"id": 7,
"name": "Dragon's Scale",
"level": 10,
"type": 1,
"att": "defensepower,25"
},
{
"id": 8,
"name": "Dragon's Plate",
"level": 30,
"type": 1,
"att": "defensepower,50"
},
{
"id": 9,
"name": "Dragon's Claw",
"level": 10,
"type": 1,
"att": "attackpower,25"
},
{
"id": 10,
"name": "Dragon's Tooth",
"level": 30,
"type": 1,
"att": "attackpower,50"
},
{
"id": 11,
"name": "Dragon's Tear",
"level": 35,
"type": 1,
"att": "strength,50"
},
{
"id": 12,
"name": "Dragon's Wing",
"level": 35,
"type": 1,
"att": "dexterity,50"
},
{
"id": 13,
"name": "Demon's Sin",
"level": 35,
"type": 1,
"att": "maxhp,-50,strength,50"
},
{
"id": 14,
"name": "Demon's Fall",
"level": 35,
"type": 1,
"att": "maxmp,-50,strength,50"
},
{
"id": 15,
"name": "Demon's Lie",
"level": 45,
"type": 1,
"att": "maxhp,-100,strength,100"
},
{
"id": 16,
"name": "Demon's Hate",
"level": 45,
"type": 1,
"att": "maxmp,-100,strength,100"
},
{
"id": 17,
"name": "Angel's Joy",
"level": 25,
"type": 1,
"att": "maxhp,25,strength,25"
},
{
"id": 18,
"name": "Angel's Rise",
"level": 30,
"type": 1,
"att": "maxhp,50,strength,50"
},
{
"id": 19,
"name": "Angel's Truth",
"level": 35,
"type": 1,
"att": "maxhp,75,strength,75"
},
{
"id": 20,
"name": "Angel's Love",
"level": 40,
"type": 1,
"att": "maxhp,100,strength,100"
},
{
"id": 21,
"name": "Seraph's Joy",
"level": 25,
"type": 1,
"att": "maxmp,25,dexterity,25"
},
{
"id": 22,
"name": "Seraph's Rise",
"level": 30,
"type": 1,
"att": "maxmp,50,dexterity,50"
},
{
"id": 23,
"name": "Seraph's Truth",
"level": 35,
"type": 1,
"att": "maxmp,75,dexterity,75"
},
{
"id": 24,
"name": "Seraph's Love",
"level": 40,
"type": 1,
"att": "maxmp,100,dexterity,100"
},
{
"id": 25,
"name": "Ruby",
"level": 50,
"type": 1,
"att": "maxhp,150"
},
{
"id": 26,
"name": "Pearl",
"level": 50,
"type": 1,
"att": "maxmp,150"
},
{
"id": 27,
"name": "Emerald",
"level": 50,
"type": 1,
"att": "strength,150"
},
{
"id": 28,
"name": "Topaz",
"level": 50,
"type": 1,
"att": "dexterity,150"
},
{
"id": 29,
"name": "Obsidian",
"level": 50,
"type": 1,
"att": "attackpower,150"
},
{
"id": 30,
"name": "Diamond",
"level": 50,
"type": 1,
"att": "defensepower,150"
},
{
"id": 31,
"name": "Memory Drop",
"level": 5,
"type": 1,
"att": "expbonus,10"
},
{
"id": 32,
"name": "Fortune Drop",
"level": 5,
"type": 1,
"att": "goldbonus,10"
}
{
"id": 5,
"name": "Magic Stone",
"level": 10,
"type": 1,
"att": "maxmp,25"
},
{
"id": 27,
"name": "Emerald",
"level": 50,
"type": 1,
"att": "strength,150"
},
{
"id": 30,
"name": "Diamond",
"level": 50,
"type": 1,
"att": "defensepower,150"
},
{
"id": 24,
"name": "Seraph's Love",
"level": 40,
"type": 1,
"att": "maxmp,100,dexterity,100"
},
{
"id": 13,
"name": "Demon's Sin",
"level": 35,
"type": 1,
"att": "maxhp,-50,strength,50"
},
{
"id": 4,
"name": "Magic Pebble",
"level": 1,
"type": 1,
"att": "maxmp,10"
},
{
"id": 11,
"name": "Dragon's Tear",
"level": 35,
"type": 1,
"att": "strength,50"
},
{
"id": 9,
"name": "Dragon's Claw",
"level": 10,
"type": 1,
"att": "attackpower,25"
},
{
"id": 20,
"name": "Angel's Love",
"level": 40,
"type": 1,
"att": "maxhp,100,strength,100"
},
{
"id": 2,
"name": "Life Stone",
"level": 10,
"type": 1,
"att": "maxhp,25"
},
{
"id": 21,
"name": "Seraph's Joy",
"level": 25,
"type": 1,
"att": "maxmp,25,dexterity,25"
},
{
"id": 10,
"name": "Dragon's Tooth",
"level": 30,
"type": 1,
"att": "attackpower,50"
},
{
"id": 16,
"name": "Demon's Hate",
"level": 45,
"type": 1,
"att": "maxmp,-100,strength,100"
},
{
"id": 26,
"name": "Pearl",
"level": 50,
"type": 1,
"att": "maxmp,150"
},
{
"id": 31,
"name": "Memory Drop",
"level": 5,
"type": 1,
"att": "expbonus,10"
},
{
"id": 14,
"name": "Demon's Fall",
"level": 35,
"type": 1,
"att": "maxmp,-50,strength,50"
},
{
"id": 28,
"name": "Topaz",
"level": 50,
"type": 1,
"att": "dexterity,150"
},
{
"id": 22,
"name": "Seraph's Rise",
"level": 30,
"type": 1,
"att": "maxmp,50,dexterity,50"
},
{
"id": 8,
"name": "Dragon's Plate",
"level": 30,
"type": 1,
"att": "defensepower,50"
},
{
"id": 3,
"name": "Life Rock",
"level": 25,
"type": 1,
"att": "maxhp,50"
},
{
"id": 17,
"name": "Angel's Joy",
"level": 25,
"type": 1,
"att": "maxhp,25,strength,25"
},
{
"id": 19,
"name": "Angel's Truth",
"level": 35,
"type": 1,
"att": "maxhp,75,strength,75"
},
{
"id": 6,
"name": "Magic Rock",
"level": 25,
"type": 1,
"att": "maxmp,50"
},
{
"id": 15,
"name": "Demon's Lie",
"level": 45,
"type": 1,
"att": "maxhp,-100,strength,100"
},
{
"id": 7,
"name": "Dragon's Scale",
"level": 10,
"type": 1,
"att": "defensepower,25"
},
{
"id": 29,
"name": "Obsidian",
"level": 50,
"type": 1,
"att": "attackpower,150"
},
{
"id": 25,
"name": "Ruby",
"level": 50,
"type": 1,
"att": "maxhp,150"
},
{
"id": 18,
"name": "Angel's Rise",
"level": 30,
"type": 1,
"att": "maxhp,50,strength,50"
},
{
"id": 23,
"name": "Seraph's Truth",
"level": 35,
"type": 1,
"att": "maxmp,75,dexterity,75"
},
{
"id": 12,
"name": "Dragon's Wing",
"level": 35,
"type": 1,
"att": "dexterity,50"
},
{
"id": 32,
"name": "Fortune Drop",
"level": 5,
"type": 1,
"att": "goldbonus,10"
},
{
"id": 1,
"name": "Life Pebble",
"level": 1,
"type": 1,
"att": "maxhp,10"
}
]

1
data/forum.json Normal file
View File

@ -0,0 +1 @@
[]

View File

@ -1,266 +1,266 @@
[
{
"id": 1,
"type": 1,
"name": "Stick",
"value": 10,
"att": 2,
"special": ""
},
{
"id": 2,
"type": 1,
"name": "Branch",
"value": 30,
"att": 4,
"special": ""
},
{
"id": 3,
"type": 1,
"name": "Club",
"value": 40,
"att": 5,
"special": ""
},
{
"id": 4,
"type": 1,
"name": "Dagger",
"value": 90,
"att": 8,
"special": ""
},
{
"id": 5,
"type": 1,
"name": "Hatchet",
"value": 150,
"att": 12,
"special": ""
},
{
"id": 6,
"type": 1,
"name": "Axe",
"value": 200,
"att": 16,
"special": ""
},
{
"id": 7,
"type": 1,
"name": "Brand",
"value": 300,
"att": 25,
"special": ""
},
{
"id": 8,
"type": 1,
"name": "Poleaxe",
"value": 500,
"att": 35,
"special": ""
},
{
"id": 9,
"type": 1,
"name": "Broadsword",
"value": 800,
"att": 45,
"special": ""
},
{
"id": 10,
"type": 1,
"name": "Battle Axe",
"value": 1200,
"att": 50,
"special": ""
},
{
"id": 11,
"type": 1,
"name": "Claymore",
"value": 2000,
"att": 60,
"special": ""
},
{
"id": 12,
"type": 1,
"name": "Dark Axe",
"value": 3000,
"att": 100,
"special": "expbonus,-5"
},
{
"id": 13,
"type": 1,
"name": "Dark Sword",
"value": 4500,
"att": 125,
"special": "expbonus,-10"
},
{
"id": 14,
"type": 1,
"name": "Bright Sword",
"value": 6000,
"att": 100,
"special": "expbonus,10"
},
{
"id": 15,
"type": 1,
"name": "Magic Sword",
"value": 10000,
"att": 150,
"special": "maxmp,50"
},
{
"id": 16,
"type": 1,
"name": "Destiny Blade",
"value": 50000,
"att": 250,
"special": "strength,50"
},
{
"id": 17,
"type": 2,
"name": "Skivvies",
"value": 25,
"att": 2,
"special": "goldbonus,10"
},
{
"id": 18,
"type": 2,
"name": "Clothes",
"value": 50,
"att": 5,
"special": ""
},
{
"id": 19,
"type": 2,
"name": "Leather Armor",
"value": 75,
"att": 10,
"special": ""
},
{
"id": 20,
"type": 2,
"name": "Hard Leather Armor",
"value": 150,
"att": 25,
"special": ""
},
{
"id": 21,
"type": 2,
"name": "Chain Mail",
"value": 300,
"att": 30,
"special": ""
},
{
"id": 22,
"type": 2,
"name": "Bronze Plate",
"value": 900,
"att": 50,
"special": ""
},
{
"id": 23,
"type": 2,
"name": "Iron Plate",
"value": 2000,
"att": 100,
"special": ""
},
{
"id": 24,
"type": 2,
"name": "Magic Armor",
"value": 4000,
"att": 125,
"special": "maxmp,50"
},
{
"id": 25,
"type": 2,
"name": "Dark Armor",
"value": 5000,
"att": 150,
"special": "expbonus,-10"
},
{
"id": 26,
"type": 2,
"name": "Bright Armor",
"value": 10000,
"att": 175,
"special": "expbonus,10"
},
{
"id": 27,
"type": 2,
"name": "Destiny Raiment",
"value": 50000,
"att": 200,
"special": "dexterity,50"
},
{
"id": 28,
"type": 3,
"name": "Reed Shield",
"value": 50,
"att": 2,
"special": ""
},
{
"id": 29,
"type": 3,
"name": "Buckler",
"value": 100,
"att": 4,
"special": ""
},
{
"id": 30,
"type": 3,
"name": "Small Shield",
"value": 500,
"att": 10,
"special": ""
},
{
"id": 31,
"type": 3,
"name": "Large Shield",
"value": 2500,
"att": 30,
"special": ""
},
{
"id": 32,
"type": 3,
"name": "Silver Shield",
"value": 10000,
"att": 60,
"special": ""
},
{
"id": 33,
"type": 3,
"name": "Destiny Aegis",
"value": 25000,
"att": 100,
"special": "maxhp,50"
}
{
"id": 17,
"type": 2,
"name": "Skivvies",
"value": 25,
"att": 2,
"special": "goldbonus,10"
},
{
"id": 11,
"type": 1,
"name": "Claymore",
"value": 2000,
"att": 60,
"special": ""
},
{
"id": 28,
"type": 3,
"name": "Reed Shield",
"value": 50,
"att": 2,
"special": ""
},
{
"id": 8,
"type": 1,
"name": "Poleaxe",
"value": 500,
"att": 35,
"special": ""
},
{
"id": 25,
"type": 2,
"name": "Dark Armor",
"value": 5000,
"att": 150,
"special": "expbonus,-10"
},
{
"id": 20,
"type": 2,
"name": "Hard Leather Armor",
"value": 150,
"att": 25,
"special": ""
},
{
"id": 12,
"type": 1,
"name": "Dark Axe",
"value": 3000,
"att": 100,
"special": "expbonus,-5"
},
{
"id": 22,
"type": 2,
"name": "Bronze Plate",
"value": 900,
"att": 50,
"special": ""
},
{
"id": 19,
"type": 2,
"name": "Leather Armor",
"value": 75,
"att": 10,
"special": ""
},
{
"id": 21,
"type": 2,
"name": "Chain Mail",
"value": 300,
"att": 30,
"special": ""
},
{
"id": 26,
"type": 2,
"name": "Bright Armor",
"value": 10000,
"att": 175,
"special": "expbonus,10"
},
{
"id": 23,
"type": 2,
"name": "Iron Plate",
"value": 2000,
"att": 100,
"special": ""
},
{
"id": 4,
"type": 1,
"name": "Dagger",
"value": 90,
"att": 8,
"special": ""
},
{
"id": 7,
"type": 1,
"name": "Brand",
"value": 300,
"att": 25,
"special": ""
},
{
"id": 29,
"type": 3,
"name": "Buckler",
"value": 100,
"att": 4,
"special": ""
},
{
"id": 18,
"type": 2,
"name": "Clothes",
"value": 50,
"att": 5,
"special": ""
},
{
"id": 3,
"type": 1,
"name": "Club",
"value": 40,
"att": 5,
"special": ""
},
{
"id": 32,
"type": 3,
"name": "Silver Shield",
"value": 10000,
"att": 60,
"special": ""
},
{
"id": 24,
"type": 2,
"name": "Magic Armor",
"value": 4000,
"att": 125,
"special": "maxmp,50"
},
{
"id": 16,
"type": 1,
"name": "Destiny Blade",
"value": 50000,
"att": 250,
"special": "strength,50"
},
{
"id": 14,
"type": 1,
"name": "Bright Sword",
"value": 6000,
"att": 100,
"special": "expbonus,10"
},
{
"id": 5,
"type": 1,
"name": "Hatchet",
"value": 150,
"att": 12,
"special": ""
},
{
"id": 1,
"type": 1,
"name": "Stick",
"value": 10,
"att": 2,
"special": ""
},
{
"id": 27,
"type": 2,
"name": "Destiny Raiment",
"value": 50000,
"att": 200,
"special": "dexterity,50"
},
{
"id": 13,
"type": 1,
"name": "Dark Sword",
"value": 4500,
"att": 125,
"special": "expbonus,-10"
},
{
"id": 9,
"type": 1,
"name": "Broadsword",
"value": 800,
"att": 45,
"special": ""
},
{
"id": 30,
"type": 3,
"name": "Small Shield",
"value": 500,
"att": 10,
"special": ""
},
{
"id": 6,
"type": 1,
"name": "Axe",
"value": 200,
"att": 16,
"special": ""
},
{
"id": 33,
"type": 3,
"name": "Destiny Aegis",
"value": 25000,
"att": 100,
"special": "maxhp,50"
},
{
"id": 10,
"type": 1,
"name": "Battle Axe",
"value": 1200,
"att": 50,
"special": ""
},
{
"id": 31,
"type": 3,
"name": "Large Shield",
"value": 2500,
"att": 30,
"special": ""
},
{
"id": 2,
"type": 1,
"name": "Branch",
"value": 30,
"att": 4,
"special": ""
},
{
"id": 15,
"type": 1,
"name": "Magic Sword",
"value": 10000,
"att": 150,
"special": "maxmp,50"
}
]

File diff suppressed because it is too large Load Diff

1
data/news.json Normal file
View File

@ -0,0 +1 @@
[]

View File

@ -1,135 +1,135 @@
[
{
"id": 1,
"name": "Heal",
"mp": 5,
"attribute": 10,
"type": 1
},
{
"id": 2,
"name": "Revive",
"mp": 10,
"attribute": 25,
"type": 1
},
{
"id": 3,
"name": "Life",
"mp": 25,
"attribute": 50,
"type": 1
},
{
"id": 4,
"name": "Breath",
"mp": 50,
"attribute": 100,
"type": 1
},
{
"id": 5,
"name": "Gaia",
"mp": 75,
"attribute": 150,
"type": 1
},
{
"id": 6,
"name": "Hurt",
"mp": 5,
"attribute": 15,
"type": 2
},
{
"id": 7,
"name": "Pain",
"mp": 12,
"attribute": 35,
"type": 2
},
{
"id": 8,
"name": "Maim",
"mp": 25,
"attribute": 70,
"type": 2
},
{
"id": 9,
"name": "Rend",
"mp": 40,
"attribute": 100,
"type": 2
},
{
"id": 10,
"name": "Chaos",
"mp": 50,
"attribute": 130,
"type": 2
},
{
"id": 11,
"name": "Sleep",
"mp": 10,
"attribute": 5,
"type": 3
},
{
"id": 12,
"name": "Dream",
"mp": 30,
"attribute": 9,
"type": 3
},
{
"id": 13,
"name": "Nightmare",
"mp": 60,
"attribute": 13,
"type": 3
},
{
"id": 14,
"name": "Craze",
"mp": 10,
"attribute": 10,
"type": 4
},
{
"id": 15,
"name": "Rage",
"mp": 20,
"attribute": 25,
"type": 4
},
{
"id": 16,
"name": "Fury",
"mp": 30,
"attribute": 50,
"type": 4
},
{
"id": 17,
"name": "Ward",
"mp": 10,
"attribute": 10,
"type": 5
},
{
"id": 18,
"name": "Fend",
"mp": 20,
"attribute": 25,
"type": 5
},
{
"id": 19,
"name": "Barrier",
"mp": 30,
"attribute": 50,
"type": 5
}
{
"id": 5,
"name": "Gaia",
"mp": 75,
"attribute": 150,
"type": 1
},
{
"id": 2,
"name": "Revive",
"mp": 10,
"attribute": 25,
"type": 1
},
{
"id": 3,
"name": "Life",
"mp": 25,
"attribute": 50,
"type": 1
},
{
"id": 8,
"name": "Maim",
"mp": 25,
"attribute": 70,
"type": 2
},
{
"id": 14,
"name": "Craze",
"mp": 10,
"attribute": 10,
"type": 4
},
{
"id": 1,
"name": "Heal",
"mp": 5,
"attribute": 10,
"type": 1
},
{
"id": 15,
"name": "Rage",
"mp": 20,
"attribute": 25,
"type": 4
},
{
"id": 9,
"name": "Rend",
"mp": 40,
"attribute": 100,
"type": 2
},
{
"id": 6,
"name": "Hurt",
"mp": 5,
"attribute": 15,
"type": 2
},
{
"id": 17,
"name": "Ward",
"mp": 10,
"attribute": 10,
"type": 5
},
{
"id": 12,
"name": "Dream",
"mp": 30,
"attribute": 9,
"type": 3
},
{
"id": 18,
"name": "Fend",
"mp": 20,
"attribute": 25,
"type": 5
},
{
"id": 19,
"name": "Barrier",
"mp": 30,
"attribute": 50,
"type": 5
},
{
"id": 13,
"name": "Nightmare",
"mp": 60,
"attribute": 13,
"type": 3
},
{
"id": 11,
"name": "Sleep",
"mp": 10,
"attribute": 5,
"type": 3
},
{
"id": 16,
"name": "Fury",
"mp": 30,
"attribute": 50,
"type": 4
},
{
"id": 10,
"name": "Chaos",
"mp": 50,
"attribute": 130,
"type": 2
},
{
"id": 7,
"name": "Pain",
"mp": 12,
"attribute": 35,
"type": 2
},
{
"id": 4,
"name": "Breath",
"mp": 50,
"attribute": 100,
"type": 1
}
]

View File

@ -1,82 +1,82 @@
[
{
"id": 1,
"name": "Midworld",
"x": 0,
"y": 0,
"inn_cost": 5,
"map_cost": 0,
"tp_cost": 0,
"shop_list": "1,2,3,17,18,19,28,29"
},
{
"id": 2,
"name": "Roma",
"x": 30,
"y": 30,
"inn_cost": 10,
"map_cost": 25,
"tp_cost": 5,
"shop_list": "2,3,4,18,19,29"
},
{
"id": 3,
"name": "Bris",
"x": 70,
"y": -70,
"inn_cost": 25,
"map_cost": 50,
"tp_cost": 15,
"shop_list": "2,3,4,5,18,19,20,29.30"
},
{
"id": 4,
"name": "Kalle",
"x": -100,
"y": 100,
"inn_cost": 40,
"map_cost": 100,
"tp_cost": 30,
"shop_list": "5,6,8,10,12,21,22,23,29,30"
},
{
"id": 5,
"name": "Narcissa",
"x": -130,
"y": -130,
"inn_cost": 60,
"map_cost": 500,
"tp_cost": 50,
"shop_list": "4,7,9,11,13,21,22,23,29,30,31"
},
{
"id": 6,
"name": "Hambry",
"x": 170,
"y": 170,
"inn_cost": 90,
"map_cost": 1000,
"tp_cost": 80,
"shop_list": "10,11,12,13,14,23,24,30,31"
},
{
"id": 7,
"name": "Gilead",
"x": 200,
"y": -200,
"inn_cost": 100,
"map_cost": 3000,
"tp_cost": 110,
"shop_list": "12,13,14,15,24,25,26,32"
},
{
"id": 8,
"name": "Endworld",
"x": -250,
"y": -250,
"inn_cost": 125,
"map_cost": 9000,
"tp_cost": 160,
"shop_list": "16,27,33"
}
{
"id": 4,
"name": "Kalle",
"x": -100,
"y": 100,
"inn_cost": 40,
"map_cost": 100,
"tp_cost": 30,
"shop_list": "5,6,8,10,12,21,22,23,29,30"
},
{
"id": 5,
"name": "Narcissa",
"x": -130,
"y": -130,
"inn_cost": 60,
"map_cost": 500,
"tp_cost": 50,
"shop_list": "4,7,9,11,13,21,22,23,29,30,31"
},
{
"id": 6,
"name": "Hambry",
"x": 170,
"y": 170,
"inn_cost": 90,
"map_cost": 1000,
"tp_cost": 80,
"shop_list": "10,11,12,13,14,23,24,30,31"
},
{
"id": 7,
"name": "Gilead",
"x": 200,
"y": -200,
"inn_cost": 100,
"map_cost": 3000,
"tp_cost": 110,
"shop_list": "12,13,14,15,24,25,26,32"
},
{
"id": 8,
"name": "Endworld",
"x": -250,
"y": -250,
"inn_cost": 125,
"map_cost": 9000,
"tp_cost": 160,
"shop_list": "16,27,33"
},
{
"id": 1,
"name": "Midworld",
"x": 0,
"y": 0,
"inn_cost": 5,
"map_cost": 0,
"tp_cost": 0,
"shop_list": "1,2,3,17,18,19,28,29"
},
{
"id": 2,
"name": "Roma",
"x": 30,
"y": 30,
"inn_cost": 10,
"map_cost": 25,
"tp_cost": 5,
"shop_list": "2,3,4,18,19,29"
},
{
"id": 3,
"name": "Bris",
"x": 70,
"y": -70,
"inn_cost": 25,
"map_cost": 50,
"tp_cost": 15,
"shop_list": "2,3,4,5,18,19,20,29.30"
}
]

View File

@ -32,10 +32,7 @@ func (d Direction) String() string {
}
func Move(user *users.User, dir Direction) (string, int, int, error) {
control, err := control.Get()
if err != nil {
panic("Move: CONTROL ROW SHOULD EXIST")
}
control := control.Get()
newX, newY := user.X, user.Y
switch dir {

View File

@ -31,26 +31,26 @@ func UserEquipItem(user *users.User, item *items.Item) {
if oldItem != nil {
switch oldItem.Type {
case items.TypeWeapon:
user.Set("Attack", user.Attack-oldItem.Att)
user.Attack -= oldItem.Att
case items.TypeArmor:
user.Set("Defense", user.Defense-oldItem.Att)
user.Defense -= oldItem.Att
case items.TypeShield:
user.Set("Defense", user.Defense-oldItem.Att)
user.Defense -= oldItem.Att
}
}
switch item.Type {
case items.TypeWeapon:
user.Set("Attack", user.Attack+item.Att)
user.Set("WeaponID", item.ID)
user.Set("WeaponName", item.Name)
user.Attack += item.Att
user.WeaponID = item.ID
user.WeaponName = item.Name
case items.TypeArmor:
user.Set("Defense", user.Defense+item.Att)
user.Set("ArmorID", item.ID)
user.Set("ArmorName", item.Name)
user.Defense += item.Att
user.ArmorID = item.ID
user.ArmorName = item.Name
case items.TypeShield:
user.Set("Defense", user.Defense+item.Att)
user.Set("ShieldID", item.ID)
user.Set("ShieldName", item.Name)
user.Defense += item.Att
user.ShieldID = item.ID
user.ShieldName = item.Name
}
}

View File

@ -5,7 +5,6 @@ import (
"fmt"
"sort"
"strings"
"sync"
"time"
)
@ -18,14 +17,11 @@ type Babble struct {
}
func (b *Babble) Save() error {
babbleStore := GetStore()
babbleStore.UpdateBabble(b)
return nil
return GetStore().UpdateWithRebuild(b.ID, b)
}
func (b *Babble) Delete() error {
babbleStore := GetStore()
babbleStore.RemoveBabble(b.ID)
GetStore().RemoveWithRebuild(b.ID)
return nil
}
@ -52,329 +48,156 @@ func (b *Babble) Validate() error {
return nil
}
// BabbleStore provides in-memory storage with O(1) lookups and babble-specific indices
// BabbleStore with enhanced BaseStore
type BabbleStore struct {
*store.BaseStore[Babble] // Embedded generic store
byAuthor map[string][]int // Author (lowercase) -> []ID
allByPosted []int // All IDs sorted by posted DESC, id DESC
mu sync.RWMutex // Protects indices
*store.BaseStore[Babble]
}
// Global in-memory store
var babbleStore *BabbleStore
var storeOnce sync.Once
// Global store with singleton pattern
var GetStore = store.NewSingleton(func() *BabbleStore {
bs := &BabbleStore{BaseStore: store.NewBaseStore[Babble]()}
// Initialize the in-memory store
func initStore() {
babbleStore = &BabbleStore{
BaseStore: store.NewBaseStore[Babble](),
byAuthor: make(map[string][]int),
allByPosted: make([]int, 0),
}
// Register indices
bs.RegisterIndex("byAuthor", store.BuildStringGroupIndex(func(b *Babble) string {
return strings.ToLower(b.Author)
}))
bs.RegisterIndex("allByPosted", store.BuildSortedListIndex(func(a, b *Babble) bool {
if a.Posted != b.Posted {
return a.Posted > b.Posted // DESC
}
return a.ID > b.ID // DESC
}))
return bs
})
// Enhanced CRUD operations
func (bs *BabbleStore) AddBabble(babble *Babble) error {
return bs.AddWithRebuild(babble.ID, babble)
}
// GetStore returns the global babble store
func GetStore() *BabbleStore {
storeOnce.Do(initStore)
return babbleStore
}
// AddBabble adds a babble message to the in-memory store and updates all indices
func (bs *BabbleStore) AddBabble(babble *Babble) {
bs.mu.Lock()
defer bs.mu.Unlock()
// Validate babble
if err := babble.Validate(); err != nil {
return
}
// Add to base store
bs.Add(babble.ID, babble)
// Rebuild indices
bs.rebuildIndicesUnsafe()
}
// RemoveBabble removes a babble message from the store and updates indices
func (bs *BabbleStore) RemoveBabble(id int) {
bs.mu.Lock()
defer bs.mu.Unlock()
// Remove from base store
bs.Remove(id)
// Rebuild indices
bs.rebuildIndicesUnsafe()
bs.RemoveWithRebuild(id)
}
// UpdateBabble updates a babble message efficiently
func (bs *BabbleStore) UpdateBabble(babble *Babble) {
bs.mu.Lock()
defer bs.mu.Unlock()
// Validate babble
if err := babble.Validate(); err != nil {
return
}
// Update base store
bs.Add(babble.ID, babble)
// Rebuild indices
bs.rebuildIndicesUnsafe()
func (bs *BabbleStore) UpdateBabble(babble *Babble) error {
return bs.UpdateWithRebuild(babble.ID, babble)
}
// LoadData loads babble data from JSON file, or starts with empty store
// Data persistence
func LoadData(dataPath string) error {
bs := GetStore()
// Load from base store, which handles JSON loading
if err := bs.BaseStore.LoadData(dataPath); err != nil {
return err
}
// Rebuild indices from loaded data
bs.rebuildIndices()
return nil
return bs.BaseStore.LoadData(dataPath)
}
// SaveData saves babble data to JSON file
func SaveData(dataPath string) error {
bs := GetStore()
return bs.BaseStore.SaveData(dataPath)
}
// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock)
func (bs *BabbleStore) rebuildIndicesUnsafe() {
// Clear indices
bs.byAuthor = make(map[string][]int)
bs.allByPosted = make([]int, 0)
// Collect all babbles and build indices
allBabbles := bs.GetAll()
for id, babble := range allBabbles {
// Author index (case-insensitive)
authorKey := strings.ToLower(babble.Author)
bs.byAuthor[authorKey] = append(bs.byAuthor[authorKey], id)
// All IDs
bs.allByPosted = append(bs.allByPosted, id)
}
// Sort allByPosted by posted DESC, then ID DESC
sort.Slice(bs.allByPosted, func(i, j int) bool {
babbleI, _ := bs.GetByID(bs.allByPosted[i])
babbleJ, _ := bs.GetByID(bs.allByPosted[j])
if babbleI.Posted != babbleJ.Posted {
return babbleI.Posted > babbleJ.Posted // DESC
}
return bs.allByPosted[i] > bs.allByPosted[j] // DESC
})
// Sort author indices by posted DESC, then ID DESC
for author := range bs.byAuthor {
sort.Slice(bs.byAuthor[author], func(i, j int) bool {
babbleI, _ := bs.GetByID(bs.byAuthor[author][i])
babbleJ, _ := bs.GetByID(bs.byAuthor[author][j])
if babbleI.Posted != babbleJ.Posted {
return babbleI.Posted > babbleJ.Posted // DESC
}
return bs.byAuthor[author][i] > bs.byAuthor[author][j] // DESC
})
}
}
// rebuildIndices rebuilds all babble-specific indices from base store data
func (bs *BabbleStore) rebuildIndices() {
bs.mu.Lock()
defer bs.mu.Unlock()
bs.rebuildIndicesUnsafe()
}
// Retrieves a babble message by ID
// Query functions using enhanced store
func Find(id int) (*Babble, error) {
bs := GetStore()
babble, exists := bs.GetByID(id)
babble, exists := bs.Find(id)
if !exists {
return nil, fmt.Errorf("babble with ID %d not found", id)
}
return babble, nil
}
// Retrieves all babble messages ordered by posted time (newest first)
func All() ([]*Babble, error) {
bs := GetStore()
bs.mu.RLock()
defer bs.mu.RUnlock()
result := make([]*Babble, 0, len(bs.allByPosted))
for _, id := range bs.allByPosted {
if babble, exists := bs.GetByID(id); exists {
result = append(result, babble)
}
}
return result, nil
return bs.AllSorted("allByPosted"), nil
}
// Retrieves babble messages by a specific author
func ByAuthor(author string) ([]*Babble, error) {
bs := GetStore()
bs.mu.RLock()
defer bs.mu.RUnlock()
messages := bs.GroupByIndex("byAuthor", strings.ToLower(author))
ids, exists := bs.byAuthor[strings.ToLower(author)]
if !exists {
return []*Babble{}, nil
}
result := make([]*Babble, 0, len(ids))
for _, id := range ids {
if babble, exists := bs.GetByID(id); exists {
result = append(result, babble)
// Sort by posted DESC, then ID DESC
sort.Slice(messages, func(i, j int) bool {
if messages[i].Posted != messages[j].Posted {
return messages[i].Posted > messages[j].Posted // DESC
}
}
return result, nil
return messages[i].ID > messages[j].ID // DESC
})
return messages, nil
}
// Retrieves the most recent babble messages (limited by count)
func Recent(limit int) ([]*Babble, error) {
bs := GetStore()
bs.mu.RLock()
defer bs.mu.RUnlock()
if limit > len(bs.allByPosted) {
limit = len(bs.allByPosted)
all := bs.AllSorted("allByPosted")
if limit > len(all) {
limit = len(all)
}
result := make([]*Babble, 0, limit)
for i := 0; i < limit; i++ {
if babble, exists := bs.GetByID(bs.allByPosted[i]); exists {
result = append(result, babble)
}
}
return result, nil
return all[:limit], nil
}
// Retrieves babble messages since a specific timestamp
func Since(since int64) ([]*Babble, error) {
bs := GetStore()
bs.mu.RLock()
defer bs.mu.RUnlock()
var result []*Babble
for _, id := range bs.allByPosted {
if babble, exists := bs.GetByID(id); exists && babble.Posted >= since {
result = append(result, babble)
}
}
return result, nil
return bs.FilterByIndex("allByPosted", func(b *Babble) bool {
return b.Posted >= since
}), nil
}
// Retrieves babble messages between two timestamps (inclusive)
func Between(start, end int64) ([]*Babble, error) {
bs := GetStore()
bs.mu.RLock()
defer bs.mu.RUnlock()
var result []*Babble
for _, id := range bs.allByPosted {
if babble, exists := bs.GetByID(id); exists && babble.Posted >= start && babble.Posted <= end {
result = append(result, babble)
}
}
return result, nil
return bs.FilterByIndex("allByPosted", func(b *Babble) bool {
return b.Posted >= start && b.Posted <= end
}), nil
}
// Retrieves babble messages containing the search term (case-insensitive)
func Search(term string) ([]*Babble, error) {
bs := GetStore()
bs.mu.RLock()
defer bs.mu.RUnlock()
var result []*Babble
lowerTerm := strings.ToLower(term)
for _, id := range bs.allByPosted {
if babble, exists := bs.GetByID(id); exists {
if strings.Contains(strings.ToLower(babble.Babble), lowerTerm) {
result = append(result, babble)
}
}
}
return result, nil
return bs.FilterByIndex("allByPosted", func(b *Babble) bool {
return strings.Contains(strings.ToLower(b.Babble), lowerTerm)
}), nil
}
// Retrieves recent messages from a specific author
func RecentByAuthor(author string, limit int) ([]*Babble, error) {
bs := GetStore()
bs.mu.RLock()
defer bs.mu.RUnlock()
ids, exists := bs.byAuthor[strings.ToLower(author)]
if !exists {
return []*Babble{}, nil
messages, err := ByAuthor(author)
if err != nil {
return nil, err
}
if limit > len(ids) {
limit = len(ids)
if limit > len(messages) {
limit = len(messages)
}
result := make([]*Babble, 0, limit)
for i := 0; i < limit; i++ {
if babble, exists := bs.GetByID(ids[i]); exists {
result = append(result, babble)
}
}
return result, nil
return messages[:limit], nil
}
// Saves a new babble to the in-memory store and sets the ID
// Insert with ID assignment
func (b *Babble) Insert() error {
bs := GetStore()
// Validate before insertion
if err := b.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Assign new ID if not set
if b.ID == 0 {
b.ID = bs.GetNextID()
}
// Add to store
bs.AddBabble(b)
return nil
return bs.AddBabble(b)
}
// Returns the posted timestamp as a time.Time
// Helper methods
func (b *Babble) PostedTime() time.Time {
return time.Unix(b.Posted, 0)
}
// Sets the posted timestamp from a time.Time
func (b *Babble) SetPostedTime(t time.Time) {
b.Posted = t.Unix()
}
// Returns true if the babble message was posted within the last hour
func (b *Babble) IsRecent() bool {
return time.Since(b.PostedTime()) < time.Hour
}
// Returns how long ago the babble message was posted
func (b *Babble) Age() time.Duration {
return time.Since(b.PostedTime())
}
// Returns true if the given username is the author of this babble message
func (b *Babble) IsAuthor(username string) bool {
return strings.EqualFold(b.Author, username)
}
// Returns a truncated version of the babble for previews
func (b *Babble) Preview(maxLength int) string {
if len(b.Babble) <= maxLength {
return b.Babble
@ -387,7 +210,6 @@ func (b *Babble) Preview(maxLength int) string {
return b.Babble[:maxLength-3] + "..."
}
// Returns the number of words in the babble message
func (b *Babble) WordCount() int {
if b.Babble == "" {
return 0
@ -415,27 +237,22 @@ func (b *Babble) WordCount() int {
return words
}
// Returns the character length of the babble message
func (b *Babble) Length() int {
return len(b.Babble)
}
// Returns true if the babble message contains the given term (case-insensitive)
func (b *Babble) Contains(term string) bool {
return strings.Contains(strings.ToLower(b.Babble), strings.ToLower(term))
}
// Returns true if the babble message is empty or whitespace-only
func (b *Babble) IsEmpty() bool {
return strings.TrimSpace(b.Babble) == ""
}
// Returns true if the message exceeds the typical chat length
func (b *Babble) IsLongMessage(threshold int) bool {
return b.Length() > threshold
}
// Returns a slice of usernames mentioned in the message (starting with @)
func (b *Babble) GetMentions() []string {
words := strings.Fields(b.Babble)
var mentions []string
@ -453,7 +270,6 @@ func (b *Babble) GetMentions() []string {
return mentions
}
// Returns true if the message mentions the given username
func (b *Babble) HasMention(username string) bool {
mentions := b.GetMentions()
for _, mention := range mentions {

View File

@ -1,14 +1,24 @@
package control
import (
"dk/internal/store"
"encoding/json"
"fmt"
"os"
"sync"
)
var (
global *Control
configPath string
mu sync.RWMutex
)
func init() {
global = New()
}
// Control represents the game control settings
type Control struct {
ID int `json:"id"`
WorldSize int `json:"world_size"`
Open int `json:"open"`
AdminEmail string `json:"admin_email"`
@ -17,220 +27,112 @@ type Control struct {
Class3Name string `json:"class_3_name"`
}
func (c *Control) Save() error {
controlStore := GetStore()
controlStore.UpdateControl(c)
return nil
}
func (c *Control) Delete() error {
controlStore := GetStore()
controlStore.RemoveControl(c.ID)
return nil
}
// Creates a new Control with sensible defaults
// New creates a new Control with sensible defaults
func New() *Control {
return &Control{
WorldSize: 200, // Default world size
Open: 1, // Default open for registration
AdminEmail: "", // No admin email by default
Class1Name: "Mage", // Default class names
WorldSize: 200,
Open: 1,
AdminEmail: "",
Class1Name: "Mage",
Class2Name: "Warrior",
Class3Name: "Paladin",
}
}
// Validate checks if control has valid values
// Load loads control settings from JSON file into global instance
func Load(filename string) error {
mu.Lock()
configPath = filename
mu.Unlock()
data, err := os.ReadFile(filename)
if err != nil {
if os.IsNotExist(err) {
return nil // Keep defaults
}
return fmt.Errorf("failed to read config file: %w", err)
}
if len(data) == 0 {
return nil
}
control := &Control{}
if err := json.Unmarshal(data, control); err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
// Apply defaults for any missing fields
defaults := New()
if control.WorldSize == 0 {
control.WorldSize = defaults.WorldSize
}
if control.Class1Name == "" {
control.Class1Name = defaults.Class1Name
}
if control.Class2Name == "" {
control.Class2Name = defaults.Class2Name
}
if control.Class3Name == "" {
control.Class3Name = defaults.Class3Name
}
mu.Lock()
global = control
mu.Unlock()
return nil
}
// Save saves global control settings to the loaded path
func Save() error {
mu.RLock()
defer mu.RUnlock()
if configPath == "" {
return fmt.Errorf("no config path set, call Load() first")
}
data, err := json.MarshalIndent(global, "", "\t")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(configPath, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
// Get returns the global control instance (thread-safe)
func Get() *Control {
mu.RLock()
defer mu.RUnlock()
return global
}
// Set updates the global control instance (thread-safe)
func Set(control *Control) {
mu.Lock()
defer mu.Unlock()
global = control
}
func (c *Control) Validate() error {
if c.WorldSize <= 0 || c.WorldSize > 10000 {
return fmt.Errorf("control WorldSize must be between 1 and 10000")
return fmt.Errorf("WorldSize must be between 1 and 10000")
}
if c.Open != 0 && c.Open != 1 {
return fmt.Errorf("control Open must be 0 or 1")
return fmt.Errorf("Open must be 0 or 1")
}
return nil
}
// ControlStore provides in-memory storage for control settings
type ControlStore struct {
*store.BaseStore[Control] // Embedded generic store
allByID []int // All IDs sorted by ID
mu sync.RWMutex // Protects indices
}
// Global in-memory store
var controlStore *ControlStore
var storeOnce sync.Once
// Initialize the in-memory store
func initStore() {
controlStore = &ControlStore{
BaseStore: store.NewBaseStore[Control](),
allByID: make([]int, 0),
}
}
// GetStore returns the global control store
func GetStore() *ControlStore {
storeOnce.Do(initStore)
return controlStore
}
// AddControl adds a control to the in-memory store and updates all indices
func (cs *ControlStore) AddControl(control *Control) {
cs.mu.Lock()
defer cs.mu.Unlock()
// Validate control
if err := control.Validate(); err != nil {
return
}
// Add to base store
cs.Add(control.ID, control)
// Rebuild indices
cs.rebuildIndicesUnsafe()
}
// RemoveControl removes a control from the store and updates indices
func (cs *ControlStore) RemoveControl(id int) {
cs.mu.Lock()
defer cs.mu.Unlock()
// Remove from base store
cs.Remove(id)
// Rebuild indices
cs.rebuildIndicesUnsafe()
}
// UpdateControl updates a control efficiently
func (cs *ControlStore) UpdateControl(control *Control) {
cs.mu.Lock()
defer cs.mu.Unlock()
// Validate control
if err := control.Validate(); err != nil {
return
}
// Update base store
cs.Add(control.ID, control)
// Rebuild indices
cs.rebuildIndicesUnsafe()
}
// LoadData loads control data from JSON file, or starts with empty store
func LoadData(dataPath string) error {
cs := GetStore()
// Load from base store, which handles JSON loading
if err := cs.BaseStore.LoadData(dataPath); err != nil {
return err
}
// Rebuild indices from loaded data
cs.rebuildIndices()
return nil
}
// SaveData saves control data to JSON file
func SaveData(dataPath string) error {
cs := GetStore()
return cs.BaseStore.SaveData(dataPath)
}
// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock)
func (cs *ControlStore) rebuildIndicesUnsafe() {
// Clear indices
cs.allByID = make([]int, 0)
// Collect all controls
allControls := cs.GetAll()
for id := range allControls {
cs.allByID = append(cs.allByID, id)
}
// Sort by ID (though typically only one control record exists)
for i := 0; i < len(cs.allByID); i++ {
for j := i + 1; j < len(cs.allByID); j++ {
if cs.allByID[i] > cs.allByID[j] {
cs.allByID[i], cs.allByID[j] = cs.allByID[j], cs.allByID[i]
}
}
}
}
// rebuildIndices rebuilds all control-specific indices from base store data
func (cs *ControlStore) rebuildIndices() {
cs.mu.Lock()
defer cs.mu.Unlock()
cs.rebuildIndicesUnsafe()
}
// Retrieves the control record by ID (typically only ID 1 exists)
func Find(id int) (*Control, error) {
cs := GetStore()
control, exists := cs.GetByID(id)
if !exists {
return nil, fmt.Errorf("control with ID %d not found", id)
}
return control, nil
}
// Retrieves the main control record (ID 1)
func Get() (*Control, error) {
return Find(1)
}
// Saves a new control to the in-memory store and sets the ID
func (c *Control) Insert() error {
cs := GetStore()
// Validate before insertion
if err := c.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Assign new ID if not set
if c.ID == 0 {
c.ID = cs.GetNextID()
}
// Add to store
cs.AddControl(c)
return nil
}
// Returns true if the game world is open for new players
// IsOpen returns true if the game world is open for new players
func (c *Control) IsOpen() bool {
return c.Open == 1
}
// Sets whether the game world is open for new players
func (c *Control) SetOpen(open bool) {
if open {
c.Open = 1
} else {
c.Open = 0
}
}
// Closes the game world to new players
func (c *Control) Close() {
c.Open = 0
}
// Opens the game world to new players
func (c *Control) OpenWorld() {
c.Open = 1
}
// Returns all class names as a slice
// GetClassNames returns all class names as a slice
func (c *Control) GetClassNames() []string {
classes := make([]string, 0, 3)
if c.Class1Name != "" {
@ -245,26 +147,7 @@ func (c *Control) GetClassNames() []string {
return classes
}
// Sets all class names from a slice
func (c *Control) SetClassNames(classes []string) {
// Reset all class names
c.Class1Name = ""
c.Class2Name = ""
c.Class3Name = ""
// Set provided class names
if len(classes) > 0 {
c.Class1Name = classes[0]
}
if len(classes) > 1 {
c.Class2Name = classes[1]
}
if len(classes) > 2 {
c.Class3Name = classes[2]
}
}
// Returns the name of a specific class (1-3)
// GetClassName returns the name of a specific class (1-3)
func (c *Control) GetClassName(classNum int) string {
switch classNum {
case 1:
@ -278,24 +161,7 @@ func (c *Control) GetClassName(classNum int) string {
}
}
// Sets the name of a specific class (1-3)
func (c *Control) SetClassName(classNum int, name string) bool {
switch classNum {
case 1:
c.Class1Name = name
return true
case 2:
c.Class2Name = name
return true
case 3:
c.Class3Name = name
return true
default:
return false
}
}
// Returns true if the given name matches one of the configured classes
// IsValidClassName returns true if the given name matches one of the configured classes
func (c *Control) IsValidClassName(name string) bool {
if name == "" {
return false
@ -303,7 +169,7 @@ func (c *Control) IsValidClassName(name string) bool {
return name == c.Class1Name || name == c.Class2Name || name == c.Class3Name
}
// Returns the class number (1-3) for a given class name, or 0 if not found
// GetClassNumber returns the class number (1-3) for a given class name, or 0 if not found
func (c *Control) GetClassNumber(name string) int {
if name == c.Class1Name && name != "" {
return 1
@ -317,28 +183,28 @@ func (c *Control) GetClassNumber(name string) int {
return 0
}
// Returns true if an admin email is configured
// HasAdminEmail returns true if an admin email is configured
func (c *Control) HasAdminEmail() bool {
return c.AdminEmail != ""
}
// Returns true if the world size is within reasonable bounds
// IsWorldSizeValid returns true if the world size is within reasonable bounds
func (c *Control) IsWorldSizeValid() bool {
return c.WorldSize > 0 && c.WorldSize <= 10000
}
// Returns the world radius (half the world size)
// GetWorldRadius returns the world radius (half the world size)
func (c *Control) GetWorldRadius() int {
return c.WorldSize / 2
}
// Returns true if the given coordinates are within world bounds
// IsWithinWorldBounds returns true if the given coordinates are within world bounds
func (c *Control) IsWithinWorldBounds(x, y int) bool {
radius := c.GetWorldRadius()
return x >= -radius && x <= radius && y >= -radius && y <= radius
}
// Returns the minimum and maximum coordinates for the world
// GetWorldBounds returns the minimum and maximum coordinates for the world
func (c *Control) GetWorldBounds() (minX, minY, maxX, maxY int) {
radius := c.GetWorldRadius()
return -radius, -radius, radius, radius

View File

@ -3,8 +3,6 @@ package drops
import (
"dk/internal/store"
"fmt"
"sort"
"sync"
)
// Drop represents a drop item in the game
@ -17,14 +15,11 @@ type Drop struct {
}
func (d *Drop) Save() error {
dropStore := GetStore()
dropStore.UpdateDrop(d)
return nil
return GetStore().UpdateWithRebuild(d.ID, d)
}
func (d *Drop) Delete() error {
dropStore := GetStore()
dropStore.RemoveDrop(d.ID)
GetStore().RemoveWithRebuild(d.ID)
return nil
}
@ -57,239 +52,96 @@ const (
TypeConsumable = 1
)
// DropStore provides in-memory storage with O(1) lookups and drop-specific indices
// DropStore with enhanced BaseStore
type DropStore struct {
*store.BaseStore[Drop] // Embedded generic store
byLevel map[int][]int // Level -> []ID
byType map[int][]int // Type -> []ID
allByID []int // All IDs sorted by ID
mu sync.RWMutex // Protects indices
*store.BaseStore[Drop]
}
// Global in-memory store
var dropStore *DropStore
var storeOnce sync.Once
// Global store with singleton pattern
var GetStore = store.NewSingleton(func() *DropStore {
ds := &DropStore{BaseStore: store.NewBaseStore[Drop]()}
// Initialize the in-memory store
func initStore() {
dropStore = &DropStore{
BaseStore: store.NewBaseStore[Drop](),
byLevel: make(map[int][]int),
byType: make(map[int][]int),
allByID: make([]int, 0),
}
// Register indices
ds.RegisterIndex("byLevel", store.BuildIntGroupIndex(func(d *Drop) int {
return d.Level
}))
ds.RegisterIndex("byType", store.BuildIntGroupIndex(func(d *Drop) int {
return d.Type
}))
ds.RegisterIndex("allByID", store.BuildSortedListIndex(func(a, b *Drop) bool {
return a.ID < b.ID
}))
return ds
})
// Enhanced CRUD operations
func (ds *DropStore) AddDrop(drop *Drop) error {
return ds.AddWithRebuild(drop.ID, drop)
}
// GetStore returns the global drop store
func GetStore() *DropStore {
storeOnce.Do(initStore)
return dropStore
}
// AddDrop adds a drop to the in-memory store and updates all indices
func (ds *DropStore) AddDrop(drop *Drop) {
ds.mu.Lock()
defer ds.mu.Unlock()
// Validate drop
if err := drop.Validate(); err != nil {
return
}
// Add to base store
ds.Add(drop.ID, drop)
// Rebuild indices
ds.rebuildIndicesUnsafe()
}
// RemoveDrop removes a drop from the store and updates indices
func (ds *DropStore) RemoveDrop(id int) {
ds.mu.Lock()
defer ds.mu.Unlock()
// Remove from base store
ds.Remove(id)
// Rebuild indices
ds.rebuildIndicesUnsafe()
ds.RemoveWithRebuild(id)
}
// UpdateDrop updates a drop efficiently
func (ds *DropStore) UpdateDrop(drop *Drop) {
ds.mu.Lock()
defer ds.mu.Unlock()
// Validate drop
if err := drop.Validate(); err != nil {
return
}
// Update base store
ds.Add(drop.ID, drop)
// Rebuild indices
ds.rebuildIndicesUnsafe()
func (ds *DropStore) UpdateDrop(drop *Drop) error {
return ds.UpdateWithRebuild(drop.ID, drop)
}
// LoadData loads drop data from JSON file, or starts with empty store
// Data persistence
func LoadData(dataPath string) error {
ds := GetStore()
// Load from base store, which handles JSON loading
if err := ds.BaseStore.LoadData(dataPath); err != nil {
return err
}
// Rebuild indices from loaded data
ds.rebuildIndices()
return nil
return ds.BaseStore.LoadData(dataPath)
}
// SaveData saves drop data to JSON file
func SaveData(dataPath string) error {
ds := GetStore()
return ds.BaseStore.SaveData(dataPath)
}
// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock)
func (ds *DropStore) rebuildIndicesUnsafe() {
// Clear indices
ds.byLevel = make(map[int][]int)
ds.byType = make(map[int][]int)
ds.allByID = make([]int, 0)
// Collect all drops and build indices
allDrops := ds.GetAll()
for id, drop := range allDrops {
// Level index
ds.byLevel[drop.Level] = append(ds.byLevel[drop.Level], id)
// Type index
ds.byType[drop.Type] = append(ds.byType[drop.Type], id)
// All IDs
ds.allByID = append(ds.allByID, id)
}
// Sort allByID by ID
sort.Ints(ds.allByID)
// Sort level indices by ID
for level := range ds.byLevel {
sort.Ints(ds.byLevel[level])
}
// Sort type indices by level, then ID
for dropType := range ds.byType {
sort.Slice(ds.byType[dropType], func(i, j int) bool {
dropI, _ := ds.GetByID(ds.byType[dropType][i])
dropJ, _ := ds.GetByID(ds.byType[dropType][j])
if dropI.Level != dropJ.Level {
return dropI.Level < dropJ.Level
}
return ds.byType[dropType][i] < ds.byType[dropType][j]
})
}
}
// rebuildIndices rebuilds all drop-specific indices from base store data
func (ds *DropStore) rebuildIndices() {
ds.mu.Lock()
defer ds.mu.Unlock()
ds.rebuildIndicesUnsafe()
}
// Retrieves a drop by ID
// Query functions using enhanced store
func Find(id int) (*Drop, error) {
ds := GetStore()
drop, exists := ds.GetByID(id)
drop, exists := ds.Find(id)
if !exists {
return nil, fmt.Errorf("drop with ID %d not found", id)
}
return drop, nil
}
// Retrieves all drops
func All() ([]*Drop, error) {
ds := GetStore()
ds.mu.RLock()
defer ds.mu.RUnlock()
result := make([]*Drop, 0, len(ds.allByID))
for _, id := range ds.allByID {
if drop, exists := ds.GetByID(id); exists {
result = append(result, drop)
}
}
return result, nil
return ds.AllSorted("allByID"), nil
}
// Retrieves drops by minimum level requirement
func ByLevel(minLevel int) ([]*Drop, error) {
ds := GetStore()
ds.mu.RLock()
defer ds.mu.RUnlock()
var result []*Drop
for level := 1; level <= minLevel; level++ {
if ids, exists := ds.byLevel[level]; exists {
for _, id := range ids {
if drop, exists := ds.GetByID(id); exists {
result = append(result, drop)
}
}
}
}
return result, nil
return ds.FilterByIndex("allByID", func(d *Drop) bool {
return d.Level <= minLevel
}), nil
}
// Retrieves drops by type
func ByType(dropType int) ([]*Drop, error) {
ds := GetStore()
ds.mu.RLock()
defer ds.mu.RUnlock()
ids, exists := ds.byType[dropType]
if !exists {
return []*Drop{}, nil
}
result := make([]*Drop, 0, len(ids))
for _, id := range ids {
if drop, exists := ds.GetByID(id); exists {
result = append(result, drop)
}
}
return result, nil
return ds.GroupByIndex("byType", dropType), nil
}
// Saves a new drop to the in-memory store and sets the ID
// Insert with ID assignment
func (d *Drop) Insert() error {
ds := GetStore()
// Validate before insertion
if err := d.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Assign new ID if not set
if d.ID == 0 {
d.ID = ds.GetNextID()
}
// Add to store
ds.AddDrop(d)
return nil
return ds.AddDrop(d)
}
// Returns true if the drop is a consumable item
// Helper methods
func (d *Drop) IsConsumable() bool {
return d.Type == TypeConsumable
}
// Returns the string representation of the drop type
func (d *Drop) TypeName() string {
switch d.Type {
case TypeConsumable:

View File

@ -5,7 +5,6 @@ import (
"fmt"
"sort"
"strings"
"sync"
"time"
)
@ -22,14 +21,11 @@ type Forum struct {
}
func (f *Forum) Save() error {
forumStore := GetStore()
forumStore.UpdateForum(f)
return nil
return GetStore().UpdateWithRebuild(f.ID, f)
}
func (f *Forum) Delete() error {
forumStore := GetStore()
forumStore.RemoveForum(f.ID)
GetStore().RemoveWithRebuild(f.ID)
return nil
}
@ -70,393 +66,191 @@ func (f *Forum) Validate() error {
return nil
}
// ForumStore provides in-memory storage with O(1) lookups and forum-specific indices
// ForumStore with enhanced BaseStore
type ForumStore struct {
*store.BaseStore[Forum] // Embedded generic store
byParent map[int][]int // Parent -> []ID
byAuthor map[int][]int // Author -> []ID
threadsOnly []int // Parent=0 IDs sorted by last_post DESC, id DESC
allByLastPost []int // All IDs sorted by last_post DESC, id DESC
mu sync.RWMutex // Protects indices
*store.BaseStore[Forum]
}
// Global in-memory store
var forumStore *ForumStore
var storeOnce sync.Once
// Global store with singleton pattern
var GetStore = store.NewSingleton(func() *ForumStore {
fs := &ForumStore{BaseStore: store.NewBaseStore[Forum]()}
// Initialize the in-memory store
func initStore() {
forumStore = &ForumStore{
BaseStore: store.NewBaseStore[Forum](),
byParent: make(map[int][]int),
byAuthor: make(map[int][]int),
threadsOnly: make([]int, 0),
allByLastPost: make([]int, 0),
}
// Register indices
fs.RegisterIndex("byParent", store.BuildIntGroupIndex(func(f *Forum) int {
return f.Parent
}))
fs.RegisterIndex("byAuthor", store.BuildIntGroupIndex(func(f *Forum) int {
return f.Author
}))
fs.RegisterIndex("allByLastPost", store.BuildSortedListIndex(func(a, b *Forum) bool {
if a.LastPost != b.LastPost {
return a.LastPost > b.LastPost // DESC
}
return a.ID > b.ID // DESC
}))
return fs
})
// Enhanced CRUD operations
func (fs *ForumStore) AddForum(forum *Forum) error {
return fs.AddWithRebuild(forum.ID, forum)
}
// GetStore returns the global forum store
func GetStore() *ForumStore {
storeOnce.Do(initStore)
return forumStore
}
// AddForum adds a forum post to the in-memory store and updates all indices
func (fs *ForumStore) AddForum(forum *Forum) {
fs.mu.Lock()
defer fs.mu.Unlock()
// Validate forum
if err := forum.Validate(); err != nil {
return
}
// Add to base store
fs.Add(forum.ID, forum)
// Rebuild indices
fs.rebuildIndicesUnsafe()
}
// RemoveForum removes a forum post from the store and updates indices
func (fs *ForumStore) RemoveForum(id int) {
fs.mu.Lock()
defer fs.mu.Unlock()
// Remove from base store
fs.Remove(id)
// Rebuild indices
fs.rebuildIndicesUnsafe()
fs.RemoveWithRebuild(id)
}
// UpdateForum updates a forum post efficiently
func (fs *ForumStore) UpdateForum(forum *Forum) {
fs.mu.Lock()
defer fs.mu.Unlock()
// Validate forum
if err := forum.Validate(); err != nil {
return
}
// Update base store
fs.Add(forum.ID, forum)
// Rebuild indices
fs.rebuildIndicesUnsafe()
func (fs *ForumStore) UpdateForum(forum *Forum) error {
return fs.UpdateWithRebuild(forum.ID, forum)
}
// LoadData loads forum data from JSON file, or starts with empty store
// Data persistence
func LoadData(dataPath string) error {
fs := GetStore()
// Load from base store, which handles JSON loading
if err := fs.BaseStore.LoadData(dataPath); err != nil {
return err
}
// Rebuild indices from loaded data
fs.rebuildIndices()
return nil
return fs.BaseStore.LoadData(dataPath)
}
// SaveData saves forum data to JSON file
func SaveData(dataPath string) error {
fs := GetStore()
return fs.BaseStore.SaveData(dataPath)
}
// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock)
func (fs *ForumStore) rebuildIndicesUnsafe() {
// Clear indices
fs.byParent = make(map[int][]int)
fs.byAuthor = make(map[int][]int)
fs.threadsOnly = make([]int, 0)
fs.allByLastPost = make([]int, 0)
// Collect all forum posts and build indices
allForums := fs.GetAll()
for id, forum := range allForums {
// Parent index
fs.byParent[forum.Parent] = append(fs.byParent[forum.Parent], id)
// Author index
fs.byAuthor[forum.Author] = append(fs.byAuthor[forum.Author], id)
// Threads only (parent = 0)
if forum.Parent == 0 {
fs.threadsOnly = append(fs.threadsOnly, id)
}
// All posts
fs.allByLastPost = append(fs.allByLastPost, id)
}
// Sort allByLastPost by last_post DESC, then ID DESC
sort.Slice(fs.allByLastPost, func(i, j int) bool {
forumI, _ := fs.GetByID(fs.allByLastPost[i])
forumJ, _ := fs.GetByID(fs.allByLastPost[j])
if forumI.LastPost != forumJ.LastPost {
return forumI.LastPost > forumJ.LastPost // DESC
}
return fs.allByLastPost[i] > fs.allByLastPost[j] // DESC
})
// Sort threadsOnly by last_post DESC, then ID DESC
sort.Slice(fs.threadsOnly, func(i, j int) bool {
forumI, _ := fs.GetByID(fs.threadsOnly[i])
forumJ, _ := fs.GetByID(fs.threadsOnly[j])
if forumI.LastPost != forumJ.LastPost {
return forumI.LastPost > forumJ.LastPost // DESC
}
return fs.threadsOnly[i] > fs.threadsOnly[j] // DESC
})
// Sort byParent replies by posted ASC, then ID ASC
for parent := range fs.byParent {
if parent > 0 { // Only sort replies, not threads
sort.Slice(fs.byParent[parent], func(i, j int) bool {
forumI, _ := fs.GetByID(fs.byParent[parent][i])
forumJ, _ := fs.GetByID(fs.byParent[parent][j])
if forumI.Posted != forumJ.Posted {
return forumI.Posted < forumJ.Posted // ASC
}
return fs.byParent[parent][i] < fs.byParent[parent][j] // ASC
})
}
}
// Sort byAuthor by posted DESC, then ID DESC
for author := range fs.byAuthor {
sort.Slice(fs.byAuthor[author], func(i, j int) bool {
forumI, _ := fs.GetByID(fs.byAuthor[author][i])
forumJ, _ := fs.GetByID(fs.byAuthor[author][j])
if forumI.Posted != forumJ.Posted {
return forumI.Posted > forumJ.Posted // DESC
}
return fs.byAuthor[author][i] > fs.byAuthor[author][j] // DESC
})
}
}
// rebuildIndices rebuilds all forum-specific indices from base store data
func (fs *ForumStore) rebuildIndices() {
fs.mu.Lock()
defer fs.mu.Unlock()
fs.rebuildIndicesUnsafe()
}
// Retrieves a forum post by ID
// Query functions using enhanced store
func Find(id int) (*Forum, error) {
fs := GetStore()
forum, exists := fs.GetByID(id)
forum, exists := fs.Find(id)
if !exists {
return nil, fmt.Errorf("forum post with ID %d not found", id)
}
return forum, nil
}
// Retrieves all forum posts ordered by last post time (most recent first)
func All() ([]*Forum, error) {
fs := GetStore()
fs.mu.RLock()
defer fs.mu.RUnlock()
result := make([]*Forum, 0, len(fs.allByLastPost))
for _, id := range fs.allByLastPost {
if forum, exists := fs.GetByID(id); exists {
result = append(result, forum)
}
}
return result, nil
return fs.AllSorted("allByLastPost"), nil
}
// Retrieves all top-level forum threads (parent = 0)
func Threads() ([]*Forum, error) {
fs := GetStore()
fs.mu.RLock()
defer fs.mu.RUnlock()
result := make([]*Forum, 0, len(fs.threadsOnly))
for _, id := range fs.threadsOnly {
if forum, exists := fs.GetByID(id); exists {
result = append(result, forum)
}
}
return result, nil
return fs.FilterByIndex("allByLastPost", func(f *Forum) bool {
return f.Parent == 0
}), nil
}
// Retrieves all replies to a specific thread/post
func ByParent(parentID int) ([]*Forum, error) {
fs := GetStore()
fs.mu.RLock()
defer fs.mu.RUnlock()
replies := fs.GroupByIndex("byParent", parentID)
ids, exists := fs.byParent[parentID]
if !exists {
return []*Forum{}, nil
// Sort replies chronologically (posted ASC, then ID ASC)
if parentID > 0 && len(replies) > 1 {
sort.Slice(replies, func(i, j int) bool {
if replies[i].Posted != replies[j].Posted {
return replies[i].Posted < replies[j].Posted // ASC
}
return replies[i].ID < replies[j].ID // ASC
})
}
result := make([]*Forum, 0, len(ids))
for _, id := range ids {
if forum, exists := fs.GetByID(id); exists {
result = append(result, forum)
}
}
return result, nil
return replies, nil
}
// Retrieves forum posts by a specific author
func ByAuthor(authorID int) ([]*Forum, error) {
fs := GetStore()
fs.mu.RLock()
defer fs.mu.RUnlock()
posts := fs.GroupByIndex("byAuthor", authorID)
ids, exists := fs.byAuthor[authorID]
if !exists {
return []*Forum{}, nil
}
result := make([]*Forum, 0, len(ids))
for _, id := range ids {
if forum, exists := fs.GetByID(id); exists {
result = append(result, forum)
// Sort by posted DESC, then ID DESC
sort.Slice(posts, func(i, j int) bool {
if posts[i].Posted != posts[j].Posted {
return posts[i].Posted > posts[j].Posted // DESC
}
}
return result, nil
return posts[i].ID > posts[j].ID // DESC
})
return posts, nil
}
// Retrieves the most recent forum activity (limited by count)
func Recent(limit int) ([]*Forum, error) {
fs := GetStore()
fs.mu.RLock()
defer fs.mu.RUnlock()
if limit > len(fs.allByLastPost) {
limit = len(fs.allByLastPost)
all := fs.AllSorted("allByLastPost")
if limit > len(all) {
limit = len(all)
}
result := make([]*Forum, 0, limit)
for i := 0; i < limit; i++ {
if forum, exists := fs.GetByID(fs.allByLastPost[i]); exists {
result = append(result, forum)
}
}
return result, nil
return all[:limit], nil
}
// Retrieves forum posts containing the search term in title or content
func Search(term string) ([]*Forum, error) {
fs := GetStore()
fs.mu.RLock()
defer fs.mu.RUnlock()
var result []*Forum
lowerTerm := strings.ToLower(term)
for _, id := range fs.allByLastPost {
if forum, exists := fs.GetByID(id); exists {
if strings.Contains(strings.ToLower(forum.Title), lowerTerm) ||
strings.Contains(strings.ToLower(forum.Content), lowerTerm) {
result = append(result, forum)
}
}
}
return result, nil
return fs.FilterByIndex("allByLastPost", func(f *Forum) bool {
return strings.Contains(strings.ToLower(f.Title), lowerTerm) ||
strings.Contains(strings.ToLower(f.Content), lowerTerm)
}), nil
}
// Retrieves forum posts with activity since a specific timestamp
func Since(since int64) ([]*Forum, error) {
fs := GetStore()
fs.mu.RLock()
defer fs.mu.RUnlock()
var result []*Forum
for _, id := range fs.allByLastPost {
if forum, exists := fs.GetByID(id); exists && forum.LastPost >= since {
result = append(result, forum)
}
}
return result, nil
return fs.FilterByIndex("allByLastPost", func(f *Forum) bool {
return f.LastPost >= since
}), nil
}
// Saves a new forum post to the in-memory store and sets the ID
// Insert with ID assignment
func (f *Forum) Insert() error {
fs := GetStore()
// Validate before insertion
if err := f.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Assign new ID if not set
if f.ID == 0 {
f.ID = fs.GetNextID()
}
// Add to store
fs.AddForum(f)
return nil
return fs.AddForum(f)
}
// Returns the posted timestamp as a time.Time
// Helper methods
func (f *Forum) PostedTime() time.Time {
return time.Unix(f.Posted, 0)
}
// Returns the last post timestamp as a time.Time
func (f *Forum) LastPostTime() time.Time {
return time.Unix(f.LastPost, 0)
}
// Sets the posted timestamp from a time.Time
func (f *Forum) SetPostedTime(t time.Time) {
f.Posted = t.Unix()
}
// Sets the last post timestamp from a time.Time
func (f *Forum) SetLastPostTime(t time.Time) {
f.LastPost = t.Unix()
}
// Returns true if this is a top-level thread (parent = 0)
func (f *Forum) IsThread() bool {
return f.Parent == 0
}
// Returns true if this is a reply to another post (parent > 0)
func (f *Forum) IsReply() bool {
return f.Parent > 0
}
// Returns true if this post has replies
func (f *Forum) HasReplies() bool {
return f.Replies > 0
}
// Returns true if there has been activity within the last 24 hours
func (f *Forum) IsRecentActivity() bool {
return time.Since(f.LastPostTime()) < 24*time.Hour
}
// Returns how long ago the last activity occurred
func (f *Forum) ActivityAge() time.Duration {
return time.Since(f.LastPostTime())
}
// Returns how long ago the post was originally made
func (f *Forum) PostAge() time.Duration {
return time.Since(f.PostedTime())
}
// Returns true if the given user ID is the author of this post
func (f *Forum) IsAuthor(userID int) bool {
return f.Author == userID
}
// Returns a truncated version of the content for previews
func (f *Forum) Preview(maxLength int) string {
if len(f.Content) <= maxLength {
return f.Content
@ -469,7 +263,6 @@ func (f *Forum) Preview(maxLength int) string {
return f.Content[:maxLength-3] + "..."
}
// Returns the number of words in the content
func (f *Forum) WordCount() int {
if f.Content == "" {
return 0
@ -497,41 +290,34 @@ func (f *Forum) WordCount() int {
return words
}
// Returns the character length of the content
func (f *Forum) Length() int {
return len(f.Content)
}
// Returns true if the title or content contains the given term (case-insensitive)
func (f *Forum) Contains(term string) bool {
lowerTerm := strings.ToLower(term)
return strings.Contains(strings.ToLower(f.Title), lowerTerm) ||
strings.Contains(strings.ToLower(f.Content), lowerTerm)
}
// Updates the last_post timestamp to current time
func (f *Forum) UpdateLastPost() {
f.LastPost = time.Now().Unix()
}
// Increments the reply count
func (f *Forum) IncrementReplies() {
f.Replies++
}
// Decrements the reply count (minimum 0)
func (f *Forum) DecrementReplies() {
if f.Replies > 0 {
f.Replies--
}
}
// Retrieves all direct replies to this post
func (f *Forum) GetReplies() ([]*Forum, error) {
return ByParent(f.ID)
}
// Retrieves the parent thread (if this is a reply) or returns self (if this is a thread)
func (f *Forum) GetThread() (*Forum, error) {
if f.IsThread() {
return f, nil

View File

@ -3,8 +3,6 @@ package items
import (
"dk/internal/store"
"fmt"
"sort"
"sync"
)
// Item represents an item in the game
@ -18,14 +16,11 @@ type Item struct {
}
func (i *Item) Save() error {
itemStore := GetStore()
itemStore.UpdateItem(i)
return nil
return GetStore().UpdateWithRebuild(i.ID, i)
}
func (i *Item) Delete() error {
itemStore := GetStore()
itemStore.RemoveItem(i.ID)
GetStore().RemoveWithRebuild(i.ID)
return nil
}
@ -64,212 +59,93 @@ const (
TypeShield = 3
)
// ItemStore provides in-memory storage with O(1) lookups and item-specific indices
// ItemStore with enhanced BaseStore
type ItemStore struct {
*store.BaseStore[Item] // Embedded generic store
byType map[int][]int // Type -> []ID
allByID []int // All IDs sorted by ID
mu sync.RWMutex // Protects indices
*store.BaseStore[Item]
}
// Global in-memory store
var itemStore *ItemStore
var storeOnce sync.Once
// Global store with singleton pattern
var GetStore = store.NewSingleton(func() *ItemStore {
is := &ItemStore{BaseStore: store.NewBaseStore[Item]()}
// Initialize the in-memory store
func initStore() {
itemStore = &ItemStore{
BaseStore: store.NewBaseStore[Item](),
byType: make(map[int][]int),
allByID: make([]int, 0),
}
// Register indices
is.RegisterIndex("byType", store.BuildIntGroupIndex(func(i *Item) int {
return i.Type
}))
is.RegisterIndex("allByID", store.BuildSortedListIndex(func(a, b *Item) bool {
return a.ID < b.ID
}))
return is
})
// Enhanced CRUD operations
func (is *ItemStore) AddItem(item *Item) error {
return is.AddWithRebuild(item.ID, item)
}
// GetStore returns the global item store
func GetStore() *ItemStore {
storeOnce.Do(initStore)
return itemStore
}
// AddItem adds an item to the in-memory store and updates all indices
func (is *ItemStore) AddItem(item *Item) {
is.mu.Lock()
defer is.mu.Unlock()
// Validate item
if err := item.Validate(); err != nil {
return
}
// Add to base store
is.Add(item.ID, item)
// Rebuild indices
is.rebuildIndicesUnsafe()
}
// RemoveItem removes an item from the store and updates indices
func (is *ItemStore) RemoveItem(id int) {
is.mu.Lock()
defer is.mu.Unlock()
// Remove from base store
is.Remove(id)
// Rebuild indices
is.rebuildIndicesUnsafe()
is.RemoveWithRebuild(id)
}
// UpdateItem updates an item efficiently
func (is *ItemStore) UpdateItem(item *Item) {
is.mu.Lock()
defer is.mu.Unlock()
// Validate item
if err := item.Validate(); err != nil {
return
}
// Update base store
is.Add(item.ID, item)
// Rebuild indices
is.rebuildIndicesUnsafe()
func (is *ItemStore) UpdateItem(item *Item) error {
return is.UpdateWithRebuild(item.ID, item)
}
// LoadData loads item data from JSON file, or starts with empty store
// Data persistence
func LoadData(dataPath string) error {
is := GetStore()
// Load from base store, which handles JSON loading
if err := is.BaseStore.LoadData(dataPath); err != nil {
return err
}
// Rebuild indices from loaded data
is.rebuildIndices()
return nil
return is.BaseStore.LoadData(dataPath)
}
// SaveData saves item data to JSON file
func SaveData(dataPath string) error {
is := GetStore()
return is.BaseStore.SaveData(dataPath)
}
// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock)
func (is *ItemStore) rebuildIndicesUnsafe() {
// Clear indices
is.byType = make(map[int][]int)
is.allByID = make([]int, 0)
// Collect all items and build indices
allItems := is.GetAll()
for id, item := range allItems {
// Type index
is.byType[item.Type] = append(is.byType[item.Type], id)
// All IDs
is.allByID = append(is.allByID, id)
}
// Sort allByID by ID
sort.Ints(is.allByID)
// Sort type indices by ID
for itemType := range is.byType {
sort.Ints(is.byType[itemType])
}
}
// rebuildIndices rebuilds all item-specific indices from base store data
func (is *ItemStore) rebuildIndices() {
is.mu.Lock()
defer is.mu.Unlock()
is.rebuildIndicesUnsafe()
}
// Retrieves an item by ID
// Query functions using enhanced store
func Find(id int) (*Item, error) {
is := GetStore()
item, exists := is.GetByID(id)
item, exists := is.Find(id)
if !exists {
return nil, fmt.Errorf("item with ID %d not found", id)
}
return item, nil
}
// Retrieves all items
func All() ([]*Item, error) {
is := GetStore()
is.mu.RLock()
defer is.mu.RUnlock()
result := make([]*Item, 0, len(is.allByID))
for _, id := range is.allByID {
if item, exists := is.GetByID(id); exists {
result = append(result, item)
}
}
return result, nil
return is.AllSorted("allByID"), nil
}
// Retrieves items by type
func ByType(itemType int) ([]*Item, error) {
is := GetStore()
is.mu.RLock()
defer is.mu.RUnlock()
ids, exists := is.byType[itemType]
if !exists {
return []*Item{}, nil
}
result := make([]*Item, 0, len(ids))
for _, id := range ids {
if item, exists := is.GetByID(id); exists {
result = append(result, item)
}
}
return result, nil
return is.GroupByIndex("byType", itemType), nil
}
// Saves a new item to the in-memory store and sets the ID
// Insert with ID assignment
func (i *Item) Insert() error {
is := GetStore()
// Validate before insertion
if err := i.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Assign new ID if not set
if i.ID == 0 {
i.ID = is.GetNextID()
}
// Add to store
is.AddItem(i)
return nil
return is.AddItem(i)
}
// Returns true if the item is a weapon
// Helper methods
func (i *Item) IsWeapon() bool {
return i.Type == TypeWeapon
}
// Returns true if the item is armor
func (i *Item) IsArmor() bool {
return i.Type == TypeArmor
}
// Returns true if the item is a shield
func (i *Item) IsShield() bool {
return i.Type == TypeShield
}
// Returns the string representation of the item type
func (i *Item) TypeName() string {
switch i.Type {
case TypeWeapon:
@ -283,12 +159,10 @@ func (i *Item) TypeName() string {
}
}
// Returns true if the item has special properties
func (i *Item) HasSpecial() bool {
return i.Special != ""
}
// Returns true if the item can be equipped
func (i *Item) IsEquippable() bool {
return i.Type == TypeWeapon || i.Type == TypeArmor || i.Type == TypeShield
}

View File

@ -3,9 +3,7 @@ package news
import (
"dk/internal/store"
"fmt"
"sort"
"strings"
"sync"
"time"
)
@ -18,14 +16,11 @@ type News struct {
}
func (n *News) Save() error {
newsStore := GetStore()
newsStore.UpdateNews(n)
return nil
return GetStore().UpdateWithRebuild(n.ID, n)
}
func (n *News) Delete() error {
newsStore := GetStore()
newsStore.RemoveNews(n.ID)
GetStore().RemoveWithRebuild(n.ID)
return nil
}
@ -49,309 +44,139 @@ func (n *News) Validate() error {
return nil
}
// NewsStore provides in-memory storage with O(1) lookups and news-specific indices
// NewsStore with enhanced BaseStore
type NewsStore struct {
*store.BaseStore[News] // Embedded generic store
byAuthor map[int][]int // Author -> []ID
allByPosted []int // All IDs sorted by posted DESC, id DESC
mu sync.RWMutex // Protects indices
*store.BaseStore[News]
}
// Global in-memory store
var newsStore *NewsStore
var storeOnce sync.Once
// Global store with singleton pattern
var GetStore = store.NewSingleton(func() *NewsStore {
ns := &NewsStore{BaseStore: store.NewBaseStore[News]()}
// Initialize the in-memory store
func initStore() {
newsStore = &NewsStore{
BaseStore: store.NewBaseStore[News](),
byAuthor: make(map[int][]int),
allByPosted: make([]int, 0),
}
// Register indices
ns.RegisterIndex("byAuthor", store.BuildIntGroupIndex(func(n *News) int {
return n.Author
}))
ns.RegisterIndex("allByPosted", store.BuildSortedListIndex(func(a, b *News) bool {
if a.Posted != b.Posted {
return a.Posted > b.Posted // DESC
}
return a.ID > b.ID // DESC
}))
return ns
})
// Enhanced CRUD operations
func (ns *NewsStore) AddNews(news *News) error {
return ns.AddWithRebuild(news.ID, news)
}
// GetStore returns the global news store
func GetStore() *NewsStore {
storeOnce.Do(initStore)
return newsStore
}
// AddNews adds a news post to the in-memory store and updates all indices
func (ns *NewsStore) AddNews(news *News) {
ns.mu.Lock()
defer ns.mu.Unlock()
// Validate news
if err := news.Validate(); err != nil {
return
}
// Add to base store
ns.Add(news.ID, news)
// Rebuild indices
ns.rebuildIndicesUnsafe()
}
// RemoveNews removes a news post from the store and updates indices
func (ns *NewsStore) RemoveNews(id int) {
ns.mu.Lock()
defer ns.mu.Unlock()
// Remove from base store
ns.Remove(id)
// Rebuild indices
ns.rebuildIndicesUnsafe()
ns.RemoveWithRebuild(id)
}
// UpdateNews updates a news post efficiently
func (ns *NewsStore) UpdateNews(news *News) {
ns.mu.Lock()
defer ns.mu.Unlock()
// Validate news
if err := news.Validate(); err != nil {
return
}
// Update base store
ns.Add(news.ID, news)
// Rebuild indices
ns.rebuildIndicesUnsafe()
func (ns *NewsStore) UpdateNews(news *News) error {
return ns.UpdateWithRebuild(news.ID, news)
}
// LoadData loads news data from JSON file, or starts with empty store
// Data persistence
func LoadData(dataPath string) error {
ns := GetStore()
// Load from base store, which handles JSON loading
if err := ns.BaseStore.LoadData(dataPath); err != nil {
return err
}
// Rebuild indices from loaded data
ns.rebuildIndices()
return nil
return ns.BaseStore.LoadData(dataPath)
}
// SaveData saves news data to JSON file
func SaveData(dataPath string) error {
ns := GetStore()
return ns.BaseStore.SaveData(dataPath)
}
// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock)
func (ns *NewsStore) rebuildIndicesUnsafe() {
// Clear indices
ns.byAuthor = make(map[int][]int)
ns.allByPosted = make([]int, 0)
// Collect all news and build indices
allNews := ns.GetAll()
for id, news := range allNews {
// Author index
ns.byAuthor[news.Author] = append(ns.byAuthor[news.Author], id)
// All IDs
ns.allByPosted = append(ns.allByPosted, id)
}
// Sort allByPosted by posted DESC, then ID DESC
sort.Slice(ns.allByPosted, func(i, j int) bool {
newsI, _ := ns.GetByID(ns.allByPosted[i])
newsJ, _ := ns.GetByID(ns.allByPosted[j])
if newsI.Posted != newsJ.Posted {
return newsI.Posted > newsJ.Posted // DESC
}
return ns.allByPosted[i] > ns.allByPosted[j] // DESC
})
// Sort author indices by posted DESC, then ID DESC
for author := range ns.byAuthor {
sort.Slice(ns.byAuthor[author], func(i, j int) bool {
newsI, _ := ns.GetByID(ns.byAuthor[author][i])
newsJ, _ := ns.GetByID(ns.byAuthor[author][j])
if newsI.Posted != newsJ.Posted {
return newsI.Posted > newsJ.Posted // DESC
}
return ns.byAuthor[author][i] > ns.byAuthor[author][j] // DESC
})
}
}
// rebuildIndices rebuilds all news-specific indices from base store data
func (ns *NewsStore) rebuildIndices() {
ns.mu.Lock()
defer ns.mu.Unlock()
ns.rebuildIndicesUnsafe()
}
// Retrieves a news post by ID
// Query functions using enhanced store
func Find(id int) (*News, error) {
ns := GetStore()
news, exists := ns.GetByID(id)
news, exists := ns.Find(id)
if !exists {
return nil, fmt.Errorf("news with ID %d not found", id)
}
return news, nil
}
// Retrieves all news posts ordered by posted date (newest first)
func All() ([]*News, error) {
ns := GetStore()
ns.mu.RLock()
defer ns.mu.RUnlock()
result := make([]*News, 0, len(ns.allByPosted))
for _, id := range ns.allByPosted {
if news, exists := ns.GetByID(id); exists {
result = append(result, news)
}
}
return result, nil
return ns.AllSorted("allByPosted"), nil
}
// Retrieves news posts by a specific author
func ByAuthor(authorID int) ([]*News, error) {
ns := GetStore()
ns.mu.RLock()
defer ns.mu.RUnlock()
ids, exists := ns.byAuthor[authorID]
if !exists {
return []*News{}, nil
}
result := make([]*News, 0, len(ids))
for _, id := range ids {
if news, exists := ns.GetByID(id); exists {
result = append(result, news)
}
}
return result, nil
return ns.GroupByIndex("byAuthor", authorID), nil
}
// Retrieves the most recent news posts (limited by count)
func Recent(limit int) ([]*News, error) {
ns := GetStore()
ns.mu.RLock()
defer ns.mu.RUnlock()
if limit > len(ns.allByPosted) {
limit = len(ns.allByPosted)
all := ns.AllSorted("allByPosted")
if limit > len(all) {
limit = len(all)
}
result := make([]*News, 0, limit)
for i := 0; i < limit; i++ {
if news, exists := ns.GetByID(ns.allByPosted[i]); exists {
result = append(result, news)
}
}
return result, nil
return all[:limit], nil
}
// Retrieves news posts since a specific timestamp
func Since(since int64) ([]*News, error) {
ns := GetStore()
ns.mu.RLock()
defer ns.mu.RUnlock()
var result []*News
for _, id := range ns.allByPosted {
if news, exists := ns.GetByID(id); exists && news.Posted >= since {
result = append(result, news)
}
}
return result, nil
return ns.FilterByIndex("allByPosted", func(n *News) bool {
return n.Posted >= since
}), nil
}
// Retrieves news posts between two timestamps (inclusive)
func Between(start, end int64) ([]*News, error) {
ns := GetStore()
ns.mu.RLock()
defer ns.mu.RUnlock()
var result []*News
for _, id := range ns.allByPosted {
if news, exists := ns.GetByID(id); exists && news.Posted >= start && news.Posted <= end {
result = append(result, news)
}
}
return result, nil
return ns.FilterByIndex("allByPosted", func(n *News) bool {
return n.Posted >= start && n.Posted <= end
}), nil
}
// Retrieves news posts containing the search term in content
func Search(term string) ([]*News, error) {
ns := GetStore()
ns.mu.RLock()
defer ns.mu.RUnlock()
var result []*News
lowerTerm := strings.ToLower(term)
for _, id := range ns.allByPosted {
if news, exists := ns.GetByID(id); exists {
if strings.Contains(strings.ToLower(news.Content), lowerTerm) {
result = append(result, news)
}
}
}
return result, nil
return ns.FilterByIndex("allByPosted", func(n *News) bool {
return strings.Contains(strings.ToLower(n.Content), lowerTerm)
}), nil
}
// Saves a new news post to the in-memory store and sets the ID
// Insert with ID assignment
func (n *News) Insert() error {
ns := GetStore()
// Validate before insertion
if err := n.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Assign new ID if not set
if n.ID == 0 {
n.ID = ns.GetNextID()
}
// Add to store
ns.AddNews(n)
return nil
return ns.AddNews(n)
}
// Returns the posted timestamp as a time.Time
// Helper methods
func (n *News) PostedTime() time.Time {
return time.Unix(n.Posted, 0)
}
// Sets the posted timestamp from a time.Time
func (n *News) SetPostedTime(t time.Time) {
n.Posted = t.Unix()
}
// Returns true if the news post was made within the last 24 hours
func (n *News) IsRecent() bool {
return time.Since(n.PostedTime()) < 24*time.Hour
}
// Returns how long ago the news post was made
func (n *News) Age() time.Duration {
return time.Since(n.PostedTime())
}
// Converts a time.Time to a human-readable date string
func (n *News) ReadableTime() string {
return n.PostedTime().Format("Jan 2, 2006 3:04 PM")
}
// Returns true if the given user ID is the author of this news post
func (n *News) IsAuthor(userID int) bool {
return n.Author == userID
}
// Returns a truncated version of the content for previews
func (n *News) Preview(maxLength int) string {
if len(n.Content) <= maxLength {
return n.Content
@ -364,7 +189,6 @@ func (n *News) Preview(maxLength int) string {
return n.Content[:maxLength-3] + "..."
}
// Returns the number of words in the content
func (n *News) WordCount() int {
if n.Content == "" {
return 0
@ -392,17 +216,14 @@ func (n *News) WordCount() int {
return words
}
// Returns the character length of the content
func (n *News) Length() int {
return len(n.Content)
}
// Returns true if the content contains the given term (case-insensitive)
func (n *News) Contains(term string) bool {
return strings.Contains(strings.ToLower(n.Content), strings.ToLower(term))
}
// Returns true if the content is empty or whitespace-only
func (n *News) IsEmpty() bool {
return strings.TrimSpace(n.Content) == ""
}

View File

@ -36,10 +36,8 @@ func Move(ctx router.Ctx, _ []string) {
return
}
user.Set("Currently", currently)
user.Set("X", newX)
user.Set("Y", newY)
user.Save()
user.Currently = currently
user.X, user.Y = newX, newY
if currently == "In Town" {
ctx.Redirect("/town", 303)

View File

@ -73,11 +73,8 @@ func rest(ctx router.Ctx, _ []string) {
return
}
user.Set("Gold", user.Gold-town.InnCost)
user.Set("HP", user.MaxHP)
user.Set("MP", user.MaxMP)
user.Set("TP", user.MaxTP)
user.Save()
user.Gold -= town.InnCost
user.HP, user.MP, user.TP = user.MaxHP, user.MaxMP, user.MaxTP
components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{
"town": town,
@ -140,7 +137,7 @@ func buyItem(ctx router.Ctx, params []string) {
return
}
user.Set("Gold", user.Gold-item.Value)
user.Gold -= item.Value
actions.UserEquipItem(user, item)
user.Save()
@ -210,11 +207,11 @@ func buyMap(ctx router.Ctx, params []string) {
return
}
user.Set("Gold", user.Gold-mapped.MapCost)
user.Gold -= mapped.MapCost
if user.Towns == "" {
user.Set("Towns", params[0])
user.Towns = params[0]
} else {
user.Set("Towns", user.Towns+","+params[0])
user.Towns += "," + params[0]
}
user.Save()

View File

@ -1,114 +0,0 @@
package server
import (
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"dk/internal/auth"
"dk/internal/middleware"
"dk/internal/monsters"
"dk/internal/router"
"dk/internal/routes"
"dk/internal/template"
"github.com/valyala/fasthttp"
)
func Start(port string) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current working directory: %w", err)
}
// Initialize template singleton
template.InitializeCache(cwd)
// Load monster data into memory
if err := monsters.LoadData("data/monsters.json"); err != nil {
return fmt.Errorf("failed to load monster data: %w", err)
}
auth.Init("sessions.json") // Initialize auth.Manager
r := router.New()
r.Use(middleware.Timing())
r.Use(middleware.Auth(auth.Manager))
r.Use(middleware.CSRF(auth.Manager))
r.Get("/", routes.Index)
r.Get("/explore", routes.Explore)
r.Post("/move", routes.Move)
routes.RegisterAuthRoutes(r)
routes.RegisterTownRoutes(r)
// Use current working directory for static files
assetsDir := filepath.Join(cwd, "assets")
// Static file server for /assets
fs := &fasthttp.FS{
Root: assetsDir,
Compress: false,
}
assetsHandler := fs.NewRequestHandler()
// Combined handler
requestHandler := func(ctx *fasthttp.RequestCtx) {
path := string(ctx.Path())
// Handle static assets - strip /assets prefix
if len(path) >= 7 && path[:7] == "/assets" {
// Strip the /assets prefix for the file system handler
originalPath := ctx.Path()
ctx.Request.URI().SetPath(path[7:]) // Remove "/assets" prefix
assetsHandler(ctx)
ctx.Request.URI().SetPathBytes(originalPath) // Restore original path
return
}
// Handle routes
r.ServeHTTP(ctx)
}
addr := ":" + port
log.Printf("Server starting on %s", addr)
// Setup graceful shutdown
server := &fasthttp.Server{
Handler: requestHandler,
}
// Channel to listen for interrupt signal
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
// Start server in a goroutine
go func() {
if err := server.ListenAndServe(addr); err != nil {
log.Printf("Server error: %v", err)
}
}()
// Wait for interrupt signal
<-c
log.Println("Received shutdown signal, shutting down gracefully...")
// Save monster data before shutdown
log.Println("Saving monster data...")
if err := monsters.SaveData("data/monsters.json"); err != nil {
log.Printf("Error saving monster data: %v", err)
}
// Save sessions before shutdown
log.Println("Saving sessions...")
if err := auth.Manager.Close(); err != nil {
log.Printf("Error saving sessions: %v", err)
}
// FastHTTP doesn't have a graceful Shutdown method like net/http
// We just let the server stop naturally when the main function exits
log.Println("Server stopped")
return nil
}

View File

@ -3,9 +3,7 @@ package spells
import (
"dk/internal/store"
"fmt"
"sort"
"strings"
"sync"
)
// Spell represents a spell in the game
@ -18,14 +16,11 @@ type Spell struct {
}
func (s *Spell) Save() error {
spellStore := GetStore()
spellStore.UpdateSpell(s)
return nil
return GetStore().UpdateWithRebuild(s.ID, s)
}
func (s *Spell) Delete() error {
spellStore := GetStore()
spellStore.RemoveSpell(s.ID)
GetStore().RemoveWithRebuild(s.ID)
return nil
}
@ -65,321 +60,138 @@ const (
TypeDefenseBoost = 5
)
// SpellStore provides in-memory storage with O(1) lookups and spell-specific indices
// SpellStore with enhanced BaseStore
type SpellStore struct {
*store.BaseStore[Spell] // Embedded generic store
byType map[int][]int // Type -> []ID
byName map[string]int // Name (lowercase) -> ID
byMP map[int][]int // MP -> []ID
allByTypeMP []int // All IDs sorted by type, MP, ID
mu sync.RWMutex // Protects indices
*store.BaseStore[Spell]
}
// Global in-memory store
var spellStore *SpellStore
var storeOnce sync.Once
// Global store with singleton pattern
var GetStore = store.NewSingleton(func() *SpellStore {
ss := &SpellStore{BaseStore: store.NewBaseStore[Spell]()}
// Initialize the in-memory store
func initStore() {
spellStore = &SpellStore{
BaseStore: store.NewBaseStore[Spell](),
byType: make(map[int][]int),
byName: make(map[string]int),
byMP: make(map[int][]int),
allByTypeMP: make([]int, 0),
}
// Register indices
ss.RegisterIndex("byType", store.BuildIntGroupIndex(func(s *Spell) int {
return s.Type
}))
ss.RegisterIndex("byName", store.BuildCaseInsensitiveLookupIndex(func(s *Spell) string {
return s.Name
}))
ss.RegisterIndex("byMP", store.BuildIntGroupIndex(func(s *Spell) int {
return s.MP
}))
ss.RegisterIndex("allByTypeMP", store.BuildSortedListIndex(func(a, b *Spell) bool {
if a.Type != b.Type {
return a.Type < b.Type
}
if a.MP != b.MP {
return a.MP < b.MP
}
return a.ID < b.ID
}))
return ss
})
// Enhanced CRUD operations
func (ss *SpellStore) AddSpell(spell *Spell) error {
return ss.AddWithRebuild(spell.ID, spell)
}
// GetStore returns the global spell store
func GetStore() *SpellStore {
storeOnce.Do(initStore)
return spellStore
}
// AddSpell adds a spell to the in-memory store and updates all indices
func (ss *SpellStore) AddSpell(spell *Spell) {
ss.mu.Lock()
defer ss.mu.Unlock()
// Validate spell
if err := spell.Validate(); err != nil {
return
}
// Add to base store
ss.Add(spell.ID, spell)
// Rebuild indices
ss.rebuildIndicesUnsafe()
}
// RemoveSpell removes a spell from the store and updates indices
func (ss *SpellStore) RemoveSpell(id int) {
ss.mu.Lock()
defer ss.mu.Unlock()
// Remove from base store
ss.Remove(id)
// Rebuild indices
ss.rebuildIndicesUnsafe()
ss.RemoveWithRebuild(id)
}
// UpdateSpell updates a spell efficiently
func (ss *SpellStore) UpdateSpell(spell *Spell) {
ss.mu.Lock()
defer ss.mu.Unlock()
// Validate spell
if err := spell.Validate(); err != nil {
return
}
// Update base store
ss.Add(spell.ID, spell)
// Rebuild indices
ss.rebuildIndicesUnsafe()
func (ss *SpellStore) UpdateSpell(spell *Spell) error {
return ss.UpdateWithRebuild(spell.ID, spell)
}
// LoadData loads spell data from JSON file, or starts with empty store
// Data persistence
func LoadData(dataPath string) error {
ss := GetStore()
// Load from base store, which handles JSON loading
if err := ss.BaseStore.LoadData(dataPath); err != nil {
return err
}
// Rebuild indices from loaded data
ss.rebuildIndices()
return nil
return ss.BaseStore.LoadData(dataPath)
}
// SaveData saves spell data to JSON file
func SaveData(dataPath string) error {
ss := GetStore()
return ss.BaseStore.SaveData(dataPath)
}
// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock)
func (ss *SpellStore) rebuildIndicesUnsafe() {
// Clear indices
ss.byType = make(map[int][]int)
ss.byName = make(map[string]int)
ss.byMP = make(map[int][]int)
ss.allByTypeMP = make([]int, 0)
// Collect all spells and build indices
allSpells := ss.GetAll()
for id, spell := range allSpells {
// Type index
ss.byType[spell.Type] = append(ss.byType[spell.Type], id)
// Name index (case-insensitive)
ss.byName[strings.ToLower(spell.Name)] = id
// MP index
ss.byMP[spell.MP] = append(ss.byMP[spell.MP], id)
// All IDs
ss.allByTypeMP = append(ss.allByTypeMP, id)
}
// Sort allByTypeMP by type, then MP, then ID
sort.Slice(ss.allByTypeMP, func(i, j int) bool {
spellI, _ := ss.GetByID(ss.allByTypeMP[i])
spellJ, _ := ss.GetByID(ss.allByTypeMP[j])
if spellI.Type != spellJ.Type {
return spellI.Type < spellJ.Type
}
if spellI.MP != spellJ.MP {
return spellI.MP < spellJ.MP
}
return ss.allByTypeMP[i] < ss.allByTypeMP[j]
})
// Sort type indices by MP, then ID
for spellType := range ss.byType {
sort.Slice(ss.byType[spellType], func(i, j int) bool {
spellI, _ := ss.GetByID(ss.byType[spellType][i])
spellJ, _ := ss.GetByID(ss.byType[spellType][j])
if spellI.MP != spellJ.MP {
return spellI.MP < spellJ.MP
}
return ss.byType[spellType][i] < ss.byType[spellType][j]
})
}
// Sort MP indices by type, then ID
for mp := range ss.byMP {
sort.Slice(ss.byMP[mp], func(i, j int) bool {
spellI, _ := ss.GetByID(ss.byMP[mp][i])
spellJ, _ := ss.GetByID(ss.byMP[mp][j])
if spellI.Type != spellJ.Type {
return spellI.Type < spellJ.Type
}
return ss.byMP[mp][i] < ss.byMP[mp][j]
})
}
}
// rebuildIndices rebuilds all spell-specific indices from base store data
func (ss *SpellStore) rebuildIndices() {
ss.mu.Lock()
defer ss.mu.Unlock()
ss.rebuildIndicesUnsafe()
}
// Retrieves a spell by ID
// Query functions using enhanced store
func Find(id int) (*Spell, error) {
ss := GetStore()
spell, exists := ss.GetByID(id)
spell, exists := ss.Find(id)
if !exists {
return nil, fmt.Errorf("spell with ID %d not found", id)
}
return spell, nil
}
// Retrieves all spells
func All() ([]*Spell, error) {
ss := GetStore()
ss.mu.RLock()
defer ss.mu.RUnlock()
result := make([]*Spell, 0, len(ss.allByTypeMP))
for _, id := range ss.allByTypeMP {
if spell, exists := ss.GetByID(id); exists {
result = append(result, spell)
}
}
return result, nil
return ss.AllSorted("allByTypeMP"), nil
}
// Retrieves spells by type
func ByType(spellType int) ([]*Spell, error) {
ss := GetStore()
ss.mu.RLock()
defer ss.mu.RUnlock()
ids, exists := ss.byType[spellType]
if !exists {
return []*Spell{}, nil
}
result := make([]*Spell, 0, len(ids))
for _, id := range ids {
if spell, exists := ss.GetByID(id); exists {
result = append(result, spell)
}
}
return result, nil
return ss.GroupByIndex("byType", spellType), nil
}
// Retrieves spells that cost at most the specified MP
func ByMaxMP(maxMP int) ([]*Spell, error) {
ss := GetStore()
ss.mu.RLock()
defer ss.mu.RUnlock()
var result []*Spell
for mp := 0; mp <= maxMP; mp++ {
if ids, exists := ss.byMP[mp]; exists {
for _, id := range ids {
if spell, exists := ss.GetByID(id); exists {
result = append(result, spell)
}
}
}
}
return result, nil
return ss.FilterByIndex("allByTypeMP", func(s *Spell) bool {
return s.MP <= maxMP
}), nil
}
// Retrieves spells of a specific type that cost at most the specified MP
func ByTypeAndMaxMP(spellType, maxMP int) ([]*Spell, error) {
ss := GetStore()
ss.mu.RLock()
defer ss.mu.RUnlock()
ids, exists := ss.byType[spellType]
if !exists {
return []*Spell{}, nil
}
var result []*Spell
for _, id := range ids {
if spell, exists := ss.GetByID(id); exists && spell.MP <= maxMP {
result = append(result, spell)
}
}
return result, nil
return ss.FilterByIndex("allByTypeMP", func(s *Spell) bool {
return s.Type == spellType && s.MP <= maxMP
}), nil
}
// Retrieves a spell by name (case-insensitive)
func ByName(name string) (*Spell, error) {
ss := GetStore()
ss.mu.RLock()
defer ss.mu.RUnlock()
id, exists := ss.byName[strings.ToLower(name)]
spell, exists := ss.LookupByIndex("byName", strings.ToLower(name))
if !exists {
return nil, fmt.Errorf("spell with name '%s' not found", name)
}
spell, exists := ss.GetByID(id)
if !exists {
return nil, fmt.Errorf("spell with name '%s' not found", name)
}
return spell, nil
}
// Saves a new spell to the in-memory store and sets the ID
// Insert with ID assignment
func (s *Spell) Insert() error {
ss := GetStore()
// Validate before insertion
if err := s.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Assign new ID if not set
if s.ID == 0 {
s.ID = ss.GetNextID()
}
// Add to store
ss.AddSpell(s)
return nil
return ss.AddSpell(s)
}
// Returns true if the spell is a healing spell
// Helper methods
func (s *Spell) IsHealing() bool {
return s.Type == TypeHealing
}
// Returns true if the spell is a hurt spell
func (s *Spell) IsHurt() bool {
return s.Type == TypeHurt
}
// Returns true if the spell is a sleep spell
func (s *Spell) IsSleep() bool {
return s.Type == TypeSleep
}
// Returns true if the spell boosts attack
func (s *Spell) IsAttackBoost() bool {
return s.Type == TypeAttackBoost
}
// Returns true if the spell boosts defense
func (s *Spell) IsDefenseBoost() bool {
return s.Type == TypeDefenseBoost
}
// Returns the string representation of the spell type
func (s *Spell) TypeName() string {
switch s.Type {
case TypeHealing:
@ -397,12 +209,10 @@ func (s *Spell) TypeName() string {
}
}
// Returns true if the spell can be cast with the given MP
func (s *Spell) CanCast(availableMP int) bool {
return availableMP >= s.MP
}
// Returns the attribute per MP ratio (higher is more efficient)
func (s *Spell) Efficiency() float64 {
if s.MP == 0 {
return 0
@ -410,12 +220,10 @@ func (s *Spell) Efficiency() float64 {
return float64(s.Attribute) / float64(s.MP)
}
// Returns true if the spell is used for attacking
func (s *Spell) IsOffensive() bool {
return s.Type == TypeHurt || s.Type == TypeSleep
}
// Returns true if the spell is used for support/buffs
func (s *Spell) IsSupport() bool {
return s.Type == TypeHealing || s.Type == TypeAttackBoost || s.Type == TypeDefenseBoost
}

View File

@ -41,8 +41,6 @@ func NewBaseStore[T any]() *BaseStore[T] {
}
}
// Index Management
// RegisterIndex registers an index builder function
func (bs *BaseStore[T]) RegisterIndex(name string, builder IndexBuilder[T]) {
bs.mu.Lock()
@ -76,8 +74,6 @@ func (bs *BaseStore[T]) rebuildIndicesUnsafe() {
}
}
// Enhanced CRUD Operations
// AddWithRebuild adds item with validation and index rebuild
func (bs *BaseStore[T]) AddWithRebuild(id int, item *T) error {
bs.mu.Lock()
@ -234,8 +230,6 @@ func (bs *BaseStore[T]) FilterByIndex(indexName string, filterFunc func(*T) bool
return result
}
// Common Index Builders
// BuildStringLookupIndex creates string-to-ID mapping
func BuildStringLookupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
return func(allItems map[int]*T) any {
@ -312,8 +306,6 @@ func BuildSortedListIndex[T any](sortFunc func(*T, *T) bool) IndexBuilder[T] {
}
}
// Singleton Management Helper
// NewSingleton creates singleton store pattern with sync.Once
func NewSingleton[S any](initFunc func() *S) func() *S {
var store *S
@ -327,8 +319,6 @@ func NewSingleton[S any](initFunc func() *S) func() *S {
}
}
// Legacy Methods (backward compatibility)
// GetNextID returns the next available ID atomically
func (bs *BaseStore[T]) GetNextID() int {
bs.mu.Lock()
@ -379,8 +369,6 @@ func (bs *BaseStore[T]) Clear() {
bs.rebuildIndicesUnsafe()
}
// JSON Persistence
// LoadFromJSON loads items from JSON using reflection
func (bs *BaseStore[T]) LoadFromJSON(filename string) error {
bs.mu.Lock()
@ -471,7 +459,7 @@ func (bs *BaseStore[T]) LoadData(dataPath string) error {
return fmt.Errorf("failed to load from JSON: %w", err)
}
fmt.Printf("Loaded %d items from JSON\n", len(bs.items))
fmt.Printf("Loaded %d items from %s\n", len(bs.items), dataPath)
bs.RebuildIndices() // Rebuild indices after loading
return nil
}
@ -488,6 +476,6 @@ func (bs *BaseStore[T]) SaveData(dataPath string) error {
return fmt.Errorf("failed to save to JSON: %w", err)
}
fmt.Printf("Saved %d items to JSON\n", len(bs.items))
fmt.Printf("Saved %d items to %s\n", len(bs.items), dataPath)
return nil
}

View File

@ -8,7 +8,6 @@ import (
"sort"
"strconv"
"strings"
"sync"
"dk/internal/helpers"
)
@ -26,14 +25,11 @@ type Town struct {
}
func (t *Town) Save() error {
townStore := GetStore()
townStore.UpdateTown(t)
return nil
return GetStore().UpdateWithRebuild(t.ID, t)
}
func (t *Town) Delete() error {
townStore := GetStore()
townStore.RemoveTown(t.ID)
GetStore().RemoveWithRebuild(t.ID)
return nil
}
@ -67,284 +63,128 @@ func (t *Town) Validate() error {
return nil
}
// TownStore provides in-memory storage with O(1) lookups and town-specific indices
type TownStore struct {
*store.BaseStore[Town] // Embedded generic store
byName map[string]int // Name (lowercase) -> ID
byCoords map[string]int // "x,y" -> ID
byInnCost map[int][]int // InnCost -> []ID
byTPCost map[int][]int // TPCost -> []ID
allByID []int // All IDs sorted by ID
mu sync.RWMutex // Protects indices
}
// Global in-memory store
var townStore *TownStore
var storeOnce sync.Once
// Initialize the in-memory store
func initStore() {
townStore = &TownStore{
BaseStore: store.NewBaseStore[Town](),
byName: make(map[string]int),
byCoords: make(map[string]int),
byInnCost: make(map[int][]int),
byTPCost: make(map[int][]int),
allByID: make([]int, 0),
}
}
// GetStore returns the global town store
func GetStore() *TownStore {
storeOnce.Do(initStore)
return townStore
}
// AddTown adds a town to the in-memory store and updates all indices
func (ts *TownStore) AddTown(town *Town) {
ts.mu.Lock()
defer ts.mu.Unlock()
// Validate town
if err := town.Validate(); err != nil {
return
}
// Add to base store
ts.Add(town.ID, town)
// Rebuild indices
ts.rebuildIndicesUnsafe()
}
// RemoveTown removes a town from the store and updates indices
func (ts *TownStore) RemoveTown(id int) {
ts.mu.Lock()
defer ts.mu.Unlock()
// Remove from base store
ts.Remove(id)
// Rebuild indices
ts.rebuildIndicesUnsafe()
}
// UpdateTown updates a town efficiently
func (ts *TownStore) UpdateTown(town *Town) {
ts.mu.Lock()
defer ts.mu.Unlock()
// Validate town
if err := town.Validate(); err != nil {
return
}
// Update base store
ts.Add(town.ID, town)
// Rebuild indices
ts.rebuildIndicesUnsafe()
}
// LoadData loads town data from JSON file, or starts with empty store
func LoadData(dataPath string) error {
ts := GetStore()
// Load from base store, which handles JSON loading
if err := ts.BaseStore.LoadData(dataPath); err != nil {
return err
}
// Rebuild indices from loaded data
ts.rebuildIndices()
return nil
}
// SaveData saves town data to JSON file
func SaveData(dataPath string) error {
ts := GetStore()
return ts.BaseStore.SaveData(dataPath)
}
// coordsKey creates a key for coordinate-based lookup
func coordsKey(x, y int) string {
return strconv.Itoa(x) + "," + strconv.Itoa(y)
}
// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock)
func (ts *TownStore) rebuildIndicesUnsafe() {
// Clear indices
ts.byName = make(map[string]int)
ts.byCoords = make(map[string]int)
ts.byInnCost = make(map[int][]int)
ts.byTPCost = make(map[int][]int)
ts.allByID = make([]int, 0)
// Collect all towns and build indices
allTowns := ts.GetAll()
for id, town := range allTowns {
// Name index (case-insensitive)
ts.byName[strings.ToLower(town.Name)] = id
// Coordinates index
ts.byCoords[coordsKey(town.X, town.Y)] = id
// Cost indices
ts.byInnCost[town.InnCost] = append(ts.byInnCost[town.InnCost], id)
ts.byTPCost[town.TPCost] = append(ts.byTPCost[town.TPCost], id)
// All IDs
ts.allByID = append(ts.allByID, id)
}
// Sort all by ID
sort.Ints(ts.allByID)
// Sort cost indices by ID
for innCost := range ts.byInnCost {
sort.Ints(ts.byInnCost[innCost])
}
for tpCost := range ts.byTPCost {
sort.Ints(ts.byTPCost[tpCost])
}
// TownStore with enhanced BaseStore
type TownStore struct {
*store.BaseStore[Town]
}
// rebuildIndices rebuilds all town-specific indices from base store data
func (ts *TownStore) rebuildIndices() {
ts.mu.Lock()
defer ts.mu.Unlock()
ts.rebuildIndicesUnsafe()
// Global store with singleton pattern
var GetStore = store.NewSingleton(func() *TownStore {
ts := &TownStore{BaseStore: store.NewBaseStore[Town]()}
// Register indices
ts.RegisterIndex("byName", store.BuildCaseInsensitiveLookupIndex(func(t *Town) string {
return t.Name
}))
ts.RegisterIndex("byCoords", store.BuildStringLookupIndex(func(t *Town) string {
return coordsKey(t.X, t.Y)
}))
ts.RegisterIndex("byInnCost", store.BuildIntGroupIndex(func(t *Town) int {
return t.InnCost
}))
ts.RegisterIndex("byTPCost", store.BuildIntGroupIndex(func(t *Town) int {
return t.TPCost
}))
ts.RegisterIndex("allByID", store.BuildSortedListIndex(func(a, b *Town) bool {
return a.ID < b.ID
}))
return ts
})
// Enhanced CRUD operations
func (ts *TownStore) AddTown(town *Town) error {
return ts.AddWithRebuild(town.ID, town)
}
// Retrieves a town by ID
func (ts *TownStore) RemoveTown(id int) {
ts.RemoveWithRebuild(id)
}
func (ts *TownStore) UpdateTown(town *Town) error {
return ts.UpdateWithRebuild(town.ID, town)
}
// Data persistence
func LoadData(dataPath string) error {
ts := GetStore()
return ts.BaseStore.LoadData(dataPath)
}
func SaveData(dataPath string) error {
ts := GetStore()
return ts.BaseStore.SaveData(dataPath)
}
// Query functions using enhanced store
func Find(id int) (*Town, error) {
ts := GetStore()
town, exists := ts.GetByID(id)
town, exists := ts.Find(id)
if !exists {
return nil, fmt.Errorf("town with ID %d not found", id)
}
return town, nil
}
// Retrieves all towns
func All() ([]*Town, error) {
ts := GetStore()
ts.mu.RLock()
defer ts.mu.RUnlock()
result := make([]*Town, 0, len(ts.allByID))
for _, id := range ts.allByID {
if town, exists := ts.GetByID(id); exists {
result = append(result, town)
}
}
return result, nil
return ts.AllSorted("allByID"), nil
}
// Retrieves a town by name (case-insensitive)
func ByName(name string) (*Town, error) {
ts := GetStore()
ts.mu.RLock()
defer ts.mu.RUnlock()
id, exists := ts.byName[strings.ToLower(name)]
town, exists := ts.LookupByIndex("byName", strings.ToLower(name))
if !exists {
return nil, fmt.Errorf("town with name '%s' not found", name)
}
town, exists := ts.GetByID(id)
if !exists {
return nil, fmt.Errorf("town with name '%s' not found", name)
}
return town, nil
}
// Retrieves towns with inn cost at most the specified amount
func ByMaxInnCost(maxCost int) ([]*Town, error) {
ts := GetStore()
ts.mu.RLock()
defer ts.mu.RUnlock()
var result []*Town
for cost := 0; cost <= maxCost; cost++ {
if ids, exists := ts.byInnCost[cost]; exists {
for _, id := range ids {
if town, exists := ts.GetByID(id); exists {
result = append(result, town)
}
}
}
}
return result, nil
return ts.FilterByIndex("allByID", func(t *Town) bool {
return t.InnCost <= maxCost
}), nil
}
// Retrieves towns with teleport cost at most the specified amount
func ByMaxTPCost(maxCost int) ([]*Town, error) {
ts := GetStore()
ts.mu.RLock()
defer ts.mu.RUnlock()
var result []*Town
for cost := 0; cost <= maxCost; cost++ {
if ids, exists := ts.byTPCost[cost]; exists {
for _, id := range ids {
if town, exists := ts.GetByID(id); exists {
result = append(result, town)
}
}
}
}
return result, nil
return ts.FilterByIndex("allByID", func(t *Town) bool {
return t.TPCost <= maxCost
}), nil
}
// Retrieves a town by its x, y coordinates
func ByCoords(x, y int) (*Town, error) {
ts := GetStore()
ts.mu.RLock()
defer ts.mu.RUnlock()
id, exists := ts.byCoords[coordsKey(x, y)]
town, exists := ts.LookupByIndex("byCoords", coordsKey(x, y))
if !exists {
return nil, nil // Return nil if not found (like original)
}
town, exists := ts.GetByID(id)
if !exists {
return nil, nil
}
return town, nil
}
// ExistsAt checks for a town at the given coordinates, returning true/false
func ExistsAt(x, y int) bool {
ts := GetStore()
ts.mu.RLock()
defer ts.mu.RUnlock()
_, exists := ts.byCoords[coordsKey(x, y)]
_, exists := ts.LookupByIndex("byCoords", coordsKey(x, y))
return exists
}
// Retrieves towns within a certain distance from a point
func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
ts := GetStore()
ts.mu.RLock()
defer ts.mu.RUnlock()
var result []*Town
maxDistance2 := float64(maxDistance * maxDistance)
for _, id := range ts.allByID {
if town, exists := ts.GetByID(id); exists {
if town.DistanceFromSquared(fromX, fromY) <= maxDistance2 {
result = append(result, town)
}
}
}
result := ts.FilterByIndex("allByID", func(t *Town) bool {
return t.DistanceFromSquared(fromX, fromY) <= maxDistance2
})
// Sort by distance, then by ID
sort.Slice(result, func(i, j int) bool {
@ -359,83 +199,62 @@ func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
return result, nil
}
// Saves a new town to the in-memory store and sets the ID
// Insert with ID assignment
func (t *Town) Insert() error {
ts := GetStore()
// Validate before insertion
if err := t.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Assign new ID if not set
if t.ID == 0 {
t.ID = ts.GetNextID()
}
// Add to store
ts.AddTown(t)
return nil
return ts.AddTown(t)
}
// Returns the shop items as a slice of item IDs
// Helper methods
func (t *Town) GetShopItems() []int {
return helpers.StringToInts(t.ShopList)
}
// Sets the shop items from a slice of item IDs
func (t *Town) SetShopItems(items []int) {
t.ShopList = helpers.IntsToString(items)
}
// Checks if the town's shop sells a specific item ID
func (t *Town) HasShopItem(itemID int) bool {
return slices.Contains(t.GetShopItems(), itemID)
}
// Calculates the squared distance from this town to given coordinates
func (t *Town) DistanceFromSquared(x, y int) float64 {
dx := float64(t.X - x)
dy := float64(t.Y - y)
return dx*dx + dy*dy // Return squared distance for performance
}
// Calculates the actual distance from this town to given coordinates
func (t *Town) DistanceFrom(x, y int) float64 {
return math.Sqrt(t.DistanceFromSquared(x, y))
}
// Returns true if this is the starting town (Midworld)
func (t *Town) IsStartingTown() bool {
return t.X == 0 && t.Y == 0
}
// Returns true if the player can afford the inn
func (t *Town) CanAffordInn(gold int) bool {
return gold >= t.InnCost
}
// Returns true if the player can afford to buy the map
func (t *Town) CanAffordMap(gold int) bool {
return gold >= t.MapCost
}
// Returns true if the player can afford to teleport here
func (t *Town) CanAffordTeleport(gold int) bool {
return gold >= t.TPCost
}
// Returns true if the town has a shop with items
func (t *Town) HasShop() bool {
return len(t.GetShopItems()) > 0
}
// Returns the town's coordinates
func (t *Town) GetPosition() (int, int) {
return t.X, t.Y
}
// Sets the town's coordinates
func (t *Town) SetPosition(x, y int) {
t.X = x
t.Y = y

View File

@ -6,7 +6,6 @@ import (
"slices"
"sort"
"strings"
"sync"
"time"
"dk/internal/helpers"
@ -67,14 +66,11 @@ type User struct {
}
func (u *User) Save() error {
userStore := GetStore()
userStore.UpdateUser(u)
return nil
return GetStore().UpdateWithRebuild(u.ID, u)
}
func (u *User) Delete() error {
userStore := GetStore()
userStore.RemoveUser(u.ID)
GetStore().RemoveWithRebuild(u.ID)
return nil
}
@ -135,161 +131,76 @@ func (u *User) Validate() error {
return nil
}
// UserStore provides in-memory storage with O(1) lookups and user-specific indices
// UserStore with enhanced BaseStore
type UserStore struct {
*store.BaseStore[User] // Embedded generic store
byUsername map[string]int // Username (lowercase) -> ID
byEmail map[string]int // Email -> ID
byLevel map[int][]int // Level -> []ID
allByRegistered []int // All IDs sorted by registered DESC, id DESC
mu sync.RWMutex // Protects indices
*store.BaseStore[User]
}
// Global in-memory store
var userStore *UserStore
var storeOnce sync.Once
// Global store with singleton pattern
var GetStore = store.NewSingleton(func() *UserStore {
us := &UserStore{BaseStore: store.NewBaseStore[User]()}
// Initialize the in-memory store
func initStore() {
userStore = &UserStore{
BaseStore: store.NewBaseStore[User](),
byUsername: make(map[string]int),
byEmail: make(map[string]int),
byLevel: make(map[int][]int),
allByRegistered: make([]int, 0),
}
// Register indices
us.RegisterIndex("byUsername", store.BuildCaseInsensitiveLookupIndex(func(u *User) string {
return u.Username
}))
us.RegisterIndex("byEmail", store.BuildStringLookupIndex(func(u *User) string {
return u.Email
}))
us.RegisterIndex("byLevel", store.BuildIntGroupIndex(func(u *User) int {
return u.Level
}))
us.RegisterIndex("allByRegistered", store.BuildSortedListIndex(func(a, b *User) bool {
if a.Registered != b.Registered {
return a.Registered > b.Registered // DESC
}
return a.ID > b.ID // DESC
}))
us.RegisterIndex("allByLevelExp", store.BuildSortedListIndex(func(a, b *User) bool {
if a.Level != b.Level {
return a.Level > b.Level // Level DESC
}
if a.Exp != b.Exp {
return a.Exp > b.Exp // Exp DESC
}
return a.ID < b.ID // ID ASC
}))
return us
})
// Enhanced CRUD operations
func (us *UserStore) AddUser(user *User) error {
return us.AddWithRebuild(user.ID, user)
}
// GetStore returns the global user store
func GetStore() *UserStore {
storeOnce.Do(initStore)
return userStore
}
// AddUser adds a user to the in-memory store and updates all indices
func (us *UserStore) AddUser(user *User) {
us.mu.Lock()
defer us.mu.Unlock()
// Validate user
if err := user.Validate(); err != nil {
return
}
// Add to base store
us.Add(user.ID, user)
// Rebuild indices
us.rebuildIndicesUnsafe()
}
// RemoveUser removes a user from the store and updates indices
func (us *UserStore) RemoveUser(id int) {
us.mu.Lock()
defer us.mu.Unlock()
// Remove from base store
us.Remove(id)
// Rebuild indices
us.rebuildIndicesUnsafe()
us.RemoveWithRebuild(id)
}
// UpdateUser updates a user efficiently
func (us *UserStore) UpdateUser(user *User) {
us.mu.Lock()
defer us.mu.Unlock()
// Validate user
if err := user.Validate(); err != nil {
return
}
// Update base store
us.Add(user.ID, user)
// Rebuild indices
us.rebuildIndicesUnsafe()
func (us *UserStore) UpdateUser(user *User) error {
return us.UpdateWithRebuild(user.ID, user)
}
// LoadData loads user data from JSON file, or starts with empty store
// Data persistence
func LoadData(dataPath string) error {
us := GetStore()
// Load from base store, which handles JSON loading
if err := us.BaseStore.LoadData(dataPath); err != nil {
return err
}
// Rebuild indices from loaded data
us.rebuildIndices()
return nil
return us.BaseStore.LoadData(dataPath)
}
// SaveData saves user data to JSON file
func SaveData(dataPath string) error {
us := GetStore()
return us.BaseStore.SaveData(dataPath)
}
// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock)
func (us *UserStore) rebuildIndicesUnsafe() {
// Clear indices
us.byUsername = make(map[string]int)
us.byEmail = make(map[string]int)
us.byLevel = make(map[int][]int)
us.allByRegistered = make([]int, 0)
// Collect all users and build indices
allUsers := us.GetAll()
for id, user := range allUsers {
// Username index (case-insensitive)
us.byUsername[strings.ToLower(user.Username)] = id
// Email index
us.byEmail[user.Email] = id
// Level index
us.byLevel[user.Level] = append(us.byLevel[user.Level], id)
// All IDs
us.allByRegistered = append(us.allByRegistered, id)
}
// Sort allByRegistered by registered DESC, then ID DESC
sort.Slice(us.allByRegistered, func(i, j int) bool {
userI, _ := us.GetByID(us.allByRegistered[i])
userJ, _ := us.GetByID(us.allByRegistered[j])
if userI.Registered != userJ.Registered {
return userI.Registered > userJ.Registered // DESC
}
return us.allByRegistered[i] > us.allByRegistered[j] // DESC
})
// Sort level indices by exp DESC, then ID ASC
for level := range us.byLevel {
sort.Slice(us.byLevel[level], func(i, j int) bool {
userI, _ := us.GetByID(us.byLevel[level][i])
userJ, _ := us.GetByID(us.byLevel[level][j])
if userI.Exp != userJ.Exp {
return userI.Exp > userJ.Exp // DESC
}
return us.byLevel[level][i] < us.byLevel[level][j] // ASC
})
}
}
// rebuildIndices rebuilds all user-specific indices from base store data
func (us *UserStore) rebuildIndices() {
us.mu.Lock()
defer us.mu.Unlock()
us.rebuildIndicesUnsafe()
}
// Query functions using enhanced store
func Find(id int) (*User, error) {
us := GetStore()
user, exists := us.GetByID(id)
user, exists := us.Find(id)
if !exists {
return nil, fmt.Errorf("user with ID %d not found", id)
}
@ -298,86 +209,39 @@ func Find(id int) (*User, error) {
func All() ([]*User, error) {
us := GetStore()
us.mu.RLock()
defer us.mu.RUnlock()
result := make([]*User, 0, len(us.allByRegistered))
for _, id := range us.allByRegistered {
if user, exists := us.GetByID(id); exists {
result = append(result, user)
}
}
return result, nil
return us.AllSorted("allByRegistered"), nil
}
func ByUsername(username string) (*User, error) {
us := GetStore()
us.mu.RLock()
defer us.mu.RUnlock()
id, exists := us.byUsername[strings.ToLower(username)]
user, exists := us.LookupByIndex("byUsername", strings.ToLower(username))
if !exists {
return nil, fmt.Errorf("user with username '%s' not found", username)
}
user, exists := us.GetByID(id)
if !exists {
return nil, fmt.Errorf("user with username '%s' not found", username)
}
return user, nil
}
func ByEmail(email string) (*User, error) {
us := GetStore()
us.mu.RLock()
defer us.mu.RUnlock()
id, exists := us.byEmail[email]
user, exists := us.LookupByIndex("byEmail", email)
if !exists {
return nil, fmt.Errorf("user with email '%s' not found", email)
}
user, exists := us.GetByID(id)
if !exists {
return nil, fmt.Errorf("user with email '%s' not found", email)
}
return user, nil
}
func ByLevel(level int) ([]*User, error) {
us := GetStore()
us.mu.RLock()
defer us.mu.RUnlock()
ids, exists := us.byLevel[level]
if !exists {
return []*User{}, nil
}
result := make([]*User, 0, len(ids))
for _, id := range ids {
if user, exists := us.GetByID(id); exists {
result = append(result, user)
}
}
return result, nil
return us.GroupByIndex("byLevel", level), nil
}
func Online(within time.Duration) ([]*User, error) {
us := GetStore()
us.mu.RLock()
defer us.mu.RUnlock()
cutoff := time.Now().Add(-within).Unix()
var result []*User
for _, id := range us.allByRegistered {
if user, exists := us.GetByID(id); exists && user.LastOnline >= cutoff {
result = append(result, user)
}
}
result := us.FilterByIndex("allByRegistered", func(u *User) bool {
return u.LastOnline >= cutoff
})
// Sort by last_online DESC, then ID ASC
sort.Slice(result, func(i, j int) bool {
@ -390,24 +254,16 @@ func Online(within time.Duration) ([]*User, error) {
return result, nil
}
// Insert with ID assignment
func (u *User) Insert() error {
us := GetStore()
// Validate before insertion
if err := u.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Assign new ID if not set
if u.ID == 0 {
u.ID = us.GetNextID()
}
// Add to store
us.AddUser(u)
return nil
return us.AddUser(u)
}
// Helper methods
func (u *User) RegisteredTime() time.Time {
return time.Unix(u.Registered, 0)
}

215
main.go
View File

@ -5,8 +5,27 @@ import (
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"dk/internal/server"
"dk/internal/auth"
"dk/internal/babble"
"dk/internal/control"
"dk/internal/drops"
"dk/internal/forum"
"dk/internal/items"
"dk/internal/middleware"
"dk/internal/monsters"
"dk/internal/news"
"dk/internal/router"
"dk/internal/routes"
"dk/internal/spells"
"dk/internal/template"
"dk/internal/towns"
"dk/internal/users"
"github.com/valyala/fasthttp"
)
func main() {
@ -25,16 +44,206 @@ func main() {
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", os.Args[1])
fmt.Fprintln(os.Stderr, "Available commands:")
fmt.Fprintln(os.Stderr, " install - Install the database")
fmt.Fprintln(os.Stderr, " serve - Start the server")
fmt.Fprintln(os.Stderr, " (no command) - Start the server")
os.Exit(1)
}
}
func loadModels() error {
dataDir := "data"
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
if err := users.LoadData(filepath.Join(dataDir, "users.json")); err != nil {
return fmt.Errorf("failed to load users data: %w", err)
}
if err := towns.LoadData(filepath.Join(dataDir, "towns.json")); err != nil {
return fmt.Errorf("failed to load towns data: %w", err)
}
if err := spells.LoadData(filepath.Join(dataDir, "spells.json")); err != nil {
return fmt.Errorf("failed to load spells data: %w", err)
}
if err := news.LoadData(filepath.Join(dataDir, "news.json")); err != nil {
return fmt.Errorf("failed to load news data: %w", err)
}
if err := monsters.LoadData(filepath.Join(dataDir, "monsters.json")); err != nil {
return fmt.Errorf("failed to load monsters data: %w", err)
}
if err := items.LoadData(filepath.Join(dataDir, "items.json")); err != nil {
return fmt.Errorf("failed to load items data: %w", err)
}
if err := forum.LoadData(filepath.Join(dataDir, "forum.json")); err != nil {
return fmt.Errorf("failed to load forum data: %w", err)
}
if err := drops.LoadData(filepath.Join(dataDir, "drops.json")); err != nil {
return fmt.Errorf("failed to load drops data: %w", err)
}
if err := babble.LoadData(filepath.Join(dataDir, "babble.json")); err != nil {
return fmt.Errorf("failed to load babble data: %w", err)
}
if err := control.Load(filepath.Join(dataDir, "control.json")); err != nil {
return fmt.Errorf("failed to load control data: %w", err)
}
return nil
}
func saveModels() error {
dataDir := "data"
if err := users.SaveData(filepath.Join(dataDir, "users.json")); err != nil {
return fmt.Errorf("failed to save users data: %w", err)
}
if err := towns.SaveData(filepath.Join(dataDir, "towns.json")); err != nil {
return fmt.Errorf("failed to save towns data: %w", err)
}
if err := spells.SaveData(filepath.Join(dataDir, "spells.json")); err != nil {
return fmt.Errorf("failed to save spells data: %w", err)
}
if err := news.SaveData(filepath.Join(dataDir, "news.json")); err != nil {
return fmt.Errorf("failed to save news data: %w", err)
}
if err := monsters.SaveData(filepath.Join(dataDir, "monsters.json")); err != nil {
return fmt.Errorf("failed to save monsters data: %w", err)
}
if err := items.SaveData(filepath.Join(dataDir, "items.json")); err != nil {
return fmt.Errorf("failed to save items data: %w", err)
}
if err := forum.SaveData(filepath.Join(dataDir, "forum.json")); err != nil {
return fmt.Errorf("failed to save forum data: %w", err)
}
if err := drops.SaveData(filepath.Join(dataDir, "drops.json")); err != nil {
return fmt.Errorf("failed to save drops data: %w", err)
}
if err := babble.SaveData(filepath.Join(dataDir, "babble.json")); err != nil {
return fmt.Errorf("failed to save babble data: %w", err)
}
if err := control.Save(); err != nil {
return fmt.Errorf("failed to save control data: %w", err)
}
return nil
}
func startServer(port string) {
fmt.Println("Starting Dragon Knight server...")
if err := server.Start(port); err != nil {
if err := start(port); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
func start(port string) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current working directory: %w", err)
}
// Initialize template singleton
template.InitializeCache(cwd)
// Load all model data into memory
if err := loadModels(); err != nil {
return fmt.Errorf("failed to load models: %w", err)
}
auth.Init("sessions.json") // Initialize auth.Manager
r := router.New()
r.Use(middleware.Timing())
r.Use(middleware.Auth(auth.Manager))
r.Use(middleware.CSRF(auth.Manager))
r.Get("/", routes.Index)
r.Get("/explore", routes.Explore)
r.Post("/move", routes.Move)
routes.RegisterAuthRoutes(r)
routes.RegisterTownRoutes(r)
// Use current working directory for static files
assetsDir := filepath.Join(cwd, "assets")
// Static file server for /assets
fs := &fasthttp.FS{
Root: assetsDir,
Compress: false,
}
assetsHandler := fs.NewRequestHandler()
// Combined handler
requestHandler := func(ctx *fasthttp.RequestCtx) {
path := string(ctx.Path())
// Handle static assets - strip /assets prefix
if len(path) >= 7 && path[:7] == "/assets" {
// Strip the /assets prefix for the file system handler
originalPath := ctx.Path()
ctx.Request.URI().SetPath(path[7:]) // Remove "/assets" prefix
assetsHandler(ctx)
ctx.Request.URI().SetPathBytes(originalPath) // Restore original path
return
}
// Handle routes
r.ServeHTTP(ctx)
}
addr := ":" + port
log.Printf("Server starting on %s", addr)
// Setup graceful shutdown
server := &fasthttp.Server{
Handler: requestHandler,
}
// Channel to listen for interrupt signal
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
// Start server in a goroutine
go func() {
if err := server.ListenAndServe(addr); err != nil {
log.Printf("Server error: %v", err)
}
}()
// Wait for interrupt signal
<-c
log.Println("Received shutdown signal, shutting down gracefully...")
// Save all model data before shutdown
log.Println("Saving model data...")
if err := saveModels(); err != nil {
log.Printf("Error saving model data: %v", err)
}
// Save sessions before shutdown
log.Println("Saving sessions...")
if err := auth.Manager.Close(); err != nil {
log.Printf("Error saving sessions: %v", err)
}
// FastHTTP doesn't have a graceful Shutdown method like net/http
// We just let the server stop naturally when the main function exits
log.Println("Server stopped")
return nil
}