diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index dffe0944e..cf746c82e 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -26,7 +26,7 @@ jobs:
- name: Setup .NET 5
uses: actions/setup-dotnet@v1
with:
- dotnet-version: 5.0.302
+ dotnet-version: 5.0.401
- name: Build
run: ./publish.cmd
- name: Test
diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml
index a74350987..f08889d15 100644
--- a/.github/workflows/create-release.yml
+++ b/.github/workflows/create-release.yml
@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- token: ${{ secrets.ACCESS_TOKEN }}
+ token: ${{ secrets.GITHUB_TOKEN }}
- name: Install NGBV
uses: dotnet/nbgv@master
id: nbgv
diff --git a/.gitignore b/.gitignore
index f2e5b33e2..325476c99 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,13 +2,16 @@
/Distribution/ModernUO
/Distribution/ModernUO.*
/Distribution/Assemblies
+/Distribution/bsdtar
/Distribution/Configuration/modernuo.json
/Distribution/Configuration/email-settings.json
/Distribution/Configuration/throttles.json
/Distribution/Logs
+/Distribution/Archives
/Distribution/Backups
/Distribution/Saves
/Distribution/docs
+/Distribution/temp
/Distribution/*.dylib
/Distribution/*.so
/Distribution/*.dll
diff --git a/Directory.Build.props b/Directory.Build.props
index ac42d2e11..0ac231599 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -57,9 +57,9 @@
-
+
+ 3.4.240
all
- runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Distribution/Data/Decoration/Britannia/_hythloth.cfg b/Distribution/Data/Decoration/Britannia/_hythloth.cfg
index f85e11cf2..b462966b2 100644
--- a/Distribution/Data/Decoration/Britannia/_hythloth.cfg
+++ b/Distribution/Data/Decoration/Britannia/_hythloth.cfg
@@ -701,10 +701,6 @@ MetalDoor2 0x06C7 (Facing=EastCCW)
5988 159 0
6060 167 0
-# Blood Moss
-Static 0x0F7B
-2678 712 0
-
# garbage
Static 0x10F0
5988 46 22
diff --git a/Distribution/Data/Professions/AOS/prof.txt b/Distribution/Data/Professions/AOS/prof.txt
new file mode 100644
index 000000000..50dc7dec1
--- /dev/null
+++ b/Distribution/Data/Professions/AOS/prof.txt
@@ -0,0 +1,85 @@
+Begin
+ Name Paladin
+ TrueName "Paladin"
+ NameId 1061177
+ DescId 1061227
+ Desc 5
+ TopLevel true
+ Gump 5587
+ Type Profession
+ Skill Chivalry 50
+ Skill Tactics 50
+ Skill Wrestling 0
+ Stat Str 30
+ Stat Dex 25
+ Stat Int 25
+End
+
+Begin
+ Name Necromancer
+ TrueName "Necromancer"
+ NameId 1061178
+ DescId 1061228
+ Desc 4
+ TopLevel true
+ Gump 5557
+ Type Profession
+ Skill Necromancy 50
+ Skill Swordsmanship 30
+ Skill Wrestling 0
+ Skill Tactics 20
+ Stat Str 25
+ Stat Dex 20
+ Stat Int 45
+End
+
+Begin
+ Name Warrior
+ TrueName "Warrior"
+ NameId 1061180
+ DescId 1061230
+ Desc 1
+ TopLevel true
+ Gump 5577
+ Type Profession
+ Skill Tactics 50
+ Skill Healing 45
+ Skill Swordsmanship 5
+ Stat Str 35
+ Stat Dex 35
+ Stat Int 10
+End
+
+Begin
+ Name Mage
+ TrueName "Mage"
+ NameId 1061179
+ DescId 1061229
+ Desc 2
+ TopLevel true
+ Gump 5569
+ Type Profession
+ Skill Magery 50
+ Skill Meditation 50
+ Skill Wrestling 0
+ Stat Str 25
+ Stat Dex 10
+ Stat Int 45
+End
+
+Begin
+ Name Blacksmith
+ TrueName "Blacksmith"
+ NameId 1061181
+ DescId 1061231
+ Desc 3
+ TopLevel true
+ Gump 5555
+ Type Profession
+ Skill Blacksmith 50
+ Skill Tinkering 45
+ Skill Mining 5
+ Stat Str 60
+ Stat Dex 10
+ Stat Int 10
+End
\ No newline at end of file
diff --git a/Distribution/Data/Professions/None/prof.txt b/Distribution/Data/Professions/None/prof.txt
new file mode 100644
index 000000000..2225b8b16
--- /dev/null
+++ b/Distribution/Data/Professions/None/prof.txt
@@ -0,0 +1,50 @@
+Begin
+ Name Warrior
+ TrueName "Warrior"
+ NameId 1061180
+ DescId 1061230
+ Desc 1
+ TopLevel true
+ Gump 5577
+ Type Profession
+ Skill Tactics 50
+ Skill Healing 45
+ Skill Swordsmanship 5
+ Stat Str 35
+ Stat Dex 35
+ Stat Int 10
+End
+
+Begin
+ Name Mage
+ TrueName "Mage"
+ NameId 1061179
+ DescId 1061229
+ Desc 2
+ TopLevel true
+ Gump 5569
+ Type Profession
+ Skill Magery 50
+ Skill Meditation 50
+ Skill Wrestling 0
+ Stat Str 25
+ Stat Dex 10
+ Stat Int 45
+End
+
+Begin
+ Name Blacksmith
+ TrueName "Blacksmith"
+ NameId 1061181
+ DescId 1061231
+ Desc 3
+ TopLevel true
+ Gump 5555
+ Type Profession
+ Skill Blacksmith 50
+ Skill Tinkering 45
+ Skill Mining 5
+ Stat Str 60
+ Stat Dex 10
+ Stat Int 10
+End
diff --git a/Distribution/Data/Professions/SA/Prof.txt b/Distribution/Data/Professions/SA/Prof.txt
new file mode 100644
index 000000000..d73afe23f
--- /dev/null
+++ b/Distribution/Data/Professions/SA/Prof.txt
@@ -0,0 +1,125 @@
+Begin
+ Name Samurai
+ TrueName "Samurai"
+ NameId 1062948
+ DescId 1062950
+ Desc 6
+ TopLevel true
+ Gump 5591
+ Type Profession
+ Skill Bushido 30
+ Skill Swordsmanship 30
+ Skill Focus 30
+ Skill Parrying 30
+ Stat Str 40
+ Stat Dex 30
+ Stat Int 20
+End
+
+Begin
+ Name Ninja
+ TrueName "Ninja"
+ NameId 1062949
+ DescId 1062951
+ Desc 7
+ TopLevel true
+ Gump 5589
+ Type Profession
+ Skill Ninjitsu 30
+ Skill Hiding 30
+ Skill Fencing 30
+ Skill Stealth 30
+ Stat Str 40
+ Stat Dex 30
+ Stat Int 20
+End
+
+Begin
+ Name Paladin
+ TrueName "Paladin"
+ NameId 1061177
+ DescId 1061227
+ Desc 5
+ TopLevel true
+ Gump 5587
+ Type Profession
+ Skill Chivalry 30
+ Skill Tactics 30
+ Skill Focus 30
+ Skill Swordsmanship 30
+ Stat Str 45
+ Stat Dex 20
+ Stat Int 25
+End
+
+Begin
+ Name Necromancer
+ TrueName "Necromancer"
+ NameId 1061178
+ DescId 1061228
+ Desc 4
+ TopLevel true
+ Gump 5557
+ Type Profession
+ Skill Necromancy 30
+ Skill Spirit Speak 30
+ Skill Swordsmanship 30
+ Skill Meditation 30
+ Stat Str 25
+ Stat Dex 20
+ Stat Int 45
+End
+
+Begin
+ Name Warrior
+ TrueName "Warrior"
+ NameId 1061180
+ DescId 1061230
+ Desc 1
+ TopLevel true
+ Gump 5577
+ Type Profession
+ Skill Tactics 30
+ Skill Healing 30
+ Skill Swordsmanship 30
+ Skill Anatomy 30
+ Stat Str 45
+ Stat Dex 35
+ Stat Int 10
+End
+
+Begin
+ Name Mage
+ TrueName "Mage"
+ NameId 1061179
+ DescId 1061229
+ Desc 2
+ TopLevel true
+ Gump 5569
+ Type Profession
+ Skill Magery 30
+ Skill Meditation 30
+ Skill Evaluate Intelligence 30
+ Skill Wrestling 30
+ Stat Str 25
+ Stat Dex 20
+ Stat Int 45
+End
+
+Begin
+ Name Blacksmith
+ TrueName "Blacksmith"
+ NameId 1061181
+ DescId 1061231
+ Desc 3
+ TopLevel true
+ Gump 5555
+ Type Profession
+ Skill Blacksmith 30
+ Skill Tinkering 30
+ Skill Mining 30
+ Skill Tailoring 30
+ Stat Str 60
+ Stat Dex 15
+ Stat Int 15
+End
\ No newline at end of file
diff --git a/Distribution/Data/Professions/SE/Prof.txt b/Distribution/Data/Professions/SE/Prof.txt
new file mode 100644
index 000000000..dc01538c0
--- /dev/null
+++ b/Distribution/Data/Professions/SE/Prof.txt
@@ -0,0 +1,118 @@
+Begin
+ Name Samurai
+ TrueName "Samurai"
+ NameId 1062948
+ DescId 1062950
+ Desc 6
+ TopLevel true
+ Gump 5591
+ Type Profession
+ Skill Bushido 50
+ Skill Swordsmanship 50
+ Skill Wrestling 0
+ Stat Str 40
+ Stat Dex 30
+ Stat Int 10
+End
+
+Begin
+ Name Ninja
+ TrueName "Ninja"
+ NameId 1062949
+ DescId 1062951
+ Desc 7
+ TopLevel true
+ Gump 5589
+ Type Profession
+ Skill Ninjitsu 50
+ Skill Hiding 50
+ Skill Wrestling 0
+ Stat Str 40
+ Stat Dex 30
+ Stat Int 10
+End
+
+Begin
+ Name Paladin
+ TrueName "Paladin"
+ NameId 1061177
+ DescId 1061227
+ Desc 5
+ TopLevel true
+ Gump 5587
+ Type Profession
+ Skill Chivalry 50
+ Skill Tactics 50
+ Skill Wrestling 0
+ Stat Str 45
+ Stat Dex 20
+ Stat Int 15
+End
+
+Begin
+ Name Necromancer
+ TrueName "Necromancer"
+ NameId 1061178
+ DescId 1061228
+ Desc 4
+ TopLevel true
+ Gump 5557
+ Type Profession
+ Skill Necromancy 50
+ Skill Swordsmanship 30
+ Skill Tactics 20
+ Stat Str 25
+ Stat Dex 20
+ Stat Int 35
+End
+
+Begin
+ Name Warrior
+ TrueName "Warrior"
+ NameId 1061180
+ DescId 1061230
+ Desc 1
+ TopLevel true
+ Gump 5577
+ Type Profession
+ Skill Tactics 50
+ Skill Healing 45
+ Skill Swordsmanship 5
+ Stat Str 35
+ Stat Dex 35
+ Stat Int 10
+End
+
+Begin
+ Name Mage
+ TrueName "Mage"
+ NameId 1061179
+ DescId 1061229
+ Desc 2
+ TopLevel true
+ Gump 5569
+ Type Profession
+ Skill Magery 50
+ Skill Meditation 50
+ Skill Wrestling 0
+ Stat Str 25
+ Stat Dex 10
+ Stat Int 45
+End
+
+Begin
+ Name Blacksmith
+ TrueName "Blacksmith"
+ NameId 1061181
+ DescId 1061231
+ Desc 3
+ TopLevel true
+ Gump 5555
+ Type Profession
+ Skill Blacksmith 50
+ Skill Tinkering 45
+ Skill Mining 5
+ Stat Str 60
+ Stat Dex 10
+ Stat Int 10
+End
\ No newline at end of file
diff --git a/Distribution/Data/Professions/all-profession-skill-names.txt b/Distribution/Data/Professions/all-profession-skill-names.txt
new file mode 100644
index 000000000..57f93a935
--- /dev/null
+++ b/Distribution/Data/Professions/all-profession-skill-names.txt
@@ -0,0 +1,58 @@
+Alchemy,
+Anatomy,
+AnimalLore,
+ItemID,
+ArmsLore,
+Parrying,
+Begging,
+Blacksmith,
+Bowcraft,
+Peacemaking,
+Camping,
+Carpentry,
+Cartography,
+Cooking,
+DetectHidden,
+Enticement,
+EvaluateIntelligence,
+Healing,
+Fishing,
+ForensicEvaluation,
+Herding,
+Hiding,
+Provocation,
+Inscription,
+Lockpicking,
+Magery,
+ResistingSpells,
+Tactics,
+Snooping,
+Musicianship,
+Poisoning,
+Archery,
+SpiritSpeak,
+Stealing,
+Tailoring,
+AnimalTaming,
+TasteIdentification,
+Tinkering,
+Tracking,
+Veterinary,
+Swordsmanship,
+MaceFighting,
+Fencing,
+Wrestling,
+Lumberjacking,
+Mining,
+Meditation,
+Stealth,
+Disarm,
+Necromancy,
+Focus,
+Chivalry,
+Bushido,
+Ninjitsu,
+Spellweaving,
+Mysticism,
+Imbuing,
+Throwing
diff --git a/Distribution/Data/expansion.json b/Distribution/Data/expansion.json
new file mode 100644
index 000000000..e0b036f61
--- /dev/null
+++ b/Distribution/Data/expansion.json
@@ -0,0 +1,794 @@
+[
+ {
+ "Name": "None",
+ "ClientVersion": null,
+ "ClientFlags": null,
+ "FeatureFlags": {
+ "None": true,
+ "T2A": false,
+ "UOR": false,
+ "UOTD": false,
+ "LBR": false,
+ "AOS": false,
+ "SixthCharacterSlot": false,
+ "SE": false,
+ "ML": false,
+ "EigthAge": false,
+ "NinthAge": false,
+ "TenthAge": false,
+ "IncreasedStorage": false,
+ "SeventhCharacterSlot": false,
+ "RoleplayFaces": false,
+ "TrialAccount": false,
+ "LiveAccount": false,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ },
+ "CharacterListFlags": {
+ "None": true,
+ "Unk1": false,
+ "OverwriteConfigButton": false,
+ "OneCharacterSlot": false,
+ "ContextMenus": false,
+ "SlotLimit": false,
+ "AOS": false,
+ "SixthCharacterSlot": false,
+ "SE": false,
+ "ML": false,
+ "Unk2": false,
+ "UO3DClientType": false,
+ "Unk3": false,
+ "SeventhCharacterSlot": false,
+ "Unk4": false,
+ "NewMovementSystem": false,
+ "NewFeluccaAreas": false
+ },
+ "HousingFlags": {
+ "None": true,
+ "AOS": false,
+ "SE": false,
+ "ML": false,
+ "Crystal": false,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ }
+ },
+ {
+ "Name": "The Second Age",
+ "ClientVersion": null,
+ "ClientFlags": "Felucca",
+ "FeatureFlags": {
+ "None": false,
+ "T2A": true,
+ "UOR": false,
+ "UOTD": false,
+ "LBR": false,
+ "AOS": false,
+ "SixthCharacterSlot": false,
+ "SE": false,
+ "ML": false,
+ "EigthAge": false,
+ "NinthAge": false,
+ "TenthAge": false,
+ "IncreasedStorage": false,
+ "SeventhCharacterSlot": false,
+ "RoleplayFaces": false,
+ "TrialAccount": false,
+ "LiveAccount": false,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ },
+ "CharacterListFlags": {
+ "None": false,
+ "Unk1": false,
+ "OverwriteConfigButton": false,
+ "OneCharacterSlot": false,
+ "ContextMenus": true,
+ "SlotLimit": false,
+ "AOS": false,
+ "SixthCharacterSlot": false,
+ "SE": false,
+ "ML": false,
+ "Unk2": false,
+ "UO3DClientType": false,
+ "Unk3": false,
+ "SeventhCharacterSlot": false,
+ "Unk4": false,
+ "NewMovementSystem": false,
+ "NewFeluccaAreas": false
+ },
+ "HousingFlags": {
+ "None": true,
+ "AOS": false,
+ "SE": false,
+ "ML": false,
+ "Crystal": false,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ }
+ },
+ {
+ "Name": "Renaissance",
+ "ClientVersion": null,
+ "ClientFlags": "Trammel",
+ "FeatureFlags": {
+ "None": false,
+ "T2A": true,
+ "UOR": true,
+ "UOTD": false,
+ "LBR": false,
+ "AOS": false,
+ "SixthCharacterSlot": false,
+ "SE": false,
+ "ML": false,
+ "EigthAge": false,
+ "NinthAge": false,
+ "TenthAge": false,
+ "IncreasedStorage": false,
+ "SeventhCharacterSlot": false,
+ "RoleplayFaces": false,
+ "TrialAccount": false,
+ "LiveAccount": false,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ },
+ "CharacterListFlags": {
+ "None": false,
+ "Unk1": false,
+ "OverwriteConfigButton": false,
+ "OneCharacterSlot": false,
+ "ContextMenus": true,
+ "SlotLimit": false,
+ "AOS": false,
+ "SixthCharacterSlot": false,
+ "SE": false,
+ "ML": false,
+ "Unk2": false,
+ "UO3DClientType": false,
+ "Unk3": false,
+ "SeventhCharacterSlot": false,
+ "Unk4": false,
+ "NewMovementSystem": false,
+ "NewFeluccaAreas": false
+ },
+ "HousingFlags": {
+ "None": true,
+ "AOS": false,
+ "SE": false,
+ "ML": false,
+ "Crystal": false,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ }
+ },
+ {
+ "Name": "Third Dawn",
+ "ClientVersion": null,
+ "ClientFlags": "Ilshenar",
+ "FeatureFlags": {
+ "None": false,
+ "T2A": true,
+ "UOR": true,
+ "UOTD": true,
+ "LBR": false,
+ "AOS": false,
+ "SixthCharacterSlot": false,
+ "SE": false,
+ "ML": false,
+ "EigthAge": false,
+ "NinthAge": false,
+ "TenthAge": false,
+ "IncreasedStorage": false,
+ "SeventhCharacterSlot": false,
+ "RoleplayFaces": false,
+ "TrialAccount": false,
+ "LiveAccount": false,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ },
+ "CharacterListFlags": {
+ "None": false,
+ "Unk1": false,
+ "OverwriteConfigButton": false,
+ "OneCharacterSlot": false,
+ "ContextMenus": true,
+ "SlotLimit": false,
+ "AOS": false,
+ "SixthCharacterSlot": false,
+ "SE": false,
+ "ML": false,
+ "Unk2": false,
+ "UO3DClientType": false,
+ "Unk3": false,
+ "SeventhCharacterSlot": false,
+ "Unk4": false,
+ "NewMovementSystem": false,
+ "NewFeluccaAreas": false
+ },
+ "HousingFlags": {
+ "None": true,
+ "AOS": false,
+ "SE": false,
+ "ML": false,
+ "Crystal": false,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ }
+ },
+ {
+ "Name": "Blackthorn's Revenge",
+ "ClientVersion": null,
+ "ClientFlags": "Ilshenar",
+ "FeatureFlags": {
+ "None": false,
+ "T2A": true,
+ "UOR": true,
+ "UOTD": true,
+ "LBR": true,
+ "AOS": false,
+ "SixthCharacterSlot": false,
+ "SE": false,
+ "ML": false,
+ "EigthAge": false,
+ "NinthAge": false,
+ "TenthAge": false,
+ "IncreasedStorage": false,
+ "SeventhCharacterSlot": false,
+ "RoleplayFaces": false,
+ "TrialAccount": false,
+ "LiveAccount": false,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ },
+ "CharacterListFlags": {
+ "None": false,
+ "Unk1": false,
+ "OverwriteConfigButton": false,
+ "OneCharacterSlot": false,
+ "ContextMenus": true,
+ "SlotLimit": false,
+ "AOS": false,
+ "SixthCharacterSlot": false,
+ "SE": false,
+ "ML": false,
+ "Unk2": false,
+ "UO3DClientType": false,
+ "Unk3": false,
+ "SeventhCharacterSlot": false,
+ "Unk4": false,
+ "NewMovementSystem": false,
+ "NewFeluccaAreas": false
+ },
+ "HousingFlags": {
+ "None": true,
+ "AOS": false,
+ "SE": false,
+ "ML": false,
+ "Crystal": false,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ }
+ },
+ {
+ "Name": "Age of Shadows",
+ "ClientVersion": null,
+ "ClientFlags": "Malas",
+ "FeatureFlags": {
+ "None": false,
+ "T2A": true,
+ "UOR": true,
+ "UOTD": true,
+ "LBR": true,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": false,
+ "ML": false,
+ "EigthAge": false,
+ "NinthAge": false,
+ "TenthAge": false,
+ "IncreasedStorage": false,
+ "SeventhCharacterSlot": false,
+ "RoleplayFaces": false,
+ "TrialAccount": false,
+ "LiveAccount": true,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ },
+ "CharacterListFlags": {
+ "None": false,
+ "Unk1": false,
+ "OverwriteConfigButton": false,
+ "OneCharacterSlot": false,
+ "ContextMenus": true,
+ "SlotLimit": false,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": false,
+ "ML": false,
+ "Unk2": false,
+ "UO3DClientType": false,
+ "Unk3": false,
+ "SeventhCharacterSlot": false,
+ "Unk4": false,
+ "NewMovementSystem": false,
+ "NewFeluccaAreas": false
+ },
+ "HousingFlags": {
+ "None": false,
+ "AOS": true,
+ "SE": false,
+ "ML": false,
+ "Crystal": false,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ }
+ },
+ {
+ "Name": "Samurai Empire",
+ "ClientVersion": null,
+ "ClientFlags": "Tokuno",
+ "FeatureFlags": {
+ "None": false,
+ "T2A": true,
+ "UOR": true,
+ "UOTD": true,
+ "LBR": true,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": true,
+ "ML": false,
+ "EigthAge": false,
+ "NinthAge": false,
+ "TenthAge": false,
+ "IncreasedStorage": false,
+ "SeventhCharacterSlot": false,
+ "RoleplayFaces": false,
+ "TrialAccount": false,
+ "LiveAccount": true,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ },
+ "CharacterListFlags": {
+ "None": false,
+ "Unk1": false,
+ "OverwriteConfigButton": false,
+ "OneCharacterSlot": false,
+ "ContextMenus": true,
+ "SlotLimit": false,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": true,
+ "ML": false,
+ "Unk2": false,
+ "UO3DClientType": false,
+ "Unk3": false,
+ "SeventhCharacterSlot": false,
+ "Unk4": false,
+ "NewMovementSystem": false,
+ "NewFeluccaAreas": false
+ },
+ "HousingFlags": {
+ "None": false,
+ "AOS": true,
+ "SE": true,
+ "ML": false,
+ "Crystal": false,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ }
+ },
+ {
+ "Name": "Mondain's Legacy",
+ "ClientVersion": "5.0.0a",
+ "ClientFlags": null,
+ "FeatureFlags": {
+ "None": false,
+ "T2A": true,
+ "UOR": true,
+ "UOTD": true,
+ "LBR": true,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": true,
+ "ML": true,
+ "EigthAge": false,
+ "NinthAge": true,
+ "TenthAge": false,
+ "IncreasedStorage": false,
+ "SeventhCharacterSlot": false,
+ "RoleplayFaces": false,
+ "TrialAccount": false,
+ "LiveAccount": true,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ },
+ "CharacterListFlags": {
+ "None": false,
+ "Unk1": false,
+ "OverwriteConfigButton": false,
+ "OneCharacterSlot": false,
+ "ContextMenus": true,
+ "SlotLimit": false,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": true,
+ "ML": true,
+ "Unk2": false,
+ "UO3DClientType": false,
+ "Unk3": false,
+ "SeventhCharacterSlot": false,
+ "Unk4": false,
+ "NewMovementSystem": false,
+ "NewFeluccaAreas": false
+ },
+ "HousingFlags": {
+ "None": false,
+ "AOS": true,
+ "SE": true,
+ "ML": true,
+ "Crystal": true,
+ "SA": false,
+ "HS": false,
+ "Gothic": false,
+ "Rustic": false,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ }
+ },
+ {
+ "Name": "Stygian Abyss",
+ "ClientVersion": null,
+ "ClientFlags": "TerMur",
+ "FeatureFlags": {
+ "None": false,
+ "T2A": true,
+ "UOR": true,
+ "UOTD": true,
+ "LBR": true,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": true,
+ "ML": true,
+ "EigthAge": false,
+ "NinthAge": true,
+ "TenthAge": false,
+ "IncreasedStorage": false,
+ "SeventhCharacterSlot": false,
+ "RoleplayFaces": false,
+ "TrialAccount": false,
+ "LiveAccount": true,
+ "SA": true,
+ "HS": false,
+ "Gothic": true,
+ "Rustic": true,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ },
+ "CharacterListFlags": {
+ "None": false,
+ "Unk1": false,
+ "OverwriteConfigButton": false,
+ "OneCharacterSlot": false,
+ "ContextMenus": true,
+ "SlotLimit": false,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": true,
+ "ML": true,
+ "Unk2": false,
+ "UO3DClientType": false,
+ "Unk3": false,
+ "SeventhCharacterSlot": false,
+ "Unk4": false,
+ "NewMovementSystem": false,
+ "NewFeluccaAreas": false
+ },
+ "HousingFlags": {
+ "None": false,
+ "AOS": true,
+ "SE": true,
+ "ML": true,
+ "Crystal": true,
+ "SA": true,
+ "HS": false,
+ "Gothic": true,
+ "Rustic": true,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ }
+ },
+ {
+ "Name": "High Seas",
+ "ClientVersion": "7.0.9.0",
+ "ClientFlags": null,
+ "FeatureFlags": {
+ "None": false,
+ "T2A": true,
+ "UOR": true,
+ "UOTD": true,
+ "LBR": true,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": true,
+ "ML": true,
+ "EigthAge": false,
+ "NinthAge": true,
+ "TenthAge": false,
+ "IncreasedStorage": false,
+ "SeventhCharacterSlot": false,
+ "RoleplayFaces": false,
+ "TrialAccount": false,
+ "LiveAccount": true,
+ "SA": true,
+ "HS": true,
+ "Gothic": true,
+ "Rustic": true,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ },
+ "CharacterListFlags": {
+ "None": false,
+ "Unk1": false,
+ "OverwriteConfigButton": false,
+ "OneCharacterSlot": false,
+ "ContextMenus": true,
+ "SlotLimit": false,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": true,
+ "ML": true,
+ "Unk2": false,
+ "UO3DClientType": false,
+ "Unk3": false,
+ "SeventhCharacterSlot": false,
+ "Unk4": false,
+ "NewMovementSystem": false,
+ "NewFeluccaAreas": false
+ },
+ "HousingFlags": {
+ "None": false,
+ "AOS": true,
+ "SE": true,
+ "ML": true,
+ "Crystal": true,
+ "SA": true,
+ "HS": true,
+ "Gothic": true,
+ "Rustic": true,
+ "Jungle": false,
+ "Shadowguard": false,
+ "TOL": false,
+ "EJ": false
+ }
+ },
+ {
+ "Name": "Time of Legends",
+ "ClientVersion": "7.0.45.65",
+ "ClientFlags": null,
+ "FeatureFlags": {
+ "None": false,
+ "T2A": true,
+ "UOR": true,
+ "UOTD": true,
+ "LBR": true,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": true,
+ "ML": true,
+ "EigthAge": false,
+ "NinthAge": true,
+ "TenthAge": false,
+ "IncreasedStorage": false,
+ "SeventhCharacterSlot": false,
+ "RoleplayFaces": false,
+ "TrialAccount": false,
+ "LiveAccount": true,
+ "SA": true,
+ "HS": true,
+ "Gothic": true,
+ "Rustic": true,
+ "Jungle": true,
+ "Shadowguard": true,
+ "TOL": true,
+ "EJ": false
+ },
+ "CharacterListFlags": {
+ "None": false,
+ "Unk1": false,
+ "OverwriteConfigButton": false,
+ "OneCharacterSlot": false,
+ "ContextMenus": true,
+ "SlotLimit": false,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": true,
+ "ML": true,
+ "Unk2": false,
+ "UO3DClientType": false,
+ "Unk3": false,
+ "SeventhCharacterSlot": false,
+ "Unk4": false,
+ "NewMovementSystem": false,
+ "NewFeluccaAreas": false
+ },
+ "HousingFlags": {
+ "None": false,
+ "AOS": true,
+ "SE": true,
+ "ML": true,
+ "Crystal": true,
+ "SA": true,
+ "HS": true,
+ "Gothic": true,
+ "Rustic": true,
+ "Jungle": true,
+ "Shadowguard": true,
+ "TOL": true,
+ "EJ": false
+ }
+ },
+ {
+ "Name": "Endless Journey",
+ "ClientVersion": "7.0.61.0",
+ "ClientFlags": null,
+ "FeatureFlags": {
+ "None": false,
+ "T2A": true,
+ "UOR": true,
+ "UOTD": true,
+ "LBR": true,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": true,
+ "ML": true,
+ "EigthAge": false,
+ "NinthAge": true,
+ "TenthAge": false,
+ "IncreasedStorage": false,
+ "SeventhCharacterSlot": false,
+ "RoleplayFaces": false,
+ "TrialAccount": false,
+ "LiveAccount": true,
+ "SA": true,
+ "HS": true,
+ "Gothic": true,
+ "Rustic": true,
+ "Jungle": true,
+ "Shadowguard": true,
+ "TOL": true,
+ "EJ": true
+ },
+ "CharacterListFlags": {
+ "None": false,
+ "Unk1": false,
+ "OverwriteConfigButton": false,
+ "OneCharacterSlot": false,
+ "ContextMenus": true,
+ "SlotLimit": false,
+ "AOS": true,
+ "SixthCharacterSlot": false,
+ "SE": true,
+ "ML": true,
+ "Unk2": false,
+ "UO3DClientType": false,
+ "Unk3": false,
+ "SeventhCharacterSlot": false,
+ "Unk4": false,
+ "NewMovementSystem": false,
+ "NewFeluccaAreas": false
+ },
+ "HousingFlags": {
+ "None": false,
+ "AOS": true,
+ "SE": true,
+ "ML": true,
+ "Crystal": true,
+ "SA": true,
+ "HS": true,
+ "Gothic": true,
+ "Rustic": true,
+ "Jungle": true,
+ "Shadowguard": true,
+ "TOL": true,
+ "EJ": true
+ }
+ }
+]
diff --git a/Distribution/Data/timezones.json b/Distribution/Data/timezones.json
new file mode 100644
index 000000000..e61680c1e
--- /dev/null
+++ b/Distribution/Data/timezones.json
@@ -0,0 +1,1157 @@
+/* Generated from https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml */
+[
+ {
+ "timezone": "Dateline Standard Time",
+ "regions": [
+ "Etc/GMT+12"
+ ]
+ },
+ {
+ "timezone": "UTC-11",
+ "regions": [
+ "Etc/GMT+11",
+ "Pacific/Pago_Pago",
+ "Pacific/Niue",
+ "Pacific/Midway"
+ ]
+ },
+ {
+ "timezone": "Aleutian Standard Time",
+ "regions": [
+ "America/Adak"
+ ]
+ },
+ {
+ "timezone": "Hawaiian Standard Time",
+ "regions": [
+ "Pacific/Honolulu",
+ "Pacific/Rarotonga",
+ "Pacific/Tahiti",
+ "Pacific/Johnston",
+ "Etc/GMT+10"
+ ]
+ },
+ {
+ "timezone": "Marquesas Standard Time",
+ "regions": [
+ "Pacific/Marquesas"
+ ]
+ },
+ {
+ "timezone": "Alaskan Standard Time",
+ "regions": [
+ "America/Anchorage",
+ "America/Juneau",
+ "America/Metlakatla",
+ "America/Nome",
+ "America/Sitka",
+ "America/Yakutat"
+ ]
+ },
+ {
+ "timezone": "UTC-09",
+ "regions": [
+ "Etc/GMT+9",
+ "Pacific/Gambier"
+ ]
+ },
+ {
+ "timezone": "Pacific Standard Time (Mexico)",
+ "regions": [
+ "America/Tijuana",
+ "America/Santa_Isabel"
+ ]
+ },
+ {
+ "timezone": "UTC-08",
+ "regions": [
+ "Etc/GMT+8",
+ "Pacific/Pitcairn"
+ ]
+ },
+ {
+ "timezone": "Pacific Standard Time",
+ "regions": [
+ "America/Los_Angeles",
+ "America/Vancouver",
+ "PST8PDT"
+ ]
+ },
+ {
+ "timezone": "US Mountain Standard Time",
+ "regions": [
+ "America/Phoenix",
+ "America/Creston",
+ "America/Dawson_Creek",
+ "America/Fort_Nelson",
+ "America/Hermosillo",
+ "Etc/GMT+7"
+ ]
+ },
+ {
+ "timezone": "Mountain Standard Time (Mexico)",
+ "regions": [
+ "America/Chihuahua",
+ "America/Mazatlan"
+ ]
+ },
+ {
+ "timezone": "Mountain Standard Time",
+ "regions": [
+ "America/Denver",
+ "America/Edmonton",
+ "America/Cambridge_Bay",
+ "America/Inuvik",
+ "America/Yellowknife",
+ "America/Ojinaga",
+ "America/Boise",
+ "MST7MDT"
+ ]
+ },
+ {
+ "timezone": "Yukon Standard Time",
+ "regions": [
+ "America/Whitehorse",
+ "America/Dawson"
+ ]
+ },
+ {
+ "timezone": "Central America Standard Time",
+ "regions": [
+ "America/Guatemala",
+ "America/Belize",
+ "America/Costa_Rica",
+ "Pacific/Galapagos",
+ "America/Tegucigalpa",
+ "America/Managua",
+ "America/El_Salvador",
+ "Etc/GMT+6"
+ ]
+ },
+ {
+ "timezone": "Central Standard Time",
+ "regions": [
+ "America/Chicago",
+ "America/Winnipeg",
+ "America/Rainy_River",
+ "America/Rankin_Inlet",
+ "America/Resolute",
+ "America/Matamoros",
+ "America/Indiana/Knox",
+ "America/Indiana/Tell_City",
+ "America/Menominee",
+ "America/North_Dakota/Beulah",
+ "America/North_Dakota/Center",
+ "America/North_Dakota/New_Salem",
+ "CST6CDT"
+ ]
+ },
+ {
+ "timezone": "Easter Island Standard Time",
+ "regions": [
+ "Pacific/Easter"
+ ]
+ },
+ {
+ "timezone": "Central Standard Time (Mexico)",
+ "regions": [
+ "America/Mexico_City",
+ "America/Bahia_Banderas",
+ "America/Merida",
+ "America/Monterrey"
+ ]
+ },
+ {
+ "timezone": "Canada Central Standard Time",
+ "regions": [
+ "America/Regina",
+ "America/Swift_Current"
+ ]
+ },
+ {
+ "timezone": "SA Pacific Standard Time",
+ "regions": [
+ "America/Bogota",
+ "America/Rio_Branco",
+ "America/Eirunepe",
+ "America/Coral_Harbour",
+ "America/Guayaquil",
+ "America/Jamaica",
+ "America/Cayman",
+ "America/Panama",
+ "America/Lima",
+ "Etc/GMT+5"
+ ]
+ },
+ {
+ "timezone": "Eastern Standard Time (Mexico)",
+ "regions": [
+ "America/Cancun"
+ ]
+ },
+ {
+ "timezone": "Eastern Standard Time",
+ "regions": [
+ "America/New_York",
+ "America/Nassau",
+ "America/Toronto",
+ "America/Iqaluit",
+ "America/Montreal",
+ "America/Nipigon",
+ "America/Pangnirtung",
+ "America/Thunder_Bay",
+ "America/Detroit",
+ "America/Indiana/Petersburg",
+ "America/Indiana/Vincennes",
+ "America/Indiana/Winamac",
+ "America/Kentucky/Monticello",
+ "America/Louisville",
+ "EST5EDT"
+ ]
+ },
+ {
+ "timezone": "Haiti Standard Time",
+ "regions": [
+ "America/Port-au-Prince"
+ ]
+ },
+ {
+ "timezone": "Cuba Standard Time",
+ "regions": [
+ "America/Havana"
+ ]
+ },
+ {
+ "timezone": "US Eastern Standard Time",
+ "regions": [
+ "America/Indianapolis",
+ "America/Indiana/Marengo",
+ "America/Indiana/Vevay"
+ ]
+ },
+ {
+ "timezone": "Turks And Caicos Standard Time",
+ "regions": [
+ "America/Grand_Turk"
+ ]
+ },
+ {
+ "timezone": "Paraguay Standard Time",
+ "regions": [
+ "America/Asuncion"
+ ]
+ },
+ {
+ "timezone": "Atlantic Standard Time",
+ "regions": [
+ "America/Halifax",
+ "Atlantic/Bermuda",
+ "America/Glace_Bay",
+ "America/Goose_Bay",
+ "America/Moncton",
+ "America/Thule"
+ ]
+ },
+ {
+ "timezone": "Venezuela Standard Time",
+ "regions": [
+ "America/Caracas"
+ ]
+ },
+ {
+ "timezone": "Central Brazilian Standard Time",
+ "regions": [
+ "America/Cuiaba",
+ "America/Campo_Grande"
+ ]
+ },
+ {
+ "timezone": "SA Western Standard Time",
+ "regions": [
+ "America/La_Paz",
+ "America/Antigua",
+ "America/Anguilla",
+ "America/Aruba",
+ "America/Barbados",
+ "America/St_Barthelemy",
+ "America/Kralendijk",
+ "America/Manaus",
+ "America/Boa_Vista",
+ "America/Porto_Velho",
+ "America/Blanc-Sablon",
+ "America/Curacao",
+ "America/Dominica",
+ "America/Santo_Domingo",
+ "America/Grenada",
+ "America/Guadeloupe",
+ "America/Guyana",
+ "America/St_Kitts",
+ "America/St_Lucia",
+ "America/Marigot",
+ "America/Martinique",
+ "America/Montserrat",
+ "America/Puerto_Rico",
+ "America/Lower_Princes",
+ "America/Port_of_Spain",
+ "America/St_Vincent",
+ "America/Tortola",
+ "America/St_Thomas",
+ "Etc/GMT+4"
+ ]
+ },
+ {
+ "timezone": "Pacific SA Standard Time",
+ "regions": [
+ "America/Santiago"
+ ]
+ },
+ {
+ "timezone": "Newfoundland Standard Time",
+ "regions": [
+ "America/St_Johns"
+ ]
+ },
+ {
+ "timezone": "Tocantins Standard Time",
+ "regions": [
+ "America/Araguaina"
+ ]
+ },
+ {
+ "timezone": "E. South America Standard Time",
+ "regions": [
+ "America/Sao_Paulo"
+ ]
+ },
+ {
+ "timezone": "SA Eastern Standard Time",
+ "regions": [
+ "America/Cayenne",
+ "Antarctica/Rothera",
+ "Antarctica/Palmer",
+ "America/Fortaleza",
+ "America/Belem",
+ "America/Maceio",
+ "America/Recife",
+ "America/Santarem",
+ "Atlantic/Stanley",
+ "America/Paramaribo",
+ "Etc/GMT+3"
+ ]
+ },
+ {
+ "timezone": "Argentina Standard Time",
+ "regions": [
+ "America/Buenos_Aires",
+ "America/Argentina/La_Rioja",
+ "America/Argentina/Rio_Gallegos",
+ "America/Argentina/Salta",
+ "America/Argentina/San_Juan",
+ "America/Argentina/San_Luis",
+ "America/Argentina/Tucuman",
+ "America/Argentina/Ushuaia",
+ "America/Catamarca",
+ "America/Cordoba",
+ "America/Jujuy",
+ "America/Mendoza"
+ ]
+ },
+ {
+ "timezone": "Greenland Standard Time",
+ "regions": [
+ "America/Godthab"
+ ]
+ },
+ {
+ "timezone": "Montevideo Standard Time",
+ "regions": [
+ "America/Montevideo"
+ ]
+ },
+ {
+ "timezone": "Magallanes Standard Time",
+ "regions": [
+ "America/Punta_Arenas"
+ ]
+ },
+ {
+ "timezone": "Saint Pierre Standard Time",
+ "regions": [
+ "America/Miquelon"
+ ]
+ },
+ {
+ "timezone": "Bahia Standard Time",
+ "regions": [
+ "America/Bahia"
+ ]
+ },
+ {
+ "timezone": "UTC-02",
+ "regions": [
+ "Etc/GMT+2",
+ "America/Noronha",
+ "Atlantic/South_Georgia"
+ ]
+ },
+ {
+ "timezone": "Azores Standard Time",
+ "regions": [
+ "Atlantic/Azores",
+ "America/Scoresbysund"
+ ]
+ },
+ {
+ "timezone": "Cape Verde Standard Time",
+ "regions": [
+ "Atlantic/Cape_Verde",
+ "Etc/GMT+1"
+ ]
+ },
+ {
+ "timezone": "UTC",
+ "regions": [
+ "Etc/UTC",
+ "Etc/GMT"
+ ]
+ },
+ {
+ "timezone": "GMT Standard Time",
+ "regions": [
+ "Europe/London",
+ "Atlantic/Canary",
+ "Atlantic/Faeroe",
+ "Europe/Guernsey",
+ "Europe/Dublin",
+ "Europe/Isle_of_Man",
+ "Europe/Jersey",
+ "Europe/Lisbon",
+ "Atlantic/Madeira"
+ ]
+ },
+ {
+ "timezone": "Greenwich Standard Time",
+ "regions": [
+ "Atlantic/Reykjavik",
+ "Africa/Ouagadougou",
+ "Africa/Abidjan",
+ "Africa/Accra",
+ "America/Danmarkshavn",
+ "Africa/Banjul",
+ "Africa/Conakry",
+ "Africa/Bissau",
+ "Africa/Monrovia",
+ "Africa/Bamako",
+ "Africa/Nouakchott",
+ "Atlantic/St_Helena",
+ "Africa/Freetown",
+ "Africa/Dakar",
+ "Africa/Lome"
+ ]
+ },
+ {
+ "timezone": "Sao Tome Standard Time",
+ "regions": [
+ "Africa/Sao_Tome"
+ ]
+ },
+ {
+ "timezone": "Morocco Standard Time",
+ "regions": [
+ "Africa/Casablanca",
+ "Africa/El_Aaiun"
+ ]
+ },
+ {
+ "timezone": "W. Europe Standard Time",
+ "regions": [
+ "Europe/Berlin",
+ "Europe/Andorra",
+ "Europe/Vienna",
+ "Europe/Zurich",
+ "Europe/Busingen",
+ "Europe/Gibraltar",
+ "Europe/Rome",
+ "Europe/Vaduz",
+ "Europe/Luxembourg",
+ "Europe/Monaco",
+ "Europe/Malta",
+ "Europe/Amsterdam",
+ "Europe/Oslo",
+ "Europe/Stockholm",
+ "Arctic/Longyearbyen",
+ "Europe/San_Marino",
+ "Europe/Vatican"
+ ]
+ },
+ {
+ "timezone": "Central Europe Standard Time",
+ "regions": [
+ "Europe/Budapest",
+ "Europe/Tirane",
+ "Europe/Prague",
+ "Europe/Podgorica",
+ "Europe/Belgrade",
+ "Europe/Ljubljana",
+ "Europe/Bratislava"
+ ]
+ },
+ {
+ "timezone": "Romance Standard Time",
+ "regions": [
+ "Europe/Paris",
+ "Europe/Brussels",
+ "Europe/Copenhagen",
+ "Europe/Madrid",
+ "Africa/Ceuta"
+ ]
+ },
+ {
+ "timezone": "Central European Standard Time",
+ "regions": [
+ "Europe/Warsaw",
+ "Europe/Sarajevo",
+ "Europe/Zagreb",
+ "Europe/Skopje"
+ ]
+ },
+ {
+ "timezone": "W. Central Africa Standard Time",
+ "regions": [
+ "Africa/Lagos",
+ "Africa/Luanda",
+ "Africa/Porto-Novo",
+ "Africa/Kinshasa",
+ "Africa/Bangui",
+ "Africa/Brazzaville",
+ "Africa/Douala",
+ "Africa/Algiers",
+ "Africa/Libreville",
+ "Africa/Malabo",
+ "Africa/Niamey",
+ "Africa/Ndjamena",
+ "Africa/Tunis",
+ "Etc/GMT-1"
+ ]
+ },
+ {
+ "timezone": "Jordan Standard Time",
+ "regions": [
+ "Asia/Amman"
+ ]
+ },
+ {
+ "timezone": "GTB Standard Time",
+ "regions": [
+ "Europe/Bucharest",
+ "Asia/Nicosia",
+ "Asia/Famagusta",
+ "Europe/Athens"
+ ]
+ },
+ {
+ "timezone": "Middle East Standard Time",
+ "regions": [
+ "Asia/Beirut"
+ ]
+ },
+ {
+ "timezone": "Egypt Standard Time",
+ "regions": [
+ "Africa/Cairo"
+ ]
+ },
+ {
+ "timezone": "E. Europe Standard Time",
+ "regions": [
+ "Europe/Chisinau"
+ ]
+ },
+ {
+ "timezone": "Syria Standard Time",
+ "regions": [
+ "Asia/Damascus"
+ ]
+ },
+ {
+ "timezone": "West Bank Standard Time",
+ "regions": [
+ "Asia/Hebron",
+ "Asia/Gaza"
+ ]
+ },
+ {
+ "timezone": "South Africa Standard Time",
+ "regions": [
+ "Africa/Johannesburg",
+ "Africa/Bujumbura",
+ "Africa/Gaborone",
+ "Africa/Lubumbashi",
+ "Africa/Maseru",
+ "Africa/Blantyre",
+ "Africa/Maputo",
+ "Africa/Kigali",
+ "Africa/Mbabane",
+ "Africa/Lusaka",
+ "Africa/Harare",
+ "Etc/GMT-2"
+ ]
+ },
+ {
+ "timezone": "FLE Standard Time",
+ "regions": [
+ "Europe/Kiev",
+ "Europe/Mariehamn",
+ "Europe/Sofia",
+ "Europe/Tallinn",
+ "Europe/Helsinki",
+ "Europe/Vilnius",
+ "Europe/Riga",
+ "Europe/Uzhgorod",
+ "Europe/Zaporozhye"
+ ]
+ },
+ {
+ "timezone": "Israel Standard Time",
+ "regions": [
+ "Asia/Jerusalem"
+ ]
+ },
+ {
+ "timezone": "South Sudan Standard Time",
+ "regions": [
+ "Africa/Juba"
+ ]
+ },
+ {
+ "timezone": "Kaliningrad Standard Time",
+ "regions": [
+ "Europe/Kaliningrad"
+ ]
+ },
+ {
+ "timezone": "Sudan Standard Time",
+ "regions": [
+ "Africa/Khartoum"
+ ]
+ },
+ {
+ "timezone": "Libya Standard Time",
+ "regions": [
+ "Africa/Tripoli"
+ ]
+ },
+ {
+ "timezone": "Namibia Standard Time",
+ "regions": [
+ "Africa/Windhoek"
+ ]
+ },
+ {
+ "timezone": "Arabic Standard Time",
+ "regions": [
+ "Asia/Baghdad"
+ ]
+ },
+ {
+ "timezone": "Turkey Standard Time",
+ "regions": [
+ "Europe/Istanbul"
+ ]
+ },
+ {
+ "timezone": "Arab Standard Time",
+ "regions": [
+ "Asia/Riyadh",
+ "Asia/Bahrain",
+ "Asia/Kuwait",
+ "Asia/Qatar",
+ "Asia/Aden"
+ ]
+ },
+ {
+ "timezone": "Belarus Standard Time",
+ "regions": [
+ "Europe/Minsk"
+ ]
+ },
+ {
+ "timezone": "Russian Standard Time",
+ "regions": [
+ "Europe/Moscow",
+ "Europe/Kirov",
+ "Europe/Simferopol"
+ ]
+ },
+ {
+ "timezone": "E. Africa Standard Time",
+ "regions": [
+ "Africa/Nairobi",
+ "Antarctica/Syowa",
+ "Africa/Djibouti",
+ "Africa/Asmera",
+ "Africa/Addis_Ababa",
+ "Indian/Comoro",
+ "Indian/Antananarivo",
+ "Africa/Mogadishu",
+ "Africa/Dar_es_Salaam",
+ "Africa/Kampala",
+ "Indian/Mayotte",
+ "Etc/GMT-3"
+ ]
+ },
+ {
+ "timezone": "Iran Standard Time",
+ "regions": [
+ "Asia/Tehran"
+ ]
+ },
+ {
+ "timezone": "Arabian Standard Time",
+ "regions": [
+ "Asia/Dubai",
+ "Asia/Muscat",
+ "Etc/GMT-4"
+ ]
+ },
+ {
+ "timezone": "Astrakhan Standard Time",
+ "regions": [
+ "Europe/Astrakhan",
+ "Europe/Ulyanovsk"
+ ]
+ },
+ {
+ "timezone": "Azerbaijan Standard Time",
+ "regions": [
+ "Asia/Baku"
+ ]
+ },
+ {
+ "timezone": "Russia Time Zone 3",
+ "regions": [
+ "Europe/Samara"
+ ]
+ },
+ {
+ "timezone": "Mauritius Standard Time",
+ "regions": [
+ "Indian/Mauritius",
+ "Indian/Reunion",
+ "Indian/Mahe"
+ ]
+ },
+ {
+ "timezone": "Saratov Standard Time",
+ "regions": [
+ "Europe/Saratov"
+ ]
+ },
+ {
+ "timezone": "Georgian Standard Time",
+ "regions": [
+ "Asia/Tbilisi"
+ ]
+ },
+ {
+ "timezone": "Volgograd Standard Time",
+ "regions": [
+ "Europe/Volgograd"
+ ]
+ },
+ {
+ "timezone": "Caucasus Standard Time",
+ "regions": [
+ "Asia/Yerevan"
+ ]
+ },
+ {
+ "timezone": "Afghanistan Standard Time",
+ "regions": [
+ "Asia/Kabul"
+ ]
+ },
+ {
+ "timezone": "West Asia Standard Time",
+ "regions": [
+ "Asia/Tashkent",
+ "Antarctica/Mawson",
+ "Asia/Oral",
+ "Asia/Aqtau",
+ "Asia/Aqtobe",
+ "Asia/Atyrau",
+ "Indian/Maldives",
+ "Indian/Kerguelen",
+ "Asia/Dushanbe",
+ "Asia/Ashgabat",
+ "Asia/Samarkand",
+ "Etc/GMT-5"
+ ]
+ },
+ {
+ "timezone": "Ekaterinburg Standard Time",
+ "regions": [
+ "Asia/Yekaterinburg"
+ ]
+ },
+ {
+ "timezone": "Pakistan Standard Time",
+ "regions": [
+ "Asia/Karachi"
+ ]
+ },
+ {
+ "timezone": "Qyzylorda Standard Time",
+ "regions": [
+ "Asia/Qyzylorda"
+ ]
+ },
+ {
+ "timezone": "India Standard Time",
+ "regions": [
+ "Asia/Calcutta"
+ ]
+ },
+ {
+ "timezone": "Sri Lanka Standard Time",
+ "regions": [
+ "Asia/Colombo"
+ ]
+ },
+ {
+ "timezone": "Nepal Standard Time",
+ "regions": [
+ "Asia/Katmandu"
+ ]
+ },
+ {
+ "timezone": "Central Asia Standard Time",
+ "regions": [
+ "Asia/Almaty",
+ "Antarctica/Vostok",
+ "Asia/Urumqi",
+ "Indian/Chagos",
+ "Asia/Bishkek",
+ "Asia/Qostanay",
+ "Etc/GMT-6"
+ ]
+ },
+ {
+ "timezone": "Bangladesh Standard Time",
+ "regions": [
+ "Asia/Dhaka",
+ "Asia/Thimphu"
+ ]
+ },
+ {
+ "timezone": "Omsk Standard Time",
+ "regions": [
+ "Asia/Omsk"
+ ]
+ },
+ {
+ "timezone": "Myanmar Standard Time",
+ "regions": [
+ "Asia/Rangoon",
+ "Indian/Cocos"
+ ]
+ },
+ {
+ "timezone": "SE Asia Standard Time",
+ "regions": [
+ "Asia/Bangkok",
+ "Antarctica/Davis",
+ "Indian/Christmas",
+ "Asia/Jakarta",
+ "Asia/Pontianak",
+ "Asia/Phnom_Penh",
+ "Asia/Vientiane",
+ "Asia/Saigon",
+ "Etc/GMT-7"
+ ]
+ },
+ {
+ "timezone": "Altai Standard Time",
+ "regions": [
+ "Asia/Barnaul"
+ ]
+ },
+ {
+ "timezone": "W. Mongolia Standard Time",
+ "regions": [
+ "Asia/Hovd"
+ ]
+ },
+ {
+ "timezone": "North Asia Standard Time",
+ "regions": [
+ "Asia/Krasnoyarsk",
+ "Asia/Novokuznetsk"
+ ]
+ },
+ {
+ "timezone": "N. Central Asia Standard Time",
+ "regions": [
+ "Asia/Novosibirsk"
+ ]
+ },
+ {
+ "timezone": "Tomsk Standard Time",
+ "regions": [
+ "Asia/Tomsk"
+ ]
+ },
+ {
+ "timezone": "China Standard Time",
+ "regions": [
+ "Asia/Shanghai",
+ "Asia/Hong_Kong",
+ "Asia/Macau"
+ ]
+ },
+ {
+ "timezone": "North Asia East Standard Time",
+ "regions": [
+ "Asia/Irkutsk"
+ ]
+ },
+ {
+ "timezone": "Singapore Standard Time",
+ "regions": [
+ "Asia/Singapore",
+ "Asia/Brunei",
+ "Asia/Makassar",
+ "Asia/Kuala_Lumpur",
+ "Asia/Kuching",
+ "Asia/Manila",
+ "Etc/GMT-8"
+ ]
+ },
+ {
+ "timezone": "W. Australia Standard Time",
+ "regions": [
+ "Australia/Perth"
+ ]
+ },
+ {
+ "timezone": "Taipei Standard Time",
+ "regions": [
+ "Asia/Taipei"
+ ]
+ },
+ {
+ "timezone": "Ulaanbaatar Standard Time",
+ "regions": [
+ "Asia/Ulaanbaatar",
+ "Asia/Choibalsan"
+ ]
+ },
+ {
+ "timezone": "Aus Central W. Standard Time",
+ "regions": [
+ "Australia/Eucla"
+ ]
+ },
+ {
+ "timezone": "Transbaikal Standard Time",
+ "regions": [
+ "Asia/Chita"
+ ]
+ },
+ {
+ "timezone": "Tokyo Standard Time",
+ "regions": [
+ "Asia/Tokyo",
+ "Asia/Jayapura",
+ "Pacific/Palau",
+ "Asia/Dili",
+ "Etc/GMT-9"
+ ]
+ },
+ {
+ "timezone": "North Korea Standard Time",
+ "regions": [
+ "Asia/Pyongyang"
+ ]
+ },
+ {
+ "timezone": "Korea Standard Time",
+ "regions": [
+ "Asia/Seoul"
+ ]
+ },
+ {
+ "timezone": "Yakutsk Standard Time",
+ "regions": [
+ "Asia/Yakutsk",
+ "Asia/Khandyga"
+ ]
+ },
+ {
+ "timezone": "Cen. Australia Standard Time",
+ "regions": [
+ "Australia/Adelaide",
+ "Australia/Broken_Hill"
+ ]
+ },
+ {
+ "timezone": "AUS Central Standard Time",
+ "regions": [
+ "Australia/Darwin"
+ ]
+ },
+ {
+ "timezone": "E. Australia Standard Time",
+ "regions": [
+ "Australia/Brisbane",
+ "Australia/Lindeman"
+ ]
+ },
+ {
+ "timezone": "AUS Eastern Standard Time",
+ "regions": [
+ "Australia/Sydney",
+ "Australia/Melbourne"
+ ]
+ },
+ {
+ "timezone": "West Pacific Standard Time",
+ "regions": [
+ "Pacific/Port_Moresby",
+ "Antarctica/DumontDUrville",
+ "Pacific/Truk",
+ "Pacific/Guam",
+ "Pacific/Saipan",
+ "Etc/GMT-10"
+ ]
+ },
+ {
+ "timezone": "Tasmania Standard Time",
+ "regions": [
+ "Australia/Hobart",
+ "Australia/Currie",
+ "Antarctica/Macquarie"
+ ]
+ },
+ {
+ "timezone": "Vladivostok Standard Time",
+ "regions": [
+ "Asia/Vladivostok",
+ "Asia/Ust-Nera"
+ ]
+ },
+ {
+ "timezone": "Lord Howe Standard Time",
+ "regions": [
+ "Australia/Lord_Howe"
+ ]
+ },
+ {
+ "timezone": "Bougainville Standard Time",
+ "regions": [
+ "Pacific/Bougainville"
+ ]
+ },
+ {
+ "timezone": "Russia Time Zone 10",
+ "regions": [
+ "Asia/Srednekolymsk"
+ ]
+ },
+ {
+ "timezone": "Magadan Standard Time",
+ "regions": [
+ "Asia/Magadan"
+ ]
+ },
+ {
+ "timezone": "Norfolk Standard Time",
+ "regions": [
+ "Pacific/Norfolk"
+ ]
+ },
+ {
+ "timezone": "Sakhalin Standard Time",
+ "regions": [
+ "Asia/Sakhalin"
+ ]
+ },
+ {
+ "timezone": "Central Pacific Standard Time",
+ "regions": [
+ "Pacific/Guadalcanal",
+ "Antarctica/Casey",
+ "Pacific/Ponape",
+ "Pacific/Kosrae",
+ "Pacific/Noumea",
+ "Pacific/Efate",
+ "Etc/GMT-11"
+ ]
+ },
+ {
+ "timezone": "Russia Time Zone 11",
+ "regions": [
+ "Asia/Kamchatka",
+ "Asia/Anadyr"
+ ]
+ },
+ {
+ "timezone": "New Zealand Standard Time",
+ "regions": [
+ "Pacific/Auckland",
+ "Antarctica/McMurdo"
+ ]
+ },
+ {
+ "timezone": "UTC+12",
+ "regions": [
+ "Etc/GMT-12",
+ "Pacific/Tarawa",
+ "Pacific/Majuro",
+ "Pacific/Kwajalein",
+ "Pacific/Nauru",
+ "Pacific/Funafuti",
+ "Pacific/Wake",
+ "Pacific/Wallis"
+ ]
+ },
+ {
+ "timezone": "Fiji Standard Time",
+ "regions": [
+ "Pacific/Fiji"
+ ]
+ },
+ {
+ "timezone": "Chatham Islands Standard Time",
+ "regions": [
+ "Pacific/Chatham"
+ ]
+ },
+ {
+ "timezone": "UTC+13",
+ "regions": [
+ "Etc/GMT-13",
+ "Pacific/Enderbury",
+ "Pacific/Fakaofo"
+ ]
+ },
+ {
+ "timezone": "Tonga Standard Time",
+ "regions": [
+ "Pacific/Tongatapu"
+ ]
+ },
+ {
+ "timezone": "Samoa Standard Time",
+ "regions": [
+ "Pacific/Apia"
+ ]
+ },
+ {
+ "timezone": "Line Islands Standard Time",
+ "regions": [
+ "Pacific/Kiritimati",
+ "Etc/GMT-14"
+ ]
+ }
+]
diff --git a/FAQ.md b/FAQ.md
index 1449ae9fe..4b7d5c3e4 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -1,15 +1,21 @@
# Frequently Asked Questions
+- [Why do I have thousands of compile errors](#why-do-i-have-thousands-of-compile-errors)
- [Where do I go to run ModernUO?](#where-do-i-go-to-run-modernuo)
- [I don't see my scripts compiling, what happened?](#i-dont-see-my-scripts-compiling-what-happened)
- [Why can't I run ModernUO on a 32-bit operating system?](#why-cant-i-run-modernuo-on-a-32-bit-operating-system)
+## Why do I have thousands of compile errors?
+ModernUO uses a new technique called source generation. Most likely the compile errors are related to that.
+To resolve the compile errors make sure you are using Visual Studio 2019 v16.10+, or Rider 2021.2+ for development and the newest .NET available.
+Specifically for Visual Studio, you must build the entire solution once, restart Visual Studio entirely, and then build again.
+This is required every time code changes in the `SerializationGenerator` or `SerializationSchemaGenerator` projects.
+
## Where do I go to run ModernUO?
Everything is run from the `Distribution` directory.
This folder is portable, so it can be moved to the production server for deployments.
## I don't see my scripts compiling, what happened?
-NET Core does not support dynamic compiling. Scripts are now compiled separately and moved to the `Distribution/Assemblies` directory.
-This also allows content to be divided into `modules` that can be included by dropping in the DLL.
+.NET 5 does not support dynamic compiling newer versions of C#. Scripts are now compiled separately and moved to the `Distribution/Assemblies` directory.
## Why can't I run ModernUO on a 32-bit operating system?
-ModernUO is optimized for performance and modern hardware.
+ModernUO is optimized for performance and modern hardware. For this reason we no longer support 32-bit operating systems
diff --git a/Projects/Benchmarks/Benchmarks.csproj b/Projects/Benchmarks/Benchmarks.csproj
index cd7d4a26c..2ed8e89fd 100644
--- a/Projects/Benchmarks/Benchmarks.csproj
+++ b/Projects/Benchmarks/Benchmarks.csproj
@@ -9,7 +9,7 @@
true
-
+
diff --git a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs
index 584515369..7611d5799 100644
--- a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs
+++ b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs
@@ -190,7 +190,7 @@ namespace SerializationGenerator
) != null
) : null;
- var serializablePropertySet = new SortedSet(new SerializablePropertyComparer());
+ var serializablePropertySet = new SortedDictionary(new SerializablePropertyComparer());
foreach (var fieldOrPropertySymbol in fieldsAndProperties)
{
@@ -267,10 +267,16 @@ namespace SerializationGenerator
serializableFieldSaveFlagMethods
);
- serializablePropertySet.Add(serializableProperty);
+ serializablePropertySet.Add(serializableProperty, fieldOrPropertySymbol);
}
- var serializableProperties = serializablePropertySet.ToImmutableArray();
+ var serializableFields = serializablePropertySet.Keys.ToImmutableArray();
+ var serializableProperties = serializablePropertySet.Select(
+ kvp => kvp.Key with
+ {
+ Name = (kvp.Value as IFieldSymbol)?.GetPropertyName() ?? ((IPropertySymbol)kvp.Value).Name
+ }
+ ).ToImmutableArray();
// If we are not inheriting ISerializable, then we need to define some stuff
if (!(isOverride || embedded))
@@ -323,6 +329,7 @@ namespace SerializationGenerator
indent,
isOverride,
encodedVersion,
+ serializableFields,
serializableProperties,
serializableFieldSaveFlags
);
@@ -337,6 +344,7 @@ namespace SerializationGenerator
version,
encodedVersion,
migrations,
+ serializableFields,
serializableProperties,
parentFieldOrProperty,
serializableFieldSaveFlags
diff --git a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs
index 6a0665b9a..59048a125 100644
--- a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs
+++ b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs
@@ -33,6 +33,7 @@ namespace SerializationGenerator
int version,
bool encodedVersion,
ImmutableArray migrations,
+ ImmutableArray fields,
ImmutableArray properties,
ISymbol parentFieldOrProperty,
SortedDictionary serializableFieldSaveFlagMethodsDictionary
@@ -124,11 +125,12 @@ namespace SerializationGenerator
source.AppendLine($"{bodyIndent}var saveFlags = reader.ReadEnum();");
}
- foreach (var property in properties)
+ for (var i = 0; i < properties.Length; i++)
{
+ var field = fields[i];
+ var property = properties[i];
var rule = SerializableMigrationRulesEngine.Rules[property.Rule];
-
if (serializableFieldSaveFlagMethodsDictionary.TryGetValue(
property.Order,
out var serializableFieldSaveFlagMethods
@@ -138,7 +140,7 @@ namespace SerializationGenerator
// Special case
if (property.Type == "bool")
{
- source.AppendLine($"{bodyIndent}{property.Name} = (saveFlags & SaveFlag.{property.Name}) != 0;");
+ source.AppendLine($"{bodyIndent}{field.Name} = (saveFlags & SaveFlag.{property.Name}) != 0;");
}
else
{
@@ -146,16 +148,22 @@ namespace SerializationGenerator
rule.GenerateDeserializationMethod(
source,
innerIndent,
- property,
+ field,
parentFieldOrProperty?.Name ?? "this"
);
- (rule as IPostDeserializeMethod)?.PostDeserializeMethod(source, innerIndent, property, compilation, classSymbol);
+ (rule as IPostDeserializeMethod)?.PostDeserializeMethod(
+ source,
+ innerIndent,
+ field,
+ compilation,
+ classSymbol
+ );
if (serializableFieldSaveFlagMethods.GetFieldDefaultValue != null)
{
source.AppendLine($"{bodyIndent}}}\n{bodyIndent}else\n{bodyIndent}{{");
source.AppendLine(
- $"{bodyIndent} {property.Name} = {serializableFieldSaveFlagMethods.GetFieldDefaultValue.Name}();"
+ $"{bodyIndent} {field.Name} = {serializableFieldSaveFlagMethods.GetFieldDefaultValue.Name}();"
);
}
@@ -168,10 +176,16 @@ namespace SerializationGenerator
rule.GenerateDeserializationMethod(
source,
bodyIndent,
- property,
+ field,
parentFieldOrProperty?.Name ?? "this"
);
- (rule as IPostDeserializeMethod)?.PostDeserializeMethod(source, bodyIndent, property, compilation, classSymbol);
+ (rule as IPostDeserializeMethod)?.PostDeserializeMethod(
+ source,
+ bodyIndent,
+ field,
+ compilation,
+ classSymbol
+ );
}
}
diff --git a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs
index d41a278b4..6aff93f5e 100644
--- a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs
+++ b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs
@@ -29,6 +29,7 @@ namespace SerializationGenerator
string indent,
bool isOverride,
bool encodedVersion,
+ ImmutableArray fields,
ImmutableArray properties,
SortedDictionary serializableFieldSaveFlagMethodsDictionary
)
@@ -74,30 +75,34 @@ namespace SerializationGenerator
source.AppendLine($"{bodyIndent}writer.WriteEnum(saveFlags);");
}
- foreach (var property in properties)
+ for (var i = 0; i < properties.Length; i++)
{
+ var field = fields[i];
+ var property = properties[i];
if (serializableFieldSaveFlagMethodsDictionary.ContainsKey(property.Order))
{
// Special case
if (property.Type != "bool")
{
source.AppendLine($"\n{bodyIndent}if ((saveFlags & SaveFlag.{property.Name}) != 0)\n{bodyIndent}{{");
- SerializableMigrationRulesEngine.Rules[property.Rule].GenerateSerializationMethod(
- source,
- innerIndent,
- property
- );
+ SerializableMigrationRulesEngine.Rules[property.Rule]
+ .GenerateSerializationMethod(
+ source,
+ innerIndent,
+ field
+ );
source.AppendLine($"{bodyIndent}}}");
}
}
else
{
source.AppendLine();
- SerializableMigrationRulesEngine.Rules[property.Rule].GenerateSerializationMethod(
- source,
- bodyIndent,
- property
- );
+ SerializableMigrationRulesEngine.Rules[property.Rule]
+ .GenerateSerializationMethod(
+ source,
+ bodyIndent,
+ field
+ );
}
}
diff --git a/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs b/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs
index 37fccfe5a..15abef66a 100644
--- a/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs
+++ b/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs
@@ -64,7 +64,7 @@ namespace SerializableMigration
if (fieldOrPropertySymbol is IFieldSymbol fieldSymbol)
{
- propertyName = fieldSymbol.GetPropertyName();
+ propertyName = fieldSymbol.Name;
propertyType = fieldSymbol.Type;
}
else if (fieldOrPropertySymbol is IPropertySymbol propertySymbol)
diff --git a/Projects/SerializationGenerator/SerializationGenerator.csproj b/Projects/SerializationGenerator/SerializationGenerator.csproj
index 1d8674e5b..1839dfeb3 100755
--- a/Projects/SerializationGenerator/SerializationGenerator.csproj
+++ b/Projects/SerializationGenerator/SerializationGenerator.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/Projects/SerializationSchemaGenerator/SerializationSchemaGenerator.csproj b/Projects/SerializationSchemaGenerator/SerializationSchemaGenerator.csproj
old mode 100644
new mode 100755
index 87098f422..31e89d561
--- a/Projects/SerializationSchemaGenerator/SerializationSchemaGenerator.csproj
+++ b/Projects/SerializationSchemaGenerator/SerializationSchemaGenerator.csproj
@@ -14,8 +14,7 @@
-
-
+
+
-
diff --git a/Projects/Server.Tests/Fixtures/ServerFixture.cs b/Projects/Server.Tests/Fixtures/ServerFixture.cs
index 4b0b7cae6..231f3ad25 100644
--- a/Projects/Server.Tests/Fixtures/ServerFixture.cs
+++ b/Projects/Server.Tests/Fixtures/ServerFixture.cs
@@ -1,4 +1,5 @@
using System;
+using System.Reflection;
namespace Server.Tests
{
@@ -7,8 +8,8 @@ namespace Server.Tests
// Global setup
static ServerFixture()
{
+ Core.Assembly = Assembly.GetExecutingAssembly();
Core.LoopContext = new EventLoopContext();
-
Core.Expansion = Expansion.EJ;
// Load Configurations
diff --git a/Projects/Server.Tests/Helpers/PacketTestUtilities.cs b/Projects/Server.Tests/Helpers/PacketTestUtilities.cs
index b90c457e9..afcbc6423 100644
--- a/Projects/Server.Tests/Helpers/PacketTestUtilities.cs
+++ b/Projects/Server.Tests/Helpers/PacketTestUtilities.cs
@@ -1,7 +1,5 @@
using System;
-using System.Collections.Generic;
using System.Net.Sockets;
-using Moq;
using Server.Network;
namespace Server.Tests.Network
@@ -11,18 +9,8 @@ namespace Server.Tests.Network
public static Span Compile(this Packet p) =>
p.Compile(false, out var length).AsSpan(0, length);
- public static NetState CreateTestNetState()
- {
- var socket = new Mock();
- socket
- .Setup(s => s.SendAsync(It.IsAny>>(), SocketFlags.None))
- .ReturnsAsync(() => 0);
-
- socket
- .Setup(s => s.ReceiveAsync(It.IsAny>>(), SocketFlags.None))
- .ReturnsAsync(() => 0);
-
- return new NetState(socket.Object);
- }
+ public static NetState CreateTestNetState() => new(
+ new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
+ );
}
}
diff --git a/Projects/Server.Tests/Server.Tests.csproj b/Projects/Server.Tests/Server.Tests.csproj
index 4360b0770..b6339a9a0 100644
--- a/Projects/Server.Tests/Server.Tests.csproj
+++ b/Projects/Server.Tests/Server.Tests.csproj
@@ -3,12 +3,19 @@
false
-
+
+
+
+ Data\Professions\LBR\prof.txt
+
+
+
+
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/LightPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/LightPacketTests.cs
index 36e79adf9..a6eb94b47 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/LightPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/LightPacketTests.cs
@@ -9,7 +9,7 @@ namespace Server.Tests.Network
[Fact]
public void TestGlobalLightLevel()
{
- byte lightLevel = 5;
+ const byte lightLevel = 5;
var expected = new GlobalLightLevel(lightLevel).Compile();
var ns = PacketTestUtilities.CreateTestNetState();
diff --git a/Projects/Server/AssemblyHandler.cs b/Projects/Server/AssemblyHandler.cs
index 2cccc222a..4cb69f497 100644
--- a/Projects/Server/AssemblyHandler.cs
+++ b/Projects/Server/AssemblyHandler.cs
@@ -173,14 +173,6 @@ namespace Server
return null;
}
-
- public static string EnsureDirectory(string dir)
- {
- var path = Path.Combine(Core.BaseDirectory, dir);
- Directory.CreateDirectory(path);
-
- return path;
- }
}
public class TypeCache
diff --git a/Projects/Server/Comparers/DescendingComparer.cs b/Projects/Server/Comparers/DescendingComparer.cs
new file mode 100644
index 000000000..4a53e9f32
--- /dev/null
+++ b/Projects/Server/Comparers/DescendingComparer.cs
@@ -0,0 +1,24 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2021 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: DescendingComparer.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Collections.Generic;
+
+namespace Server
+{
+ public class DescendingComparer : IComparer where T : IComparable {
+ public int Compare(T x, T y) => y?.CompareTo(x) ?? 1;
+ }
+}
diff --git a/Projects/Server/Configuration/ServerConfiguration.cs b/Projects/Server/Configuration/ServerConfiguration.cs
index f2e785b1f..7463aadff 100644
--- a/Projects/Server/Configuration/ServerConfiguration.cs
+++ b/Projects/Server/Configuration/ServerConfiguration.cs
@@ -169,6 +169,17 @@ namespace Server
return value;
}
+ public static void SetSetting(string key, TimeSpan value) => SetSetting(key, value.ToString());
+
+ public static void SetSetting(string key, int value) => SetSetting(key, value.ToString());
+
+ public static void SetSetting(string key, long value) => SetSetting(key, value.ToString());
+
+ public static void SetSetting(string key, bool value) => SetSetting(key, value.ToString());
+
+ public static void SetSetting(string key, T value) where T : struct, Enum =>
+ SetSetting(key, value.ToString());
+
public static void SetSetting(string key, string value)
{
m_Settings.Settings[key] = value;
diff --git a/Projects/Server/EventLoopTasks.cs b/Projects/Server/EventLoopTasks.cs
index 3ead229cb..12f5ac664 100644
--- a/Projects/Server/EventLoopTasks.cs
+++ b/Projects/Server/EventLoopTasks.cs
@@ -55,7 +55,7 @@ namespace Server
evt.WaitOne();
}
- public int ExecuteTasks()
+ public void ExecuteTasks()
{
if (Thread.CurrentThread != _mainThread)
{
@@ -66,15 +66,11 @@ namespace Server
for (int i = 0; i < count; i++)
{
- if (!_queue.TryDequeue(out var a))
+ if (_queue.TryDequeue(out var a))
{
- return count;
+ a();
}
-
- a();
}
-
- return count;
}
}
}
diff --git a/Projects/Server/Events/CharacterCreatedEvent.cs b/Projects/Server/Events/CharacterCreatedEvent.cs
index b919193ea..dc089daae 100644
--- a/Projects/Server/Events/CharacterCreatedEvent.cs
+++ b/Projects/Server/Events/CharacterCreatedEvent.cs
@@ -23,9 +23,9 @@ namespace Server
public class CharacterCreatedEventArgs
{
public CharacterCreatedEventArgs(
- NetState state, IAccount a, string name, bool female, int hue, int str, int dex,
- int intel, CityInfo city, SkillNameValue[] skills, int shirtHue, int pantsHue, int hairID, int hairHue,
- int beardID, int beardHue, int profession, Race race
+ NetState state, IAccount a, string name, bool female, int hue, StatNameValue[] stats, CityInfo city,
+ SkillNameValue[] skills, int shirtHue, int pantsHue, int hairID, int hairHue, int beardID, int beardHue,
+ int profession, Race race
)
{
State = state;
@@ -33,9 +33,7 @@ namespace Server
Name = name;
Female = female;
Hue = hue;
- Str = str;
- Dex = dex;
- Int = intel;
+ Stats = stats;
City = city;
Skills = skills;
ShirtHue = shirtHue;
@@ -60,11 +58,7 @@ namespace Server
public int Hue { get; }
- public int Str { get; }
-
- public int Dex { get; }
-
- public int Int { get; }
+ public StatNameValue[] Stats { get; }
public CityInfo City { get; }
@@ -87,18 +81,8 @@ namespace Server
public Race Race { get; }
}
- public struct SkillNameValue
- {
- public SkillName Name { get; }
-
- public int Value { get; }
-
- public SkillNameValue(SkillName name, int value)
- {
- Name = name;
- Value = value;
- }
- }
+ public readonly record struct SkillNameValue(SkillName Name, int Value);
+ public readonly record struct StatNameValue(StatType Name, int Value);
public static partial class EventSink
{
diff --git a/Projects/Server/ExpansionInfo.cs b/Projects/Server/ExpansionInfo.cs
index 4ed98bea1..18407c052 100644
--- a/Projects/Server/ExpansionInfo.cs
+++ b/Projects/Server/ExpansionInfo.cs
@@ -1,4 +1,23 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2021 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: ExpansionInfo.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json.Serialization;
+using Server.Json;
namespace Server
{
@@ -139,107 +158,63 @@ namespace Server
public class ExpansionInfo
{
+ public static string GetEraFolder(string parentDirectory)
+ {
+ var expansion = Core.Expansion;
+ var folders = Directory.GetDirectories(
+ parentDirectory,
+ "*",
+ new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }
+ );
+
+ while (expansion-- >= 0)
+ {
+ foreach (var folder in folders)
+ {
+ var di = new DirectoryInfo(folder);
+ if (di.Name.InsensitiveEquals(expansion.ToString()))
+ {
+ return folder;
+ }
+ }
+ }
+
+ return null;
+ }
+
static ExpansionInfo()
{
- Table = new[]
+ var path = Path.Combine(Core.BaseDirectory, "Data/expansion.json");
+ var expansions = JsonConfig.Deserialize>(path);
+
+ Table = new ExpansionInfo[expansions.Count];
+
+ for (var i = 0; i < expansions.Count; i++)
{
- new ExpansionInfo(
- 0,
- "None",
- ClientFlags.None,
- FeatureFlags.ExpansionNone,
- CharacterListFlags.ExpansionNone,
- HousingFlags.None
- ),
- new ExpansionInfo(
- 1,
- "The Second Age",
- ClientFlags.Felucca,
- FeatureFlags.ExpansionT2A,
- CharacterListFlags.ExpansionT2A,
- HousingFlags.None
- ),
- new ExpansionInfo(
- 2,
- "Renaissance",
- ClientFlags.Trammel,
- FeatureFlags.ExpansionUOR,
- CharacterListFlags.ExpansionUOR,
- HousingFlags.None
- ),
- new ExpansionInfo(
- 3,
- "Third Dawn",
- ClientFlags.Ilshenar,
- FeatureFlags.ExpansionUOTD,
- CharacterListFlags.ExpansionUOTD,
- HousingFlags.None
- ),
- new ExpansionInfo(
- 4,
- "Blackthorn's Revenge",
- ClientFlags.Ilshenar,
- FeatureFlags.ExpansionLBR,
- CharacterListFlags.ExpansionLBR,
- HousingFlags.None
- ),
- new ExpansionInfo(
- 5,
- "Age of Shadows",
- ClientFlags.Malas,
- FeatureFlags.ExpansionAOS,
- CharacterListFlags.ExpansionAOS,
- HousingFlags.HousingAOS
- ),
- new ExpansionInfo(
- 6,
- "Samurai Empire",
- ClientFlags.Tokuno,
- FeatureFlags.ExpansionSE,
- CharacterListFlags.ExpansionSE,
- HousingFlags.HousingSE
- ),
- new ExpansionInfo(
- 7,
- "Mondain's Legacy",
- new ClientVersion("5.0.0a"),
- FeatureFlags.ExpansionML,
- CharacterListFlags.ExpansionML,
- HousingFlags.HousingML
- ),
- new ExpansionInfo(
- 8,
- "Stygian Abyss",
- ClientFlags.TerMur,
- FeatureFlags.ExpansionSA,
- CharacterListFlags.ExpansionSA,
- HousingFlags.HousingSA
- ),
- new ExpansionInfo(
- 9,
- "High Seas",
- new ClientVersion("7.0.9.0"),
- FeatureFlags.ExpansionHS,
- CharacterListFlags.ExpansionHS,
- HousingFlags.HousingHS
- ),
- new ExpansionInfo(
- 10,
- "Time of Legends",
- new ClientVersion("7.0.45.65"),
- FeatureFlags.ExpansionTOL,
- CharacterListFlags.ExpansionTOL,
- HousingFlags.HousingTOL
- ),
- new ExpansionInfo(
- 11,
- "Endless Journey",
- new ClientVersion("7.0.61.0"),
- FeatureFlags.ExpansionEJ,
- CharacterListFlags.ExpansionEJ,
- HousingFlags.HousingEJ
- )
- };
+ var expansion = expansions[i];
+ if (expansion.ClientVersion != null)
+ {
+ Table[i] = new ExpansionInfo(
+ i,
+ expansion.Name,
+ expansion.ClientVersion,
+ expansion.FeatureFlags,
+ expansion.CharacterListFlags,
+ expansion.HousingFlags
+ );
+ }
+ else
+ {
+ Table[i] = new ExpansionInfo(
+ i,
+ expansion.Name,
+ expansion.ClientFlags ?? ClientFlags.None,
+ expansion.FeatureFlags,
+ expansion.CharacterListFlags,
+ expansion.HousingFlags
+ );
+ }
+ }
}
public ExpansionInfo(
@@ -331,4 +306,22 @@ namespace Server
public override string ToString() => Name;
}
+
+ public record ExpansionConfig
+ {
+ public string Name { get; init; }
+
+ public ClientVersion? ClientVersion { get; init; }
+
+ public ClientFlags? ClientFlags { get; init; }
+
+ [JsonConverter(typeof(FlagsConverter))]
+ public FeatureFlags FeatureFlags { get; init; }
+
+ [JsonConverter(typeof(FlagsConverter))]
+ public CharacterListFlags CharacterListFlags { get; init; }
+
+ [JsonConverter(typeof(FlagsConverter))]
+ public HousingFlags HousingFlags { get; init; }
+ }
}
diff --git a/Projects/Server/Guild.cs b/Projects/Server/Guild.cs
index e9e6cc84a..5ff38abd9 100644
--- a/Projects/Server/Guild.cs
+++ b/Projects/Server/Guild.cs
@@ -63,13 +63,21 @@ namespace Server.Guilds
[CommandProperty(AccessLevel.Counselor)]
public Serial Serial { get; }
+ [CommandProperty(AccessLevel.GameMaster, readOnly: true)]
+ public DateTime Created { get; set; } = Core.Now;
+
+ [CommandProperty(AccessLevel.GameMaster)]
+ DateTime ISerializable.LastSerialized { get; set; } = Core.Now;
+
long ISerializable.SavePosition { get; set; } = -1;
BufferWriter ISerializable.SaveBuffer { get; set; }
public int TypeRef { get; private set; }
+ public abstract void BeforeSerialize();
public abstract void Serialize(IGenericWriter writer);
+
public abstract void Deserialize(IGenericReader reader);
public abstract void OnDelete(Mobile mob);
diff --git a/Projects/Server/IEntity.cs b/Projects/Server/IEntity.cs
index 1d2dbcf62..f5b36b0d9 100644
--- a/Projects/Server/IEntity.cs
+++ b/Projects/Server/IEntity.cs
@@ -47,6 +47,10 @@ namespace Server
{
}
+ DateTime ISerializable.Created { get; set; } = Core.Now;
+
+ DateTime ISerializable.LastSerialized { get; set; } = DateTime.MaxValue;
+
long ISerializable.SavePosition { get; set; } = -1;
BufferWriter ISerializable.SaveBuffer { get; set; }
@@ -103,6 +107,10 @@ namespace Server
&& p.Y >= Location.m_Y - range
&& p.Y <= Location.m_Y + range;
+ public void BeforeSerialize()
+ {
+ }
+
public void Deserialize(IGenericReader reader)
{
// Should not actually be saved
diff --git a/Projects/Server/Items/Container.cs b/Projects/Server/Items/Container.cs
index f0dd8e630..43a2ba939 100644
--- a/Projects/Server/Items/Container.cs
+++ b/Projects/Server/Items/Container.cs
@@ -763,7 +763,7 @@ namespace Server.Items
{
if (Core.ML)
{
- if (ParentsContain()) // Root Parent is the Mobile. Parent could be another containter.
+ if (ParentsContain()) // Root Parent is the Mobile. Parent could be another container.
{
list.Add(
1073841, // Contents: ~1_COUNT~/~2_MAXCOUNT~ items, ~3_WEIGHT~ stones
diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs
index cfb7e14b8..0e49d5568 100644
--- a/Projects/Server/Items/Item.cs
+++ b/Projects/Server/Items/Item.cs
@@ -771,6 +771,12 @@ namespace Server
AddNameProperties(list);
}
+ [CommandProperty(AccessLevel.GameMaster, readOnly: true)]
+ public DateTime Created { get; set; } = Core.Now;
+
+ [CommandProperty(AccessLevel.GameMaster)]
+ DateTime ISerializable.LastSerialized { get; set; } = Core.Now;
+
long ISerializable.SavePosition { get; set; } = -1;
BufferWriter ISerializable.SaveBuffer { get; set; }
@@ -790,12 +796,12 @@ namespace Server
if (x != 0 || y != 0 || z != 0)
{
- if (x >= short.MinValue && x <= short.MaxValue && y >= short.MinValue && y <= short.MaxValue &&
- z >= sbyte.MinValue && z <= sbyte.MaxValue)
+ if (x is >= short.MinValue and <= short.MaxValue && y is >= short.MinValue and <= short.MaxValue &&
+ z is >= sbyte.MinValue and <= sbyte.MaxValue)
{
if (x != 0 || y != 0)
{
- if (x >= byte.MinValue && x <= byte.MaxValue && y >= byte.MinValue && y <= byte.MaxValue)
+ if (x is >= byte.MinValue and <= byte.MaxValue && y is >= byte.MinValue and <= byte.MaxValue)
{
flags |= SaveFlag.LocationByteXY;
}
@@ -1285,7 +1291,7 @@ namespace Server
var worldLoc = GetWorldLocation();
var update = (flags & ItemDelta.Update) != 0;
- if (update && m_Parent is Container contParent && !contParent.IsPublicContainer)
+ if (update && m_Parent is Container { IsPublicContainer: false } contParent)
{
var rootParent = contParent.RootParent as Mobile;
Mobile tradeRecip = null;
@@ -1332,39 +1338,36 @@ namespace Server
if (openers != null)
{
- lock (openers)
+ for (var i = 0; i < openers.Count; ++i)
{
- for (var i = 0; i < openers.Count; ++i)
+ var mob = openers[i];
+
+ var range = GetUpdateRange(mob);
+
+ if (mob.Map != map || !mob.InRange(worldLoc, range))
{
- var mob = openers[i];
-
- var range = GetUpdateRange(mob);
-
- if (mob.Map != map || !mob.InRange(worldLoc, range))
+ openers.RemoveAt(i--);
+ }
+ else
+ {
+ if (mob == rootParent || mob == tradeRecip)
{
- openers.RemoveAt(i--);
+ continue;
}
- else
+
+ var ns = mob.NetState;
+
+ if (ns != null && mob.CanSee(this))
{
- if (mob == rootParent || mob == tradeRecip)
- {
- continue;
- }
-
- var ns = mob.NetState;
-
- if (ns != null && mob.CanSee(this))
- {
- ns.SendContainerContentUpdate(this);
- SendOPLPacketTo(ns);
- }
+ ns.SendContainerContentUpdate(this);
+ SendOPLPacketTo(ns);
}
}
+ }
- if (openers.Count == 0)
- {
- contParent.Openers = null;
- }
+ if (openers.Count == 0)
+ {
+ contParent.Openers = null;
}
}
@@ -1719,6 +1722,7 @@ namespace Server
}
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool GetFlag(ImplFlag flag) => (m_Flags & flag) != 0;
public BounceInfo GetBounce() => LookupCompactInfo()?.m_Bounce;
@@ -1748,12 +1752,9 @@ namespace Server
parentItem.OnItemBounceCleared(this);
}
}
- else if (bounce.Parent is Mobile parentMobile)
+ else if (bounce.Parent is Mobile { Deleted: false } parentMobile)
{
- if (!parentMobile.Deleted)
- {
- parentMobile.OnItemBounceCleared(this);
- }
+ parentMobile.OnItemBounceCleared(this);
}
VerifyCompactInfo();
@@ -2344,7 +2345,7 @@ namespace Server
var itemID = m_ItemID;
var doubled = m_Amount > 1;
- if (itemID >= 0xEEA && itemID <= 0xEF2) // Are we coins?
+ if (itemID is >= 0xEEA and <= 0xEF2) // Are we coins?
{
var coinBase = (itemID - 0xEEA) / 3;
coinBase *= 3;
@@ -2408,7 +2409,7 @@ namespace Server
}
#nullable enable
- public void InvalidateProperties()
+ public virtual void InvalidateProperties()
{
if (!ObjectPropertyList.Enabled)
{
@@ -2573,6 +2574,10 @@ namespace Server
}
}
+ public virtual void BeforeSerialize()
+ {
+ }
+
public virtual void Deserialize(IGenericReader reader)
{
var version = reader.ReadInt();
@@ -3251,10 +3256,8 @@ namespace Server
m_DeltaFlags &= ~flags;
}
- public static int ProcessDeltaQueue()
+ public static void ProcessDeltaQueue()
{
- int count = 0;
-
var limit = m_DeltaQueue.Count;
while (m_DeltaQueue.Count > 0 && --limit >= 0)
@@ -3266,8 +3269,6 @@ namespace Server
continue;
}
- count++;
-
item.SetFlag(ImplFlag.InQueue, false);
try
@@ -3288,8 +3289,6 @@ namespace Server
Console.WriteLine("Warning: {0} items left in delta queue after processing.", m_DeltaQueue.Count);
Utility.PopColor();
}
-
- return count;
}
public virtual void OnDelete()
diff --git a/Projects/Server/Json/Converters/ClientVersionConverter.cs b/Projects/Server/Json/Converters/ClientVersionConverter.cs
new file mode 100644
index 000000000..c1dc8ecb9
--- /dev/null
+++ b/Projects/Server/Json/Converters/ClientVersionConverter.cs
@@ -0,0 +1,37 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2021 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: ClientVersionConverter.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Server.Json
+{
+ public class ClientVersionConverter : JsonConverter
+ {
+ public override ClientVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ return new ClientVersion(reader.GetString());
+ }
+
+ throw new JsonException($"Value must be a string");
+ }
+
+ public override void Write(Utf8JsonWriter writer, ClientVersion value, JsonSerializerOptions options) =>
+ writer.WriteStringValue(value.ToString());
+ }
+}
diff --git a/Projects/Server/Json/Converters/ClientVersionConverterFactory.cs b/Projects/Server/Json/Converters/ClientVersionConverterFactory.cs
new file mode 100644
index 000000000..3e778d591
--- /dev/null
+++ b/Projects/Server/Json/Converters/ClientVersionConverterFactory.cs
@@ -0,0 +1,30 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2021 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: ClientVersionConverterFactory.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Net;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Server.Json
+{
+ public class ClientVersionConverterFactory : JsonConverterFactory
+ {
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(ClientVersion);
+
+ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) =>
+ new ClientVersionConverter();
+ }
+}
diff --git a/Projects/Server/Json/Converters/FlagsConverter.cs b/Projects/Server/Json/Converters/FlagsConverter.cs
new file mode 100644
index 000000000..38584d05a
--- /dev/null
+++ b/Projects/Server/Json/Converters/FlagsConverter.cs
@@ -0,0 +1,149 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2021 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: FlagsConverter.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Server.Json
+{
+ public class FlagsConverter : JsonConverter where T : struct, Enum
+ {
+ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var flags = 0ul;
+ var underlyingType = Enum.GetUnderlyingType(typeof(T));
+
+ while (true)
+ {
+ reader.Read();
+ if (reader.TokenType == JsonTokenType.EndObject)
+ {
+ break;
+ }
+
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ throw new JsonException("Invalid Json structure for Flag object");
+ }
+
+ var key = reader.GetString();
+
+ reader.Read();
+
+ if (!reader.GetBoolean() || !Enum.TryParse(key, out var val))
+ {
+ continue;
+ }
+
+ flags |= ConvertToUInt64(underlyingType, val);
+ }
+
+ switch (Type.GetTypeCode(underlyingType))
+ {
+ case TypeCode.SByte:
+ {
+ var num = (sbyte)flags;
+ return Unsafe.As(ref num);
+ }
+ case TypeCode.Byte:
+ {
+ var num = (byte)flags;
+ return Unsafe.As(ref num);
+ }
+ case TypeCode.Int16:
+ {
+ var num = (short)flags;
+ return Unsafe.As(ref num);
+ }
+ case TypeCode.UInt16:
+ {
+ var num = (ushort)flags;
+ return Unsafe.As(ref num);
+ }
+ case TypeCode.UInt32:
+ {
+ var num = (uint)flags;
+ return Unsafe.As(ref num);
+ }
+ case TypeCode.Int64:
+ {
+ var num = (long)flags;
+ return Unsafe.As(ref num);
+ }
+ case TypeCode.UInt64:
+ {
+ return Unsafe.As(ref flags);
+ }
+ default:
+ {
+ var num = (int)flags;
+ return Unsafe.As(ref num);
+ }
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+ var underlyingType = Enum.GetUnderlyingType(typeof(T));
+ var size = GetUnderlyingTypeLength(Type.GetTypeCode(underlyingType)) - 1;
+ var intValue = ConvertToUInt64(underlyingType, value);
+
+ foreach (var flagName in Enum.GetNames(typeof(T)))
+ {
+ var flagValue = Enum.Parse(flagName, false);
+ var flag = ConvertToUInt64(underlyingType, flagValue);
+ if (flag == 0 || (flag & size) == 0)
+ {
+ writer.WriteBoolean(flagName, (intValue & flag) != 0);
+ }
+ }
+
+ writer.WriteEndObject();
+ }
+
+ private static ulong ConvertToUInt64(Type underlyingType, object value) =>
+ Type.GetTypeCode(underlyingType) switch
+ {
+ TypeCode.SByte => (ulong)(sbyte)value,
+ TypeCode.Byte => (byte)value,
+ TypeCode.Int16 => (ulong)(short)value,
+ TypeCode.UInt16 => (ushort)value,
+ TypeCode.Int32 => (ulong)(int)value,
+ TypeCode.UInt32 => (uint)value,
+ TypeCode.Int64 => (ulong)(long)value,
+ TypeCode.UInt64 => (ulong)value,
+ _ => throw new InvalidOperationException()
+ };
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong GetUnderlyingTypeLength(TypeCode typeCode) =>
+ typeCode switch
+ {
+ TypeCode.Byte => 8,
+ TypeCode.SByte => 8,
+ TypeCode.Int16 => 16,
+ TypeCode.UInt16 => 16,
+ TypeCode.Char => 16,
+ TypeCode.Int32 => 32,
+ TypeCode.UInt32 => 32,
+ TypeCode.Int64 => 64,
+ TypeCode.UInt64 => 64,
+ _ => 64
+ };
+ }
+}
diff --git a/Projects/Server/Json/Converters/GuidConverter.cs b/Projects/Server/Json/Converters/GuidConverter.cs
index 3f61742b8..b6c373094 100644
--- a/Projects/Server/Json/Converters/GuidConverter.cs
+++ b/Projects/Server/Json/Converters/GuidConverter.cs
@@ -14,7 +14,6 @@
*************************************************************************/
using System;
-using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
diff --git a/Projects/Server/Json/Converters/GuidConverterFactory.cs b/Projects/Server/Json/Converters/GuidConverterFactory.cs
index 92acf4183..e0ba7c4ce 100644
--- a/Projects/Server/Json/Converters/GuidConverterFactory.cs
+++ b/Projects/Server/Json/Converters/GuidConverterFactory.cs
@@ -14,7 +14,6 @@
*************************************************************************/
using System;
-using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
diff --git a/Projects/Server/Json/JsonConfig.cs b/Projects/Server/Json/JsonConfig.cs
index c44ba874c..163776d84 100644
--- a/Projects/Server/Json/JsonConfig.cs
+++ b/Projects/Server/Json/JsonConfig.cs
@@ -39,6 +39,7 @@ namespace Server.Json
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
+ options.Converters.Add(new ClientVersionConverterFactory());
options.Converters.Add(new GuidConverterFactory());
options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new MapConverterFactory());
diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs
index 18804b003..1eac5d8e8 100644
--- a/Projects/Server/Main.cs
+++ b/Projects/Server/Main.cs
@@ -17,6 +17,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Reflection;
using System.Runtime;
using System.Runtime.InteropServices;
@@ -55,7 +56,8 @@ namespace Server
public static readonly bool IsDarwin = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public static readonly bool IsFreeBSD = RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD);
public static readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || IsFreeBSD;
- public static readonly bool Unix = IsDarwin || IsFreeBSD || IsLinux;
+ public static readonly bool IsBSD = IsDarwin || IsFreeBSD;
+ public static readonly bool Unix = IsBSD || IsLinux;
private const string AssembliesConfiguration = "Data/assemblies.json";
@@ -155,6 +157,11 @@ namespace Server
set => _now = value;
}
+ private static long _cycleIndex = 1;
+ private static float[] _cyclesPerSecond = new float[100];
+
+ public static float CyclesPerSecond => _cyclesPerSecond[(_cycleIndex - 1) % _cyclesPerSecond.Length];
+
public static bool MultiProcessor { get; private set; }
public static int ProcessorCount { get; private set; }
@@ -242,6 +249,17 @@ namespace Server
{
fullPath = Path.Combine(p, path);
+ if (IsLinux)
+ {
+ var fi = new FileInfo(fullPath);
+ fullPath = fi.Directory!.EnumerateFiles(
+ fi.Name,
+ new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }
+ ).FirstOrDefault()?.FullName;
+
+ break;
+ }
+
if (File.Exists(fullPath))
{
break;
@@ -488,40 +506,51 @@ namespace Server
{
try
{
+#if DEBUG
+ const bool idleCPU = true;
+#else
+ var idleCPU = ServerConfiguration.GetOrUpdateSetting("core.enableIdleCPU", false);
+#endif
+
+ long last = TickCount;
const int interval = 100;
- int idleCount = 0;
+ const float ticksPerSecond = 1000 * interval;
+
+ int sample = 0;
while (!Closing)
{
_tickCount = TickCount;
_now = DateTime.UtcNow;
- var events = Mobile.ProcessDeltaQueue();
- events += Item.ProcessDeltaQueue();
- events += Timer.Slice(_tickCount);
+ Mobile.ProcessDeltaQueue();
+ Item.ProcessDeltaQueue();
+ Timer.Slice(_tickCount);
// Handle networking
- events += TcpServer.Slice();
- events += NetState.HandleAllReceives();
- events += NetState.Slice();
+ TcpServer.Slice();
+ NetState.Slice();
// Execute captured post-await methods (like Timer.Pause)
- events += LoopContext.ExecuteTasks();
+ LoopContext.ExecuteTasks();
Timer.CheckTimerPool(); // Check for pool depletion so we can async refill it.
_tickCount = 0;
_now = DateTime.MinValue;
- if (events > 0)
+ if (idleCPU && ++sample % interval == 0)
{
- idleCount = 0;
- continue;
- }
+ var now = TickCount;
- if (++idleCount > interval)
- {
- Thread.Sleep(1);
+ var cyclesPerSecond = ticksPerSecond / (now - last);
+ _cyclesPerSecond[_cycleIndex++ % _cyclesPerSecond.Length] = cyclesPerSecond;
+ last = now;
+
+ if (cyclesPerSecond > 80)
+ {
+ Thread.Sleep(2);
+ }
}
}
}
diff --git a/Projects/Server/Maps/Map.cs b/Projects/Server/Maps/Map.cs
index 0c0e489ec..8dfe26e66 100644
--- a/Projects/Server/Maps/Map.cs
+++ b/Projects/Server/Maps/Map.cs
@@ -692,7 +692,7 @@ namespace Server
var tile = staticTiles[i];
var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue];
- if (id.Surface || (id.Flags & TileFlag.Wet) != 0)
+ if (id.Surface || id.Wet)
{
var tileZ = tile.Z + id.CalcHeight;
@@ -720,7 +720,7 @@ namespace Server
{
var id = item.ItemData;
- if (id.Surface || (id.Flags & TileFlag.Wet) != 0)
+ if (id.Surface || id.Wet)
{
var itemZ = item.Z + id.CalcHeight;
diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs
index d4164130c..5e259a849 100644
--- a/Projects/Server/Mobiles/Mobile.cs
+++ b/Projects/Server/Mobiles/Mobile.cs
@@ -916,8 +916,15 @@ namespace Server
else
{
m_NetState.SendChangeCombatant(m_Combatant.Serial);
- Timer.StartTimer(ExpireCombatantDelay, ExpireCombatant, out _expireCombatantTimerToken);
- Timer.StartTimer(TimeSpan.FromSeconds(0.01), 0, CheckCombatTime, out _combatTimerToken);
+ if (!_expireCombatantTimerToken.Running)
+ {
+ Timer.StartTimer(ExpireCombatantDelay, ExpireCombatant, out _expireCombatantTimerToken);
+ }
+
+ if (!_combatTimerToken.Running)
+ {
+ Timer.StartTimer(TimeSpan.FromSeconds(0.01), 0, CheckCombatTime, out _combatTimerToken);
+ }
if (CanBeHarmful(m_Combatant, false))
{
@@ -1147,9 +1154,6 @@ namespace Server
public virtual bool ShouldCheckStatTimers => true;
- [CommandProperty(AccessLevel.GameMaster)]
- public DateTime CreationTime { get; private set; }
-
[CommandProperty(AccessLevel.GameMaster)]
public int LightLevel
{
@@ -2472,6 +2476,12 @@ namespace Server
AddNameProperties(list);
}
+ [CommandProperty(AccessLevel.GameMaster, readOnly: true)]
+ public DateTime Created { get; set; } = Core.Now;
+
+ [CommandProperty(AccessLevel.GameMaster)]
+ DateTime ISerializable.LastSerialized { get; set; } = Core.Now;
+
long ISerializable.SavePosition { get; set; } = -1;
BufferWriter ISerializable.SaveBuffer { get; set; }
@@ -2483,7 +2493,7 @@ namespace Server
public virtual void Serialize(IGenericWriter writer)
{
- writer.Write(32); // version
+ writer.Write(33); // version
writer.WriteDeltaTime(LastStrGain);
writer.WriteDeltaTime(LastIntGain);
@@ -2519,7 +2529,7 @@ namespace Server
writer.Write(Corpse);
- writer.Write(CreationTime);
+ // writer.Write(CreationTime);
Stabled.Tidy();
writer.Write(Stabled);
@@ -6167,12 +6177,21 @@ namespace Server
{
}
+ public virtual void BeforeSerialize()
+ {
+ }
+
public virtual void Deserialize(IGenericReader reader)
{
var version = reader.ReadInt();
switch (version)
{
+ case 33:
+ {
+ // Removed created
+ goto case 32;
+ }
case 32:
{
// Removed StuckMenu
@@ -6232,7 +6251,10 @@ namespace Server
}
case 23:
{
- CreationTime = reader.ReadDateTime();
+ if (version < 33)
+ {
+ Created = reader.ReadDateTime();
+ }
goto case 22;
}
@@ -7818,7 +7840,6 @@ namespace Server
DamageEntries = new List();
NextSkillTime = Core.TickCount;
- CreationTime = Core.Now;
}
public virtual void Delta(MobileDelta flag)
@@ -7848,10 +7869,8 @@ namespace Server
}
}
- public static int ProcessDeltaQueue()
+ public static void ProcessDeltaQueue()
{
- int count = 0;
-
var limit = m_DeltaQueue.Count;
while (m_DeltaQueue.Count > 0 && --limit >= 0)
@@ -7863,8 +7882,6 @@ namespace Server
continue;
}
- count++;
-
mob.m_InDeltaQueue = false;
try
@@ -7885,8 +7902,6 @@ namespace Server
Console.WriteLine("Warning: {0} mobiles left in delta queue after processing.", m_DeltaQueue.Count);
Utility.PopColor();
}
-
- return count;
}
public virtual void OnKillsChange(int oldValue)
diff --git a/Projects/Server/Network/ISocket.cs b/Projects/Server/Network/ISocket.cs
index b1182c77a..75d3d8475 100644
--- a/Projects/Server/Network/ISocket.cs
+++ b/Projects/Server/Network/ISocket.cs
@@ -23,6 +23,8 @@ namespace Server.Network
{
public interface ISocket
{
+ public IntPtr Handle { get; }
+
public EndPoint LocalEndPoint { get; }
public EndPoint RemoteEndPoint { get; }
@@ -33,6 +35,8 @@ namespace Server.Network
public Task ReceiveAsync(IList> buffer, SocketFlags flags);
+ public int Receive(IList> buffers, SocketFlags flags);
+
public void Shutdown(SocketShutdown how);
public void Close();
diff --git a/Projects/Server/Network/NetState/DumpNetStates.cs b/Projects/Server/Network/NetState/DumpNetStates.cs
index ad0e3a821..6992254dc 100644
--- a/Projects/Server/Network/NetState/DumpNetStates.cs
+++ b/Projects/Server/Network/NetState/DumpNetStates.cs
@@ -32,7 +32,7 @@ namespace Server.Network
foreach (var ns in TcpServer.Instances)
{
- file.WriteLine($"{ns}, {ns._recvState}, {ns._sendState}, {ns._protocolState}, {ns._parserState}");
+ file.WriteLine($"{ns}, {ns._protocolState}, {ns._parserState}");
}
}
}
diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs
old mode 100644
new mode 100755
index 84ce948be..47c6bf381
--- a/Projects/Server/Network/NetState/NetState.cs
+++ b/Projects/Server/Network/NetState/NetState.cs
@@ -20,8 +20,9 @@ using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
+using System.Network;
using System.Runtime.CompilerServices;
-using System.Threading;
+using System.Runtime.InteropServices;
using Server.Accounting;
using Server.Diagnostics;
using Server.Gumps;
@@ -43,12 +44,15 @@ namespace Server.Network
private const int RecvPipeSize = 1024 * 64;
private const int SendPipeSize = 1024 * 256;
- private static int GumpCap = 512;
- private static int HuePickerCap = 512;
- private static int MenuCap = 512;
- private static int PacketPerSecondThreshold = 3000;
+ private const int GumpCap = 512;
+ private const int HuePickerCap = 512;
+ private const int MenuCap = 512;
+ private const int PacketPerSecondThreshold = 3000;
+ private static GCHandle[] _polledStates = new GCHandle[2048];
+ private static readonly IPollGroup _pollGroup = PollGroup.Create();
private static readonly Queue FlushPending = new(2048);
+ private static readonly Queue FlushedPartials = new(2048);
private static readonly ConcurrentQueue Disposed = new();
public static NetStateCreatedCallback CreatedCallback { get; set; }
@@ -56,7 +60,7 @@ namespace Server.Network
private readonly string _toString;
private ClientVersion _version;
private long _nextActivityCheck;
- private int _running;
+ private bool _running = true;
private volatile DecodePacket _packetDecoder;
private volatile EncodePacket _packetEncoder;
private bool _flushQueued;
@@ -66,14 +70,12 @@ namespace Server.Network
internal int _authId;
internal int _seed;
- internal ParserState _parserState = ParserState.Uninitialized;
- internal ProtocolState _protocolState = ProtocolState.Uninitialized;
- internal RecvState _recvState = RecvState.Uninitialized;
- internal SendState _sendState = SendState.Uninitialized;
+ internal ParserState _parserState = ParserState.AwaitingNextPacket;
+ internal ProtocolState _protocolState = ProtocolState.AwaitingSeed;
+ internal GCHandle _handle;
internal enum ParserState
{
- Uninitialized,
AwaitingNextPacket,
AwaitingPartialPacket,
ProcessingPacket,
@@ -83,7 +85,6 @@ namespace Server.Network
internal enum ProtocolState
{
- Uninitialized,
AwaitingSeed, // Based on the way the seed arrives, we know if this is a login server or a game server connection
LoginServer_AwaitingLogin,
@@ -96,38 +97,12 @@ namespace Server.Network
Error
}
- internal enum RecvState
- {
- Uninitialized,
- AwaitingMemory,
- AwaitingRecv,
- DataReceived,
- Exited,
- }
-
- internal enum SendState
- {
- Uninitialized,
- AwaitingData,
- Sending,
- SendCompleted,
- Exited,
- }
-
- public static void Configure()
- {
- GumpCap = ServerConfiguration.GetOrUpdateSetting("netstate.gumpCap", GumpCap);
- HuePickerCap = ServerConfiguration.GetOrUpdateSetting("netstate.huePickerCap", HuePickerCap);
- MenuCap = ServerConfiguration.GetOrUpdateSetting("netstate.menuCap", MenuCap);
- PacketPerSecondThreshold = ServerConfiguration.GetOrUpdateSetting("netstate.packetsPerSecondThreshold", PacketPerSecondThreshold);
- }
-
public static void Initialize()
{
Timer.DelayCall(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1.5), CheckAllAlive);
}
- public NetState(ISocket connection)
+ public NetState(Socket connection)
{
Connection = connection;
Seeded = false;
@@ -138,6 +113,7 @@ namespace Server.Network
RecvPipe = new Pipe(GC.AllocateUninitializedArray(RecvPipeSize));
SendPipe = new Pipe(GC.AllocateUninitializedArray(SendPipeSize));
_nextActivityCheck = Core.TickCount + 30000;
+ ConnectedOn = Core.Now;
try
{
@@ -151,7 +127,17 @@ namespace Server.Network
_toString = "(error)";
}
- ConnectedOn = Core.Now;
+ _handle = GCHandle.Alloc(this);
+
+ try
+ {
+ _pollGroup.Add(connection, _handle);
+ }
+ catch (Exception ex)
+ {
+ TraceException(ex);
+ Disconnect("Unable to add socket to poll group");
+ }
CreatedCallback?.Invoke(this);
}
@@ -174,25 +160,21 @@ namespace Server.Network
set => _packetEncoder = value;
}
+ public int CurrentPacket { get; internal set; }
+
public bool SentFirstPacket { get; set; }
public bool BlockAllPackets { get; set; }
public List Trades { get; }
- public bool Running
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _running == 1;
- }
-
public bool Seeded { get; set; }
public Pipe RecvPipe { get; }
public Pipe SendPipe { get; }
- public ISocket Connection { get; }
+ public Socket Connection { get; }
public bool CompressionEnabled { get; set; }
@@ -524,36 +506,21 @@ namespace Server.Network
}
}
- internal void Start()
+ public void HandleReceive()
{
- if (Interlocked.CompareExchange(ref _running, 1, 0) == 1 || Connection == null)
+ if (!_running)
{
return;
}
- _parserState = ParserState.AwaitingNextPacket;
- _protocolState = ProtocolState.AwaitingSeed;
-
- ThreadPool.UnsafeQueueUserWorkItem(RecvTask, null);
- ThreadPool.UnsafeQueueUserWorkItem(SendTask, null);
- }
-
- // Return true if there was any data to be processed. False otherwise. Used for idle detection.
- public bool HandleReceive()
- {
- if (!Running)
- {
- return false;
- }
-
- bool active = false;
+ ReceiveData();
var reader = RecvPipe.Reader;
try
{
// Process as many packets as we can synchronously
- while (Running && _parserState != ParserState.Error && _protocolState != ProtocolState.Error)
+ while (_running && _parserState != ParserState.Error && _protocolState != ProtocolState.Error)
{
var result = reader.TryRead();
var length = result.Length;
@@ -563,9 +530,6 @@ namespace Server.Network
break;
}
- // There was at least some data found, so it's not idle.
- active = true;
-
var packetReader = new CircularBufferReader(result.Buffer);
var packetId = packetReader.ReadByte();
int packetLength = length;
@@ -580,12 +544,6 @@ namespace Server.Network
{
switch (_protocolState)
{
- case ProtocolState.Uninitialized:
- {
- HandleError(packetId, packetLength);
- return true;
- }
-
case ProtocolState.AwaitingSeed:
{
if (packetId == 0xEF)
@@ -604,7 +562,7 @@ namespace Server.Network
if (seed == 0)
{
HandleError(0, 0);
- return true;
+ return;
}
_seed = seed;
@@ -626,7 +584,7 @@ namespace Server.Network
{
LogInfo("Possible encrypted client detected, disconnecting...");
HandleError(packetId, packetLength);
- return true;
+ return;
}
_parserState = ParserState.ProcessingPacket;
@@ -643,7 +601,7 @@ namespace Server.Network
if (packetId != 0xA0)
{
HandleError(packetId, packetLength);
- return true;
+ return;
}
_parserState = ParserState.ProcessingPacket;
@@ -665,7 +623,7 @@ namespace Server.Network
_parserState = ParserState.AwaitingNextPacket;
_protocolState = ProtocolState.AwaitingSeed;
#endif
- return true;
+ return;
}
case ProtocolState.GameServer_AwaitingGameServerLogin:
@@ -673,7 +631,7 @@ namespace Server.Network
if (packetId != 0x91 && packetId != 0x80)
{
HandleError(packetId, packetLength);
- return true;
+ return;
}
_parserState = ParserState.ProcessingPacket;
@@ -719,8 +677,6 @@ namespace Server.Network
TraceException(ex);
Disconnect("Exception during HandleReceive");
}
-
- return active;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -811,83 +767,56 @@ namespace Server.Network
return ParserState.AwaitingNextPacket;
}
- private async void SendTask(object state)
+ private bool Flush()
{
+ _flushQueued = false;
+
+ if (Connection == null)
+ {
+ return true;
+ }
+
+ SendPipe.Writer.Flush();
+
var reader = SendPipe.Reader;
+ var result = reader.TryRead();
+
+ if (result.IsClosed || result.Length == 0)
+ {
+ return true;
+ }
+
+ var bytesWritten = 0;
try
{
- while (Running)
- {
- _sendState = SendState.AwaitingData;
-
- var result = await reader.Read();
-
- if (result.IsClosed || !Running)
- {
- break;
- }
-
- if (result.Length <= 0)
- {
- continue;
- }
-
- _sendState = SendState.Sending;
-
- var bytesWritten = await Connection.SendAsync(result.Buffer, SocketFlags.None);
-
- _sendState = SendState.SendCompleted;
-
- if (bytesWritten > 0)
- {
- _nextActivityCheck = Core.TickCount + 90000;
- reader.Advance((uint)bytesWritten);
- }
- }
-
- // Grab any remaining data and flush it
- var data = reader.TryRead();
-
- if (data.Length > 0)
- {
- _sendState = SendState.Sending;
- Connection.Send(data.Buffer, SocketFlags.None);
-
- reader.Advance((uint)data.Length);
- }
+ bytesWritten = Connection.Send(result.Buffer, SocketFlags.None);
}
catch (SocketException ex)
{
- // If the user closes the connection (or the recv side does)
- // between the check for m_Running above and the call to SendAsync,
- // we can still get a socket exception here. That's ok.
+ // Socket exceptions are generally ok, just spammy
#if DEBUG
Console.WriteLine(ex);
#endif
+
+ Disconnect(string.Empty);
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine(ex);
#endif
+ Disconnect($"Disconnected with error: {ex}");
TraceException(ex);
}
- finally
- {
- try
- {
- Connection.Shutdown(SocketShutdown.Both);
- Connection.Close();
- }
- catch (Exception ex)
- {
- TraceException(ex);
- }
- Disconnect("Exiting SendTask.");
- _sendState = SendState.Exited;
+ if (bytesWritten > 0)
+ {
+ _nextActivityCheck = Core.TickCount + 90000;
+ reader.Advance((uint)bytesWritten);
}
+
+ return bytesWritten == result.Length;
}
private void DecodePacket(ArraySegment[] buffer, ref int length)
@@ -896,47 +825,21 @@ namespace Server.Network
_packetDecoder?.Invoke(cBuffer, ref length);
}
- private async void RecvTask(object state)
+ private void ReceiveData()
{
- var socket = Connection;
var writer = RecvPipe.Writer;
+ var result = writer.TryGetMemory();
+
+ if (result.IsClosed || result.Length == 0)
+ {
+ return;
+ }
+
+ var bytesWritten = 0;
try
{
- while (Running)
- {
- _recvState = RecvState.AwaitingMemory;
- var result = await writer.GetMemory();
-
- if (result.IsClosed || !Running)
- {
- break;
- }
-
- if (result.Length <= 0)
- {
- continue;
- }
-
- _recvState = RecvState.AwaitingRecv;
-
- var bytesWritten = await socket.ReceiveAsync(result.Buffer, SocketFlags.None);
- if (bytesWritten <= 0)
- {
- break;
- }
-
- _recvState = RecvState.DataReceived;
-
- DecodePacket(result.Buffer, ref bytesWritten);
-
- writer.Advance((uint)bytesWritten);
- _nextActivityCheck = Core.TickCount + 90000;
-
- // No need to flush
- }
-
- Disconnect(string.Empty);
+ bytesWritten = Connection.Receive(result.Buffer, SocketFlags.None);
}
catch (SocketException ex)
{
@@ -953,38 +856,20 @@ namespace Server.Network
#if DEBUG
Console.WriteLine(ex);
#endif
- Disconnect("RecvTask exited unexpectedly.");
+ Disconnect($"Disconnected with error: {ex}");
TraceException(ex);
}
- finally
- {
- _recvState = RecvState.Exited;
- }
- }
- public static int HandleAllReceives()
- {
- int count = 0;
-
- foreach (var ns in TcpServer.Instances)
+ if (bytesWritten <= 0)
{
- if (ns.HandleReceive())
- {
- count++;
- }
+ Disconnect(string.Empty);
+ return;
}
- return count;
- }
+ DecodePacket(result.Buffer, ref bytesWritten);
- public void Flush()
- {
- if (Connection != null)
- {
- SendPipe.Writer.Flush();
- }
-
- _flushQueued = false;
+ writer.Advance((uint)bytesWritten);
+ _nextActivityCheck = Core.TickCount + 90000;
}
public static void FlushAll()
@@ -995,22 +880,38 @@ namespace Server.Network
}
}
- public static int Slice()
+ public static void Slice()
{
- int count = 0;
- while (FlushPending.Count != 0)
+ int count = _pollGroup.Poll(ref _polledStates);
+
+ if (count > 0)
{
- FlushPending.Dequeue()?.Flush();
- count++;
+ for (int i = 0; i < count; i++)
+ {
+ (_polledStates[i].Target as NetState)?.HandleReceive();
+ _polledStates[i] = default;
+ }
}
+ while (FlushPending.TryDequeue(out var ns))
+ {
+ if (!ns.Flush())
+ {
+ // Incomplete data, so we need to requeue
+ FlushedPartials.Enqueue(ns);
+ }
+ }
+
+ var hasDisposes = !Disposed.IsEmpty;
while (Disposed.TryDequeue(out var ns))
{
- TcpServer.Instances.Remove(ns);
ns.Dispose();
}
- return count;
+ if (hasDisposes)
+ {
+ _pollGroup.Poll(ref _polledStates);
+ }
}
public void CheckAlive(long curTicks)
@@ -1064,11 +965,13 @@ namespace Server.Network
public void Disconnect(string reason)
{
- if (Interlocked.CompareExchange(ref _running, 0, 1) == 0)
+ if (!_running)
{
return;
}
+ _running = false;
+
try
{
if (_disconnectReason != string.Empty)
@@ -1082,7 +985,6 @@ namespace Server.Network
}
_disconnectReason = reason;
-
Disposed.Enqueue(this);
}
@@ -1114,6 +1016,11 @@ namespace Server.Network
{
TraceDisconnect(_disconnectReason, _toString);
+ if (_running)
+ {
+ throw new Exception("Disconnected a NetState that is still running.");
+ }
+
#if THREADGUARD
if (Thread.CurrentThread != Core.Thread)
{
@@ -1125,8 +1032,18 @@ namespace Server.Network
}
#endif
- RecvPipe.Writer.Close();
- SendPipe.Writer.Close();
+ TcpServer.Instances.Remove(this);
+ try
+ {
+ _pollGroup.Remove(Connection);
+ }
+ catch (Exception ex)
+ {
+ TraceException(ex);
+ }
+
+ Connection.Close();
+ _handle.Free();
var m = Mobile;
Mobile = null;
diff --git a/Projects/Server/Network/NetworkCompression.cs b/Projects/Server/Network/NetworkCompression.cs
index 908d51339..53fe87d2c 100644
--- a/Projects/Server/Network/NetworkCompression.cs
+++ b/Projects/Server/Network/NetworkCompression.cs
@@ -125,7 +125,6 @@ namespace Server.Network
return outputIdx;
}
-
public static int Compress(CircularBuffer input, CircularBuffer output)
{
if (input.Length > DefiniteOverflow)
diff --git a/Projects/Server/Network/NetworkSocket.cs b/Projects/Server/Network/NetworkSocket.cs
deleted file mode 100644
index e4e4c3a8a..000000000
--- a/Projects/Server/Network/NetworkSocket.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-/*************************************************************************
- * ModernUO *
- * Copyright 2019-2020 - ModernUO Development Team *
- * Email: hi@modernuo.com *
- * File: NetworkSocket.cs *
- * *
- * This program is free software: you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation, either version 3 of the License, or *
- * (at your option) any later version. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program. If not, see . *
- *************************************************************************/
-
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.Net.Sockets;
-using System.Runtime.CompilerServices;
-using System.Threading.Tasks;
-
-namespace Server.Network
-{
- public class NetworkSocket : ISocket
- {
- private readonly Socket _connection;
-
- public Socket Connection
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _connection;
- }
-
- public EndPoint LocalEndPoint
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _connection.LocalEndPoint;
- }
-
- public EndPoint RemoteEndPoint
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _connection.RemoteEndPoint;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public NetworkSocket(Socket connection) => _connection = connection;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public Task SendAsync(IList> buffers, SocketFlags flags) =>
- _connection.SendAsync(buffers, flags);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int Send(IList> buffers, SocketFlags flags) => _connection.Send(buffers, flags);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public Task ReceiveAsync(IList> buffers, SocketFlags flags) =>
- _connection.ReceiveAsync(buffers, flags);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void Shutdown(SocketShutdown how) => _connection.Shutdown(how);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void Close() => _connection.Close();
- }
-}
diff --git a/Projects/Server/Network/Packets/IncomingAccountPackets.cs b/Projects/Server/Network/Packets/IncomingAccountPackets.cs
index dd34c9099..4444b95ad 100644
--- a/Projects/Server/Network/Packets/IncomingAccountPackets.cs
+++ b/Projects/Server/Network/Packets/IncomingAccountPackets.cs
@@ -73,40 +73,20 @@ namespace Server.Network
int genderRace = reader.ReadByte();
- int str = reader.ReadByte();
- int dex = reader.ReadByte();
- int intl = reader.ReadByte();
- int is1 = reader.ReadByte();
- int vs1 = reader.ReadByte();
- int is2 = reader.ReadByte();
- int vs2 = reader.ReadByte();
- int is3 = reader.ReadByte();
- int vs3 = reader.ReadByte();
+ var stats = new StatNameValue[]
+ {
+ new(StatType.Str, reader.ReadByte()),
+ new(StatType.Dex, reader.ReadByte()),
+ new(StatType.Int, reader.ReadByte())
+ };
- SkillNameValue[] skills;
-
- // We have protocol changes by now, so this is ok
+ var skills = new SkillNameValue[state.NewCharacterCreation ? 4 : 3];
+ skills[0] = new SkillNameValue((SkillName)reader.ReadByte(), reader.ReadByte());
+ skills[1] = new SkillNameValue((SkillName)reader.ReadByte(), reader.ReadByte());
+ skills[2] = new SkillNameValue((SkillName)reader.ReadByte(), reader.ReadByte());
if (state.NewCharacterCreation)
{
- int is4 = reader.ReadByte();
- int vs4 = reader.ReadByte();
-
- skills = new[]
- {
- new SkillNameValue((SkillName)is1, vs1),
- new SkillNameValue((SkillName)is2, vs2),
- new SkillNameValue((SkillName)is3, vs3),
- new SkillNameValue((SkillName)is4, vs4)
- };
- }
- else
- {
- skills = new[]
- {
- new SkillNameValue((SkillName)is1, vs1),
- new SkillNameValue((SkillName)is2, vs2),
- new SkillNameValue((SkillName)is3, vs3)
- };
+ skills[3] = new SkillNameValue((SkillName)reader.ReadByte(), reader.ReadByte());
}
int hue = reader.ReadUInt16();
@@ -171,9 +151,7 @@ namespace Server.Network
name,
female,
hue,
- str,
- dex,
- intl,
+ stats,
info[cityIndex],
skills,
shirtHue,
diff --git a/Projects/Server/Network/Packets/OutgoingEquipmentPackets.cs b/Projects/Server/Network/Packets/OutgoingEquipmentPackets.cs
index 3e613466f..7f2a0a3f1 100644
--- a/Projects/Server/Network/Packets/OutgoingEquipmentPackets.cs
+++ b/Projects/Server/Network/Packets/OutgoingEquipmentPackets.cs
@@ -110,7 +110,6 @@ namespace Server.Network
parentSerial = Serial.Zero;
}
-
var writer = new SpanWriter(stackalloc byte[15]);
writer.Write((byte)0x2E); // Packet ID
writer.Write(item.Serial);
diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs
index 7dcd9a962..a9b9f1955 100644
--- a/Projects/Server/Network/TcpServer.cs
+++ b/Projects/Server/Network/TcpServer.cs
@@ -84,6 +84,14 @@ namespace Server.Network
Listeners = listeners.ToArray();
}
+ public static void Shutdown()
+ {
+ foreach (var listener in Listeners)
+ {
+ listener.Server.Close();
+ }
+ }
+
public static IEnumerable GetListeningAddresses(IPEndPoint ipep) =>
NetworkInterface.GetAllNetworkInterfaces().SelectMany(adapter =>
adapter.GetIPProperties().UnicastAddresses
@@ -93,12 +101,19 @@ namespace Server.Network
public static TcpListener CreateListener(IPEndPoint ipep)
{
- var listener = new TcpListener(ipep);
- listener.Server.ExclusiveAddressUse = false;
+ var listener = new TcpListener(ipep)
+ {
+ Server =
+ {
+ LingerState = new LingerOption(false, 0),
+ ExclusiveAddressUse = true,
+ NoDelay = true
+ }
+ };
try
{
- listener.Start(8);
+ listener.Start(32);
return listener;
}
catch (SocketException se)
@@ -122,7 +137,7 @@ namespace Server.Network
return null;
}
- public static int Slice()
+ public static void Slice()
{
int count = 0;
@@ -130,10 +145,7 @@ namespace Server.Network
{
Instances.Add(ns);
ns.LogInfo("Connected. [{0} Online]", Instances.Count);
- ns.Start();
}
-
- return count;
}
private static async void BeginAcceptingSockets(this TcpListener listener)
@@ -184,7 +196,7 @@ namespace Server.Network
}
else
{
- var ns = new NetState(new NetworkSocket(socket));
+ var ns = new NetState(socket);
_connectedQueue.Enqueue(ns);
}
}
diff --git a/Projects/Server/Serialization/BinaryFileReader.cs b/Projects/Server/Serialization/BinaryFileReader.cs
index 890fc0c49..81064faa3 100644
--- a/Projects/Server/Serialization/BinaryFileReader.cs
+++ b/Projects/Server/Serialization/BinaryFileReader.cs
@@ -30,6 +30,8 @@ namespace Server
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Close() => _reader.Close();
+ public DateTime LastSerialized { get; init; }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadString(bool intern = false)
{
diff --git a/Projects/Server/Serialization/BinaryFileWriter.cs b/Projects/Server/Serialization/BinaryFileWriter.cs
index 340e9c66b..9980eb000 100644
--- a/Projects/Server/Serialization/BinaryFileWriter.cs
+++ b/Projects/Server/Serialization/BinaryFileWriter.cs
@@ -35,7 +35,6 @@ namespace Server
public override long Position => _position + Index;
-
protected override int BufferSize => 81920;
public override void Flush()
diff --git a/Projects/Server/Serialization/BufferReader.cs b/Projects/Server/Serialization/BufferReader.cs
index 5b512215b..e04135db5 100644
--- a/Projects/Server/Serialization/BufferReader.cs
+++ b/Projects/Server/Serialization/BufferReader.cs
@@ -37,6 +37,8 @@ namespace Server
_encoding = encoding ?? TextEncoding.UTF8;
}
+ public BufferReader(byte[] buffer, DateTime LastSerialized) : this(buffer) => LastSerialized = LastSerialized;
+
public void Reset(byte[] newBuffer, out byte[] oldBuffer)
{
oldBuffer = _buffer;
@@ -45,6 +47,8 @@ namespace Server
}
// Compatible with BinaryReader.ReadString()
+ public DateTime LastSerialized { get; init; } = DateTime.MinValue;
+
public string ReadString(bool intern = false)
{
if (!ReadBool())
diff --git a/Projects/Server/Serialization/GenericPersistence.cs b/Projects/Server/Serialization/GenericPersistence.cs
index 2912664b7..eb878303c 100644
--- a/Projects/Server/Serialization/GenericPersistence.cs
+++ b/Projects/Server/Serialization/GenericPersistence.cs
@@ -41,7 +41,7 @@ namespace Server
{
var path = Path.Combine(savePath, name);
- AssemblyHandler.EnsureDirectory(path);
+ PathUtility.EnsureDirectory(path);
string binPath = Path.Combine(path, $"{name}.bin");
using var bin = new BinaryFileWriter(binPath, true);
@@ -54,7 +54,7 @@ namespace Server
{
var path = Path.Combine(savePath, name);
- AssemblyHandler.EnsureDirectory(path);
+ PathUtility.EnsureDirectory(path);
string binPath = Path.Combine(path, $"{name}.bin");
diff --git a/Projects/Server/Serialization/IGenericReader.cs b/Projects/Server/Serialization/IGenericReader.cs
index 27194c4eb..67e27764e 100644
--- a/Projects/Server/Serialization/IGenericReader.cs
+++ b/Projects/Server/Serialization/IGenericReader.cs
@@ -21,6 +21,9 @@ namespace Server
{
public interface IGenericReader
{
+ // Used to determine valid Entity deserialization
+ DateTime LastSerialized { get; init; }
+
string ReadString(bool intern = false);
long ReadLong();
ulong ReadULong();
diff --git a/Projects/Server/Serialization/IGenericWriter.cs b/Projects/Server/Serialization/IGenericWriter.cs
index c7f8a0281..95aa9d92f 100644
--- a/Projects/Server/Serialization/IGenericWriter.cs
+++ b/Projects/Server/Serialization/IGenericWriter.cs
@@ -39,14 +39,13 @@ namespace Server
void Write(DateTime value)
{
- var ticks = (value.Kind switch
+ // If DateTimeKind is Unspecified, we can't assume it needs to be converted.
+ if (value.Kind == DateTimeKind.Local)
{
- DateTimeKind.Local => value.ToUniversalTime(),
- DateTimeKind.Unspecified => value.ToLocalTime().ToUniversalTime(),
- _ => value
- }).Ticks;
+ value = value.ToUniversalTime();
+ }
- Write(ticks);
+ Write(value.Ticks);
}
void WriteDeltaTime(DateTime value)
{
@@ -59,17 +58,16 @@ namespace Server
if (value == DateTime.MaxValue)
{
Write(long.MaxValue);
+ return;
}
- var ticks = (value.Kind switch
+ if (value.Kind == DateTimeKind.Local)
{
- DateTimeKind.Local => value.ToUniversalTime(),
- DateTimeKind.Unspecified => value.ToLocalTime().ToUniversalTime(),
- _ => value
- }).Ticks;
+ value = value.ToUniversalTime();
+ }
// Technically supports negative deltas for times in the past
- Write(ticks - DateTime.UtcNow.Ticks);
+ Write(value.Ticks - DateTime.UtcNow.Ticks);
}
void Write(IPAddress value)
{
diff --git a/Projects/Server/Serialization/ISerializable.cs b/Projects/Server/Serialization/ISerializable.cs
index d91366c9b..3488c2c38 100644
--- a/Projects/Server/Serialization/ISerializable.cs
+++ b/Projects/Server/Serialization/ISerializable.cs
@@ -20,10 +20,20 @@ namespace Server
{
public interface ISerializable
{
+ // Should be serialized/deserialized with the index so it can be referenced by IGenericReader
+ DateTime Created { get; set; }
+
+ // Should be serialized/deserialized with the index so it can be referenced by IGenericReader
+ DateTime LastSerialized { get; protected internal set; }
long SavePosition { get; protected internal set; }
BufferWriter SaveBuffer { get; protected internal set; }
+
int TypeRef { get; }
Serial Serial { get; }
+
+ // Executed on every entity, before it's serialized.
+ // For example, this is used to clean up weak references and mark them dirty.
+ void BeforeSerialize();
void Deserialize(IGenericReader reader);
void Serialize(IGenericWriter writer);
void Delete();
@@ -48,6 +58,8 @@ namespace Server
{
SaveBuffer ??= new BufferWriter(true);
+ BeforeSerialize();
+
// Clean, don't bother serializing
if (SavePosition > -1)
{
@@ -55,6 +67,7 @@ namespace Server
return;
}
+ LastSerialized = Core.Now;
SaveBuffer.Seek(0, SeekOrigin.Begin);
Serialize(SaveBuffer);
@@ -64,7 +77,7 @@ namespace Server
}
else
{
- SavePosition = -1;
+ this.MarkDirty();
}
}
}
diff --git a/Projects/Server/Serialization/ISerializableExtensions.cs b/Projects/Server/Serialization/ISerializableExtensions.cs
index 17ca4b8bf..b98b239ef 100644
--- a/Projects/Server/Serialization/ISerializableExtensions.cs
+++ b/Projects/Server/Serialization/ISerializableExtensions.cs
@@ -27,6 +27,13 @@ namespace Server
entity.SavePosition = -1;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Delete(this ISerializable entity, IEntity toDelete)
+ {
+ toDelete?.Delete();
+ entity.MarkDirty();
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add(this ISerializable entity, ICollection list, T value)
{
diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj
index 60936169a..cf357a582 100755
--- a/Projects/Server/Server.csproj
+++ b/Projects/Server/Server.csproj
@@ -12,31 +12,30 @@
0.0.0
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
@@ -47,32 +46,7 @@
all
-
-
- TargetFramework=netstandard2.0
- Analyzer
- false
- all
-
-
-
-
-
-
- true
- Generated
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Projects/Server/Skills.cs b/Projects/Server/Skills.cs
index 082e548fd..077205507 100644
--- a/Projects/Server/Skills.cs
+++ b/Projects/Server/Skills.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Server.Network;
@@ -384,7 +385,8 @@ namespace Server
{
public SkillInfo(
int skillID, string name, double strScale, double dexScale, double intScale, string title,
- SkillUseCallback callback, double strGain, double dexGain, double intGain, double gainFactor
+ SkillUseCallback callback, double strGain, double dexGain, double intGain, double gainFactor,
+ string professionSkillName = null
)
{
Name = name;
@@ -398,7 +400,7 @@ namespace Server
DexGain = dexGain;
IntGain = intGain;
GainFactor = gainFactor;
-
+ ProfessionSkillName = professionSkillName ?? Name.Replace(" ", "");
StatTotal = strScale + dexScale + intScale;
}
@@ -426,25 +428,27 @@ namespace Server
public double GainFactor { get; set; }
+ public string ProfessionSkillName { get; set; }
+
public static SkillInfo[] Table { get; set; } =
{
new(0, "Alchemy", 0.0, 5.0, 5.0, "Alchemist", null, 0.0, 0.5, 0.5, 1.0),
new(1, "Anatomy", 0.0, 0.0, 0.0, "Biologist", null, 0.15, 0.15, 0.7, 1.0),
new(2, "Animal Lore", 0.0, 0.0, 0.0, "Naturalist", null, 0.0, 0.0, 1.0, 1.0),
- new(3, "Item Identification", 0.0, 0.0, 0.0, "Merchant", null, 0.0, 0.0, 1.0, 1.0),
+ new(3, "Item Identification", 0.0, 0.0, 0.0, "Merchant", null, 0.0, 0.0, 1.0, 1.0, "ItemID"),
new(4, "Arms Lore", 0.0, 0.0, 0.0, "Weapon Master", null, 0.75, 0.15, 0.1, 1.0),
new(5, "Parrying", 7.5, 2.5, 0.0, "Duelist", null, 0.75, 0.25, 0.0, 1.0),
new(6, "Begging", 0.0, 0.0, 0.0, "Beggar", null, 0.0, 0.0, 0.0, 1.0),
- new(7, "Blacksmithy", 10.0, 0.0, 0.0, "Blacksmith", null, 1.0, 0.0, 0.0, 1.0),
- new(8, "Bowcraft/Fletching", 6.0, 16.0, 0.0, "Bowyer", null, 0.6, 1.6, 0.0, 1.0),
+ new(7, "Blacksmithy", 10.0, 0.0, 0.0, "Blacksmith", null, 1.0, 0.0, 0.0, 1.0, "Blacksmith"),
+ new(8, "Bowcraft/Fletching", 6.0, 16.0, 0.0, "Bowyer", null, 0.6, 1.6, 0.0, 1.0, "Bowcraft"),
new(9, "Peacemaking", 0.0, 0.0, 0.0, "Pacifier", null, 0.0, 0.0, 0.0, 1.0),
new(10, "Camping", 20.0, 15.0, 15.0, "Explorer", null, 2.0, 1.5, 1.5, 1.0),
new(11, "Carpentry", 20.0, 5.0, 0.0, "Carpenter", null, 2.0, 0.5, 0.0, 1.0),
new(12, "Cartography", 0.0, 7.5, 7.5, "Cartographer", null, 0.0, 0.75, 0.75, 1.0),
new(13, "Cooking", 0.0, 20.0, 30.0, "Chef", null, 0.0, 2.0, 3.0, 1.0),
new(14, "Detecting Hidden", 0.0, 0.0, 0.0, "Scout", null, 0.0, 0.4, 0.6, 1.0),
- new(15, "Discordance", 0.0, 2.5, 2.5, "Demoralizer", null, 0.0, 0.25, 0.25, 1.0),
- new(16, "Evaluating Intelligence", 0.0, 0.0, 0.0, "Scholar", null, 0.0, 0.0, 1.0, 1.0),
+ new(15, "Discordance", 0.0, 2.5, 2.5, "Demoralizer", null, 0.0, 0.25, 0.25, 1.0, "Enticement"),
+ new(16, "Evaluating Intelligence", 0.0, 0.0, 0.0, "Scholar", null, 0.0, 0.0, 1.0, 1.0, "EvaluateIntelligence"),
new(17, "Healing", 6.0, 6.0, 8.0, "Healer", null, 0.6, 0.6, 0.8, 1.0),
new(18, "Fishing", 0.0, 0.0, 0.0, "Fisherman", null, 0.5, 0.5, 0.0, 1.0),
new(19, "Forensic Evaluation", 0.0, 0.0, 0.0, "Detective", null, 0.0, 0.2, 0.8, 1.0),
@@ -476,7 +480,7 @@ namespace Server
new(45, "Mining", 20.0, 0.0, 0.0, "Miner", null, 2.0, 0.0, 0.0, 1.0),
new(46, "Meditation", 0.0, 0.0, 0.0, "Stoic", null, 0.0, 0.0, 0.0, 1.0),
new(47, "Stealth", 0.0, 0.0, 0.0, "Rogue", null, 0.0, 0.0, 0.0, 1.0),
- new(48, "Remove Trap", 0.0, 0.0, 0.0, "Trap Specialist", null, 0.0, 0.0, 0.0, 1.0),
+ new(48, "Remove Trap", 0.0, 0.0, 0.0, "Trap Specialist", null, 0.0, 0.0, 0.0, 1.0, "Disarm"),
new(49, "Necromancy", 0.0, 0.0, 0.0, "Necromancer", null, 0.0, 0.0, 0.0, 1.0),
new(50, "Focus", 0.0, 0.0, 0.0, "Driven", null, 0.0, 0.0, 0.0, 1.0),
new(51, "Chivalry", 0.0, 0.0, 0.0, "Paladin", null, 0.0, 0.0, 0.0, 1.0),
@@ -892,16 +896,16 @@ namespace Server
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public SkillsEnumerator GetEnumerator() => new(m_Skills);
+ public Enumerator GetEnumerator() => new(m_Skills);
- public ref struct SkillsEnumerator
+ public ref struct Enumerator
{
private readonly Skill[] _skills;
private int _index;
private Skill _current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal SkillsEnumerator(Skill[] skills)
+ internal Enumerator(Skill[] skills)
{
_skills = skills;
_index = 0;
diff --git a/Projects/Server/Text/StringHelpers.cs b/Projects/Server/Text/StringHelpers.cs
index 7e05497c2..cf97b6a7c 100644
--- a/Projects/Server/Text/StringHelpers.cs
+++ b/Projects/Server/Text/StringHelpers.cs
@@ -169,6 +169,28 @@ namespace Server
return str;
}
+ public static string TrimMultiline(this string str, string lineSeparator = "\n")
+ {
+ var parts = str.Split(lineSeparator);
+ for (var i = 0; i < parts.Length; i++)
+ {
+ parts[i] = parts[i].Trim();
+ }
+
+ return string.Join(lineSeparator, parts);
+ }
+
+ public static string IndentMultiline(this string str, string indent = "\t", string lineSeparator = "\n")
+ {
+ var parts = str.Split(lineSeparator);
+ for (var i = 0; i < parts.Length; i++)
+ {
+ parts[i] = $"{indent}{parts[i]}";
+ }
+
+ return string.Join(lineSeparator, parts);
+ }
+
public static List Wrap(this string value, int perLine, int maxLines)
{
if ((value = value?.Trim() ?? "").Length <= 0)
diff --git a/Projects/Server/TileData.cs b/Projects/Server/TileData.cs
index 3434d0455..6e2b66aaf 100644
--- a/Projects/Server/TileData.cs
+++ b/Projects/Server/TileData.cs
@@ -104,11 +104,28 @@ namespace Server
}
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
- public int CalcHeight => (Flags & TileFlag.Bridge) != 0 ? _height / 2 : _height;
+ public int CalcHeight => Bridge ? _height / 2 : _height;
+
+ [CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
+ public bool Door
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this[TileFlag.Door];
+ set => this[TileFlag.Door] = value;
+ }
+
+ [CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
+ public bool Background
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this[TileFlag.Background];
+ set => this[TileFlag.Background] = value;
+ }
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
public bool Bridge
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this[TileFlag.Bridge];
set => this[TileFlag.Bridge] = value;
}
@@ -116,6 +133,7 @@ namespace Server
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
public bool Wall
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this[TileFlag.Wall];
set => this[TileFlag.Wall] = value;
}
@@ -123,13 +141,22 @@ namespace Server
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
public bool Window
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this[TileFlag.Window];
set => this[TileFlag.Window] = value;
}
+ public bool ImpassableSurface
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this[TileFlag.Impassable | TileFlag.Surface];
+ set => this[TileFlag.Impassable | TileFlag.Surface] = value;
+ }
+
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
public bool Impassable
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this[TileFlag.Impassable];
set => this[TileFlag.Impassable] = value;
}
@@ -137,6 +164,7 @@ namespace Server
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
public bool Surface
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this[TileFlag.Surface];
set => this[TileFlag.Surface] = value;
}
@@ -144,6 +172,7 @@ namespace Server
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
public bool Roof
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this[TileFlag.Roof];
set => this[TileFlag.Roof] = value;
}
@@ -151,10 +180,19 @@ namespace Server
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
public bool LightSource
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this[TileFlag.LightSource];
set => this[TileFlag.LightSource] = value;
}
+ [CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
+ public bool Wet
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this[TileFlag.Wet];
+ set => this[TileFlag.Wet] = value;
+ }
+
public bool this[TileFlag flag]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/Projects/Server/TimeZones/TimeZoneHandler.cs b/Projects/Server/TimeZones/TimeZoneHandler.cs
new file mode 100644
index 000000000..5e9450f02
--- /dev/null
+++ b/Projects/Server/TimeZones/TimeZoneHandler.cs
@@ -0,0 +1,80 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2021 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: TimeZoneHandler.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+using Server.Json;
+
+namespace Server
+{
+ /**
+ * TimeZoneHandler provides a cross platform compatible way to handle system timezones.
+ *
+ * By default the system timezone is used.
+ * To manually configure the local timezone add the following setting to modernuo.json:
+ * "system.localTimeZone": "Pacific Standard Time"
+ * All valid timezones can be found in the Distribution/Data/timezones.json file.
+ *
+ * Notes for timezones.json:
+ * By default, Windows will use the values in the "timezone" property.
+ * By default, Unix/MacOS will use the values in the "region" property.
+ * system.localTimeZone can be either timezone or a region.
+ *
+ * Example:
+ * "system.localTimeZone": "Europe/Lisbon"
+ */
+ public static class TimeZoneHandler
+ {
+ private static readonly Dictionary _timeZoneById = new();
+ public static TimeZoneInfo SystemTimeZone { get; private set; }
+
+ static TimeZoneHandler()
+ {
+ var timezones = JsonConfig.Deserialize(Path.Combine(Core.BaseDirectory, "Data/timezones.json"));
+ foreach (var tz in timezones)
+ {
+ // Get the timezone if we are on Windows
+ var tzInfo = Core.IsWindows ? TimeZoneInfo.FindSystemTimeZoneById(tz.TimeZone) : null;
+
+ foreach (var region in tz.Regions)
+ {
+ // Get the timezone by region if we are on linux
+ tzInfo ??= TimeZoneInfo.FindSystemTimeZoneById(region);
+ _timeZoneById[region] = tzInfo;
+ }
+
+ _timeZoneById[tz.TimeZone] = tzInfo;
+ }
+ }
+
+ public static void Configure()
+ {
+ var tzId = ServerConfiguration.GetSetting("system.localTimeZone", TimeZoneInfo.Local.Id);
+ SystemTimeZone = FindTimeZoneById(tzId);
+ }
+
+ /**
+ * Cross platform version of TimeZoneInfo.FindSystemTimeZoneById
+ */
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static TimeZoneInfo FindTimeZoneById(string id) => _timeZoneById[id];
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static DateTime ToSystemLocalTime(this DateTime date) =>
+ TimeZoneInfo.ConvertTimeFromUtc(date, SystemTimeZone);
+ }
+}
diff --git a/Projects/Server/TimeZones/TimeZoneWithRegions.cs b/Projects/Server/TimeZones/TimeZoneWithRegions.cs
new file mode 100644
index 000000000..4c19a0086
--- /dev/null
+++ b/Projects/Server/TimeZones/TimeZoneWithRegions.cs
@@ -0,0 +1,28 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2021 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: TimeZoneWithRegions.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System.Text.Json.Serialization;
+
+namespace Server
+{
+ public record TimeZoneWithRegions
+ {
+ [JsonPropertyName("timezone")]
+ public string TimeZone { get; init; }
+
+ [JsonPropertyName("regions")]
+ public string[] Regions { get; init; }
+ }
+}
diff --git a/Projects/Server/Timer/Timer.DelayCall.cs b/Projects/Server/Timer/Timer.DelayCall.cs
index 11d6b84c6..5d98180bb 100644
--- a/Projects/Server/Timer/Timer.DelayCall.cs
+++ b/Projects/Server/Timer/Timer.DelayCall.cs
@@ -71,7 +71,7 @@ namespace Server
public static void StartTimer(TimeSpan delay, TimeSpan interval, int count, Action callback)
{
DelayCallTimer t = DelayCallTimer.GetTimer(delay, interval, count, callback);
- t._selfReturn = true;
+ t._returnOnDetach = true;
t.Start();
#if DEBUG_TIMERS
@@ -115,7 +115,7 @@ namespace Server
public sealed class DelayCallTimer : Timer, INotifyCompletion
{
- internal bool _selfReturn;
+ internal bool _returnOnDetach;
#if DEBUG_TIMERS
internal bool _allowFinalization;
#endif
@@ -143,57 +143,50 @@ namespace Server
_continuation?.Invoke();
}
- public override void Stop()
- {
- base.Stop();
-
- if (_selfReturn)
- {
- Return();
- }
- }
-
- internal void Return()
+ internal override void OnDetach()
{
if (Running)
{
- logger.Error($"Timer is returned while still running! {new StackTrace()}");
+ logger.Error($"Timer is returned while still running!\n{new StackTrace()}");
return;
}
- Version++; // Increment the version so if this is called from OnTick() and another timer is started, we don't have a problem
-
-#if DEBUG_TIMERS
- _stackTraces.Remove(GetHashCode());
-#endif
-
- if (_poolCount >= _poolCapacity)
+ if (_returnOnDetach)
{
-#if DEBUG_TIMERS
- logger.Warning($"DelayCallTimer pool reached maximum of {_poolCapacity} timers");
- _allowFinalization = true;
-#endif
- return;
- }
+ // Increment the version so if this is called from OnTick() and another timer is started, we don't have a problem
+ // Version++;
- _continuation = null;
- ReturnToPool(1, this, this);
+#if DEBUG_TIMERS
+ _stackTraces.Remove(GetHashCode());
+#endif
+
+ if (_poolCount >= _poolCapacity)
+ {
+#if DEBUG_TIMERS
+ logger.Warning($"DelayCallTimer pool reached maximum of {_poolCapacity} timers");
+ _allowFinalization = true;
+#endif
+ return;
+ }
+
+ _continuation = null;
+ _returnOnDetach = false;
+ ReturnToPool(1, this, this);
+ }
}
public static DelayCallTimer GetTimer(TimeSpan delay, TimeSpan interval, int count, Action callback)
{
- if (_poolHead != null)
+ var timer = GetFromPool();
+ if (timer != null)
{
- _poolCount--;
#if DEBUG_TIMERS
- logger.Information($"Pool count: {_poolCount} / {_poolCapacity}");
+ logger.Information($"Getting from pool: ({_poolCount} / {_poolCapacity})");
#endif
- var timer = GetFromPool();
-
timer.Init(delay, interval, count);
timer._continuation = callback;
- timer._selfReturn = false;
+ timer._returnOnDetach = false;
#if DEBUG_TIMERS
timer._allowFinalization = false;
#endif
diff --git a/Projects/Server/Timer/Timer.Pool.cs b/Projects/Server/Timer/Timer.Pool.cs
index a7db9c273..cb92fc18f 100644
--- a/Projects/Server/Timer/Timer.Pool.cs
+++ b/Projects/Server/Timer/Timer.Pool.cs
@@ -62,22 +62,29 @@ namespace Server
_poolHead = head;
_poolCount += amount;
#if DEBUG_TIMERS
- logger.Information($"Pool count: {_poolCount} / {_poolCapacity}");
+ logger.Information($"Returning to pool. ({_poolCount} / {_poolCapacity})");
#endif
}
private static DelayCallTimer GetFromPool()
{
+ if (_poolHead == null)
+ {
+ return null;
+ }
+
var timer = _poolHead;
_poolHead = _poolHead._nextTimer as DelayCallTimer;
timer.Detach();
+ _poolCount--;
+
return timer;
}
internal static void RefillPool(int amount, out DelayCallTimer head, out DelayCallTimer tail)
{
#if DEBUG_TIMERS
- logger.Information($"Filling pool with {amount} timers.");
+ logger.Information($"Filling pool with {amount} timers.");
#endif
head = null;
diff --git a/Projects/Server/Timer/Timer.TimerWheel.cs b/Projects/Server/Timer/Timer.TimerWheel.cs
index e1af8df8e..5e9d66334 100644
--- a/Projects/Server/Timer/Timer.TimerWheel.cs
+++ b/Projects/Server/Timer/Timer.TimerWheel.cs
@@ -28,11 +28,12 @@ namespace Server
private const int _tickRatePowerOf2 = 3;
private const int _tickRate = 1 << _tickRatePowerOf2; // 8ms
- private static long _lastTickTurned = -1;
-
private static readonly Timer[][] _rings = new Timer[_ringLayers][];
private static readonly int[] _ringIndexes = new int[_ringLayers];
+ private static long _lastTickTurned = -1;
+ private static bool _timerWheelExecuting;
+
public static void Init(long tickCount)
{
_lastTickTurned = tickCount;
@@ -44,46 +45,63 @@ namespace Server
}
}
- public static int Slice(long tickCount)
+ public static void Slice(long tickCount)
{
var deltaSinceTurn = tickCount - _lastTickTurned;
- var events = 0;
while (deltaSinceTurn >= _tickRate)
{
deltaSinceTurn -= _tickRate;
_lastTickTurned += _tickRate;
- events += Turn() ? 1 : 0;
+ Turn();
}
-
- return events;
}
- private static bool Turn()
+ private static void Turn()
{
- bool events = false;
+ _timerWheelExecuting = true;
+ var turnNextWheel = false;
+
+ var _executingRings = new Timer[_ringLayers];
+
+ // Detach the chain from the timer wheel. This allows adding timers to the same slot during execution.
+ for (var i = 0; i < _ringLayers; i++)
+ {
+ if (i == 0 || turnNextWheel)
+ {
+ var ringIndex = ++_ringIndexes[i];
+ turnNextWheel = ringIndex >= _ringSize;
+
+ if (turnNextWheel)
+ {
+ ringIndex = _ringIndexes[i] = 0;
+ }
+
+ _executingRings[i] = _rings[i][ringIndex];
+ _rings[i][ringIndex] = null;
+ }
+ else
+ {
+ _executingRings[i] = null;
+ }
+ }
for (var i = 0; i < _ringLayers; i++)
{
- // Increment the ring index, then get the timer.
- var ringIndex = ++_ringIndexes[i];
- bool turnNextWheel = ringIndex >= _ringSize;
-
- if (turnNextWheel)
+ var timer = _executingRings[i];
+ if (timer == null)
{
- ringIndex = _ringIndexes[i] = 0;
+ continue;
}
- var timer = _rings[i][ringIndex];
- if (timer != null)
+ do
{
- events = true;
+ var next = timer._nextTimer;
- do
+ timer.Detach();
+
+ // Check to see if it's running just in case it was stopped by another timer
+ if (timer.Running)
{
- var next = _rings[i][ringIndex] = timer._nextTimer;
-
- timer.Detach();
-
if (i > 0 && timer._remaining > 0)
{
// Promote
@@ -93,18 +111,18 @@ namespace Server
{
Execute(timer);
}
+ }
- timer = next;
- } while (timer != null);
- }
+ if (!timer.Running)
+ {
+ timer.OnDetach();
+ }
- if (!turnNextWheel)
- {
- break;
- }
+ timer = next;
+ } while (timer != null);
}
- return events;
+ _timerWheelExecuting = false;
}
private static void Execute(Timer timer)
@@ -136,14 +154,17 @@ namespace Server
private static void AddTimer(Timer timer, long delay)
{
+#if DEBUG_TIMERS
+ var originalDelay = delay;
+#endif
delay = Math.Max(0, delay);
var resolutionPowerOf2 = _tickRatePowerOf2;
for (var i = 0; i < _ringLayers; i++)
{
- var resolution = 1 << resolutionPowerOf2;
+ var resolution = 1L << resolutionPowerOf2;
var nextResolutionPowerOf2 = resolutionPowerOf2 + _ringSizePowerOf2;
- long max = 1 << nextResolutionPowerOf2;
+ var max = 1L << nextResolutionPowerOf2;
if (delay < max)
{
var remaining = delay & (resolution - 1);
@@ -168,13 +189,18 @@ namespace Server
_rings[i][slot] = timer;
- break;
+ return;
}
// The remaining amount until we turn this ring
delay -= resolution * (_ringSize - _ringIndexes[i]);
resolutionPowerOf2 = nextResolutionPowerOf2;
}
+
+ // TODO: Handle timers > 17yrs
+#if DEBUG_TIMERS
+ logger.Error($"Timer is more than max duration. ({originalDelay})");
+#endif
}
public static void DumpInfo(TextWriter tw)
@@ -216,7 +242,7 @@ namespace Server
}
#if DEBUG_TIMERS
- tw.WriteLine("\nStack Traces:");
+ tw.WriteLine($"{Environment.NewLine}Stack Traces:");
foreach (var kvp in DelayCallTimer._stackTraces)
{
tw.WriteLine(kvp.Value);
diff --git a/Projects/Server/Timer/Timer.cs b/Projects/Server/Timer/Timer.cs
index 7d10bf441..786d1c86c 100644
--- a/Projects/Server/Timer/Timer.cs
+++ b/Projects/Server/Timer/Timer.cs
@@ -35,9 +35,9 @@ namespace Server
private Timer _nextTimer;
private Timer _prevTimer;
- public Timer(TimeSpan delay) : this(delay, TimeSpan.Zero, 1)
- {
- }
+ public Timer(TimeSpan delay) => Init(delay, TimeSpan.Zero, 1);
+
+ public Timer(TimeSpan interval, int count) => Init(interval, interval, count);
public Timer(TimeSpan delay, TimeSpan interval, int count = 0) => Init(delay, interval, count);
@@ -102,17 +102,22 @@ namespace Server
return;
}
- // We are at the head
- if (_rings[_ring][_slot] == this)
+ // Do not detach if we are in the middle of executing the timer wheel for this ring/slot
+ if (!_timerWheelExecuting || _ringIndexes[_ring] != _slot)
{
- _rings[_ring][_slot] = _nextTimer;
+ // We are at the head
+ if (_rings[_ring][_slot] == this)
+ {
+ _rings[_ring][_slot] = _nextTimer;
+ }
+
+ Detach();
}
- Detach();
Running = false;
Version++;
- var prof = GetProfile();
+ var prof = GetProfile();
if (prof != null)
{
prof.Stopped++;
@@ -147,5 +152,9 @@ namespace Server
_nextTimer = null;
_prevTimer = null;
}
+
+ internal virtual void OnDetach()
+ {
+ }
}
}
diff --git a/Projects/Server/Timer/TimerExecutionToken.cs b/Projects/Server/Timer/TimerExecutionToken.cs
index 9535e726e..9e9992e3b 100644
--- a/Projects/Server/Timer/TimerExecutionToken.cs
+++ b/Projects/Server/Timer/TimerExecutionToken.cs
@@ -50,9 +50,12 @@ namespace Server
public void Cancel()
{
- _timer?.Stop();
- _timer?.Return();
- _timer = null;
+ if (_timer != null)
+ {
+ _timer._returnOnDetach = true;
+ _timer?.Stop();
+ _timer = null;
+ }
this = default;
}
diff --git a/Projects/Server/Utilities/PathUtility.cs b/Projects/Server/Utilities/PathUtility.cs
new file mode 100644
index 000000000..d2206143d
--- /dev/null
+++ b/Projects/Server/Utilities/PathUtility.cs
@@ -0,0 +1,67 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2021 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: PathUtility.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.IO;
+using Server.Text;
+
+namespace Server
+{
+ public static class PathUtility
+ {
+ public static string EnsureDirectory(string dir)
+ {
+ var path = GetFullPath(dir, Core.BaseDirectory);
+ Directory.CreateDirectory(path);
+
+ return path;
+ }
+
+ public static void EnsureDirectory(this FileInfo fi)
+ {
+ var dir = GetFullPath(fi.DirectoryName, Core.BaseDirectory);
+ if (dir != null)
+ {
+ Directory.CreateDirectory(dir);
+ }
+ }
+
+ public static void EnsureDirectory(this DirectoryInfo di)
+ {
+ var file = GetFullPath(di.FullName, Core.BaseDirectory);
+ Directory.CreateDirectory(file);
+ }
+
+ public static string GetFullPath(string relativeOrAbsolutePath) =>
+ GetFullPath(relativeOrAbsolutePath, Core.BaseDirectory);
+
+ public static string GetFullPath(string relativeOrAbsolutePath, string basePath) =>
+ relativeOrAbsolutePath switch
+ {
+ null => null,
+ "" => basePath,
+ _ => Path.IsPathRooted(relativeOrAbsolutePath)
+ ? relativeOrAbsolutePath
+ : Path.GetFullPath(relativeOrAbsolutePath, basePath)
+ };
+
+ public static string EnsureRandomPath(string basePath)
+ {
+ Span bytes = stackalloc byte[8];
+ Utility.RandomBytes(bytes);
+ return EnsureDirectory(Path.Combine(basePath, bytes.ToHexString()));
+ }
+ }
+}
diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs
index 430dd0907..286e488e7 100644
--- a/Projects/Server/Utilities/Utility.cs
+++ b/Projects/Server/Utilities/Utility.cs
@@ -484,7 +484,6 @@ namespace Server
break;
}
-
num = BinaryPrimitives.ReadUInt16BigEndian(ip.Slice(byteIndex, 2));
byteIndex += 2;
@@ -927,14 +926,11 @@ namespace Server
return outputList;
}
- public static bool ToBoolean(string value)
- {
-#pragma warning disable CA1806 // Do not ignore method results
- bool.TryParse(value, out var b);
-#pragma warning restore CA1806 // Do not ignore method results
-
- return b;
- }
+ public static bool ToBoolean(string value) =>
+ bool.TryParse(value, out var b) && b ||
+ value.InsensitiveEquals("enabled") ||
+ value.InsensitiveEquals("on") ||
+ !value.InsensitiveEquals("disabled") && !value.InsensitiveEquals("off");
public static double ToDouble(string value)
{
@@ -1335,28 +1331,6 @@ namespace Server
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Max(T val, T max) where T : IComparable => val.CompareTo(max) > 0 ? val : max;
- public static string TrimMultiline(this string str, string lineSeparator = "\n")
- {
- var parts = str.Split(lineSeparator);
- for (var i = 0; i < parts.Length; i++)
- {
- parts[i] = parts[i].Trim();
- }
-
- return string.Join(lineSeparator, parts);
- }
-
- public static string IndentMultiline(this string str, string indent = "\t", string lineSeparator = "\n")
- {
- var parts = str.Split(lineSeparator);
- for (var i = 0; i < parts.Length; i++)
- {
- parts[i] = $"{indent}{parts[i]}";
- }
-
- return string.Join(lineSeparator, parts);
- }
-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Tidy(this List list) where T : ISerializable
{
@@ -1484,7 +1458,10 @@ namespace Server
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static string GetTimeStamp() => Core.Now.ToString("yyyy-MM-dd-HH-mm-ss");
+ public static string GetTimeStamp() => Core.Now.ToTimeStamp();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string ToTimeStamp(this DateTime dt) => dt.ToString("yyyy-MM-dd-HH-mm-ss");
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add(ref List list, T value)
@@ -1639,5 +1616,26 @@ namespace Server
dict.Clear();
dict = null;
}
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void DateToComponents(
+ DateTime date,
+ out int year,
+ out int month,
+ out int day,
+ out DayOfWeek dayOfWeek,
+ out int hour,
+ out int min,
+ out int sec
+ )
+ {
+ year = date.Year;
+ month = date.Month;
+ day = date.Day;
+ dayOfWeek = date.DayOfWeek;
+ hour = date.Hour;
+ min = date.Minute;
+ sec = date.Second;
+ }
}
}
diff --git a/Projects/Server/World/EntityPersistence.cs b/Projects/Server/World/EntityPersistence.cs
index 93c597bba..bf2f415df 100644
--- a/Projects/Server/World/EntityPersistence.cs
+++ b/Projects/Server/World/EntityPersistence.cs
@@ -24,6 +24,8 @@ namespace Server
{
public static class EntityPersistence
{
+ private const int _idxVersion = 1;
+
public static void WriteEntities(
IIndexInfo indexInfo,
Dictionary entities,
@@ -38,7 +40,7 @@ namespace Server
var path = Path.Combine(savePath, typeName);
- AssemblyHandler.EnsureDirectory(path);
+ PathUtility.EnsureDirectory(path);
string idxPath = Path.Combine(path, $"{typeName}.idx");
string tdbPath = Path.Combine(path, $"{typeName}.tdb");
@@ -48,6 +50,7 @@ namespace Server
using var tdb = new BinaryFileWriter(tdbPath, false);
using var bin = new BinaryFileWriter(binPath, true);
+ idx.Write(1); // Version
idx.Write(entities.Count);
foreach (var e in entities.Values)
{
@@ -55,6 +58,8 @@ namespace Server
idx.Write(e.TypeRef);
idx.Write(e.Serial);
+ idx.Write(e.Created.Ticks);
+ idx.Write(e.LastSerialized.Ticks);
idx.Write(start);
e.SerializeTo(bin);
@@ -110,12 +115,28 @@ namespace Server
List> types = ReadTypes(tdbReader);
- var count = idxReader.ReadInt32();
+ int count;
+ var version = idxReader.ReadInt32();
+
+ // Handle non-versioned (version 0).
+ if (version > _idxVersion || idx.Length - 4 - version * 20 == 0)
+ {
+ count = version;
+ version = 0;
+ }
+ else
+ {
+ count = idxReader.ReadInt32();
+ }
+
+ var now = DateTime.UtcNow;
for (int i = 0; i < count; ++i)
{
var typeID = idxReader.ReadInt32();
- var number = idxReader.ReadUInt32();
+ var serial = idxReader.ReadUInt32();
+ var created = version == 0 ? now : new DateTime(idxReader.ReadInt64(), DateTimeKind.Utc);
+ var lastSerialized = version == 0 ? DateTime.MinValue : new DateTime(idxReader.ReadInt64(), DateTimeKind.Utc);
var pos = idxReader.ReadInt64();
var length = idxReader.ReadInt32();
@@ -127,12 +148,14 @@ namespace Server
}
ConstructorInfo ctor = objs.Item1;
- I indexer = indexInfo.CreateIndex(number);
+ I indexer = indexInfo.CreateIndex(serial);
ctorArgs[0] = indexer;
if (ctor.Invoke(ctorArgs) is T t)
{
+ t.Created = created;
+ t.LastSerialized = lastSerialized;
entities.Add(new EntityIndex(t, typeID, pos, length));
map[indexer] = t;
}
@@ -180,7 +203,7 @@ namespace Server
var buffer = GC.AllocateUninitializedArray(entry.Length);
if (br == null)
{
- br = new BufferReader(buffer);
+ br = new BufferReader(buffer, t.LastSerialized);
}
else
{
diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs
index 0120d2991..02b3013d5 100644
--- a/Projects/Server/World/World.cs
+++ b/Projects/Server/World/World.cs
@@ -46,7 +46,7 @@ namespace Server
private static readonly ConcurrentQueue- _decayQueue = new();
private static string _tempSavePath; // Path to the temporary folder for the save
- private static string _savePath; // Path to "Saves" folder
+ private static bool _enableSaveStats;
public const bool DirtyTrackingEnabled = false;
public const uint ItemOffset = 0x40000000;
@@ -129,9 +129,11 @@ namespace Server
internal static List MobileTypes { get; } = new();
internal static List GuildTypes { get; } = new();
+ public static string SavePath { get; private set; }
+
public static WorldState WorldState { get; private set; }
public static bool Saving => WorldState == WorldState.Saving;
- public static bool Running => WorldState != WorldState.Loading && WorldState != WorldState.Initial;
+ public static bool Running => WorldState is not WorldState.Loading and not WorldState.Initial;
public static bool Loading => WorldState == WorldState.Loading;
public static Dictionary Mobiles { get; private set; }
@@ -140,10 +142,13 @@ namespace Server
public static void Configure()
{
- var tempSavePath = ServerConfiguration.GetOrUpdateSetting("world.tempSavePath", "temp");
- _tempSavePath = Path.Combine(Core.BaseDirectory, tempSavePath);
+ var tempSavePath = ServerConfiguration.GetSetting("world.tempSavePath", "temp");
+ _tempSavePath = PathUtility.GetFullPath(tempSavePath);
+
var savePath = ServerConfiguration.GetOrUpdateSetting("world.savePath", "Saves");
- _savePath = Path.Combine(Core.BaseDirectory, savePath);
+ SavePath = PathUtility.GetFullPath(savePath);
+
+ _enableSaveStats = ServerConfiguration.GetOrUpdateSetting("world.enableSaveStats", false);
// Mobiles & Items
Persistence.Register("Mobiles & Items", SaveEntities, WriteEntities, LoadEntities, 1);
@@ -254,7 +259,7 @@ namespace Server
logger.Information("Loading world");
var watch = Stopwatch.StartNew();
- Persistence.Load(_savePath);
+ Persistence.Load(SavePath);
EventSink.InvokeWorldLoad();
ProcessSafetyQueues();
@@ -341,7 +346,10 @@ namespace Server
int count = 0;
var timestamp = Utility.GetTimeStamp();
- using var op = new StreamWriter("Logs/Saves/Save-Stats-{0}.log", true);
+ var saveStatsPath = Path.Combine(Core.BaseDirectory, $"Logs/Saves/Save-Stats-{timestamp}.log");
+ PathUtility.EnsureDirectory(saveStatsPath);
+
+ using var op = new StreamWriter(saveStatsPath, true);
for (var i = 0; i < entityTypes.Length; i++)
{
@@ -373,14 +381,17 @@ namespace Server
EntityPersistence.WriteEntities(itemIndexInfo, Items, ItemTypes, basePath, out var itemCounts);
EntityPersistence.WriteEntities(guildIndexInfo, Guilds, GuildTypes, basePath, out var guildCounts);
- TraceSave(mobileCounts?.ToList(), itemCounts?.ToList(), guildCounts?.ToList());
+ if (_enableSaveStats)
+ {
+ TraceSave(mobileCounts?.ToList(), itemCounts?.ToList(), guildCounts?.ToList());
+ }
}
public static void WriteFiles(object state)
{
Exception exception = null;
- var tempPath = Path.Combine(_tempSavePath, Utility.GetTimeStamp());
+ var tempPath = PathUtility.EnsureRandomPath(_tempSavePath);
try
{
@@ -409,8 +420,8 @@ namespace Server
{
try
{
- EventSink.InvokeWorldSavePostSnapshot(_savePath, tempPath);
- Directory.Move(tempPath, _savePath);
+ EventSink.InvokeWorldSavePostSnapshot(SavePath, tempPath);
+ Directory.Move(tempPath, SavePath);
}
catch (Exception ex)
{
@@ -652,13 +663,24 @@ namespace Server
Serial serial = reader.ReadSerial();
var typeT = typeof(T);
+ T entity;
+
// Add to this list when creating new serializable types
if (typeof(BaseGuild).IsAssignableTo(typeT))
{
- return FindGuild(serial) as T;
+ entity = FindGuild(serial) as T;
+ }
+ else
+ {
+ entity = FindEntity(serial) as T;
}
- return FindEntity(serial) as T;
+ if (entity?.Deleted == false)
+ {
+ return entity;
+ }
+
+ return entity?.Created <= reader.LastSerialized ? entity : null;
}
public static List ReadEntityList(this IGenericReader reader) where T : class, ISerializable
diff --git a/Projects/UOContent.Tests/Fixtures/ServerFixture.cs b/Projects/UOContent.Tests/Fixtures/ServerFixture.cs
index ab391505b..a5cbdca8e 100644
--- a/Projects/UOContent.Tests/Fixtures/ServerFixture.cs
+++ b/Projects/UOContent.Tests/Fixtures/ServerFixture.cs
@@ -1,4 +1,5 @@
using System;
+using System.Reflection;
using Server.Misc;
namespace Server.Tests
@@ -8,8 +9,8 @@ namespace Server.Tests
// Global setup
static ServerFixture()
{
+ Core.Assembly = Assembly.GetExecutingAssembly();
Core.LoopContext = new EventLoopContext();
-
Core.Expansion = Expansion.EJ;
// Load Configurations
diff --git a/Projects/UOContent.Tests/UOContent.Tests.csproj b/Projects/UOContent.Tests/UOContent.Tests.csproj
index 2fd4f6f7b..baa2a972e 100644
--- a/Projects/UOContent.Tests/UOContent.Tests.csproj
+++ b/Projects/UOContent.Tests/UOContent.Tests.csproj
@@ -3,7 +3,7 @@
false
-
+
diff --git a/Projects/UOContent/Accounting/Account.Migrations.cs b/Projects/UOContent/Accounting/Account.Migrations.cs
new file mode 100644
index 000000000..dd9148b01
--- /dev/null
+++ b/Projects/UOContent/Accounting/Account.Migrations.cs
@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using Server.Accounting.Security;
+
+namespace Server.Accounting
+{
+ public partial class Account
+ {
+ private void MigrateFrom(V3Content content)
+ {
+ _username = content.Username;
+ _passwordAlgorithm = content.PasswordAlgorithm;
+ _password = content.Password;
+ _accessLevel = content.AccessLevel;
+ _flags = content.Flags;
+ Created = content.Created;
+ _lastLogin = content.LastLogin;
+ _totalGold = content.TotalGold;
+ _totalPlat = content.TotalPlat;
+ _mobiles = content.Mobiles;
+ _comments = content.Comments;
+ _tags = content.Tags;
+ _loginIPs = content.LoginIPs;
+ _ipRestrictions = content.IpRestrictions;
+ _totalGameTime = content.TotalGameTime;
+ _email = content.Email;
+ }
+
+ private void Deserialize(IGenericReader reader, int version)
+ {
+ if (version != 2)
+ {
+ // Due to a bug where we were not versioning at all, reset so we don't have an issue deserializing
+ reader.Seek(0, SeekOrigin.Begin);
+ }
+
+ _username = reader.ReadString();
+ _passwordAlgorithm = version < 2 ? (PasswordProtectionAlgorithm)reader.ReadInt() : reader.ReadEnum();
+ _password = reader.ReadString();
+ _accessLevel = version < 2 ? (AccessLevel)reader.ReadInt() : reader.ReadEnum();
+ _flags = reader.ReadInt();
+ Created = reader.ReadDateTime();
+ _lastLogin = reader.ReadDateTime();
+
+ _totalGold = reader.ReadInt();
+ _totalPlat = reader.ReadInt();
+
+ var length = reader.ReadInt();
+ _mobiles = new Mobile[length];
+ for (int i = 0; i < length; i++)
+ {
+ _mobiles[i] = reader.ReadEntity();
+ }
+
+ length = reader.ReadInt();
+ _comments = length > 0 ? new List(length) : null;
+ for (int i = 0; i < length; i++)
+ {
+ _comments!.Add(new AccountComment(reader));
+ }
+
+ length = reader.ReadInt();
+ _tags = length > 0 ? new List(length) : null;
+ for (int i = 0; i < length; i++)
+ {
+ _tags!.Add(new AccountTag(reader));
+ }
+
+ length = reader.ReadInt();
+ _loginIPs = new IPAddress[length];
+ for (int i = 0; i < length; i++)
+ {
+ if (version < 2)
+ {
+ if (IPAddress.TryParse(reader.ReadString(), out var address))
+ {
+ _loginIPs[i] = Utility.Intern(address);
+ }
+ }
+ else
+ {
+ _loginIPs[i] = reader.ReadIPAddress();
+ }
+ }
+
+ length = reader.ReadInt();
+ _ipRestrictions = new string[length];
+ for (int i = 0; i < length; i++)
+ {
+ _ipRestrictions[i] = reader.ReadString();
+ }
+
+ _totalGameTime = reader.ReadTimeSpan();
+
+ if (version > 1)
+ _email = reader.ReadString();
+
+ Timer.StartTimer(AfterDeserialization);
+ }
+ }
+}
diff --git a/Projects/UOContent/Accounting/Account.cs b/Projects/UOContent/Accounting/Account.cs
index 02a3cc2ff..43a1f8fb8 100644
--- a/Projects/UOContent/Accounting/Account.cs
+++ b/Projects/UOContent/Accounting/Account.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Net;
using System.Xml;
using Server.Accounting.Security;
@@ -11,7 +10,7 @@ using Server.Network;
namespace Server.Accounting
{
- [Serializable(3)]
+ [Serializable(4)]
public partial class Account : IAccount, IComparable, ISerializable
{
public static readonly TimeSpan YoungDuration = TimeSpan.FromHours(40.0);
@@ -33,10 +32,7 @@ namespace Server.Accounting
[SerializableField(4)]
private int _flags;
- [SerializableField(5, setter: "private")]
- private DateTime _created;
-
- [SerializableField(6)]
+ [SerializableField(5)]
private DateTime _lastLogin;
///
@@ -44,7 +40,7 @@ namespace Server.Accounting
/// The value does not include the value of Platinum and ranges from
/// 0 to 999,999,999 by default.
///
- [SerializableField(7, setter: "private")]
+ [SerializableField(6, setter: "private")]
[SerializableFieldAttr("[CommandProperty(AccessLevel.Administrator)]")]
public int _totalGold;
@@ -54,16 +50,16 @@ namespace Server.Accounting
/// 0 to 2,147,483,647 by default.
/// One Platinum represents the value of CurrencyThreshold in Gold.
///
- [SerializableField(8, setter: "private")]
+ [SerializableField(7, setter: "private")]
[SerializableFieldAttr("[CommandProperty(AccessLevel.Administrator)]")]
public int _totalPlat;
- [SerializableField(9, "private", "private")]
+ [SerializableField(8, "private", "private")]
private Mobile[] _mobiles;
private List _comments;
- [SerializableField(10)]
+ [SerializableField(9)]
public List Comments
{
get => _comments ??= new List();
@@ -76,7 +72,7 @@ namespace Server.Accounting
private List _tags;
- [SerializableField(11)]
+ [SerializableField(10)]
public List Tags
{
get => _tags ??= new List();
@@ -87,14 +83,14 @@ namespace Server.Accounting
}
}
- [SerializableField(12)]
+ [SerializableField(11)]
private IPAddress[] _loginIPs;
///
/// List of IP addresses for restricted access. '*' wildcard supported. If the array contains zero entries, all IP addresses
/// are allowed.
///
- [SerializableField(13)]
+ [SerializableField(12)]
private string[] _ipRestrictions;
private TimeSpan _totalGameTime;
@@ -103,7 +99,7 @@ namespace Server.Accounting
/// Gets the total game time of this account, also considering the game time of characters
/// that have been deleted.
///
- [SerializableField(14)]
+ [SerializableField(13)]
public TimeSpan TotalGameTime
{
get
@@ -125,7 +121,7 @@ namespace Server.Accounting
}
}
- [SerializableField(15)]
+ [SerializableField(14)]
[SerializableFieldAttr("[CommandProperty(AccessLevel.Administrator)]")]
private string _email;
@@ -139,7 +135,7 @@ namespace Server.Accounting
_accessLevel = AccessLevel.Player;
- _created = _lastLogin = Core.Now;
+ _lastLogin = Core.Now;
_totalGameTime = TimeSpan.Zero;
_mobiles = new Mobile[7];
@@ -184,7 +180,7 @@ namespace Server.Accounting
Enum.TryParse(Utility.GetText(node["accessLevel"], "Player"), true, out _accessLevel);
_flags = Utility.GetXMLInt32(Utility.GetText(node["flags"], "0"), 0);
- _created = Utility.GetXMLDateTime(Utility.GetText(node["created"], null), Core.Now);
+ Created = Utility.GetXMLDateTime(Utility.GetText(node["created"], null), Core.Now);
_lastLogin = Utility.GetXMLDateTime(Utility.GetText(node["lastLogin"], null), Core.Now);
_totalGold = Utility.GetXMLInt32(Utility.GetText(node["totalGold"], "0"), 0);
@@ -306,10 +302,22 @@ namespace Server.Accounting
}
}
+ public TimeSpan AccountAge => Core.Now - Created;
+
+ [CommandProperty(AccessLevel.GameMaster, readOnly: true)]
+ public DateTime Created { get; set; } = Core.Now;
+
+ [CommandProperty(AccessLevel.GameMaster)]
+ DateTime ISerializable.LastSerialized { get; set; } = Core.Now;
+
public int TypeRef { get; private set; }
public Serial Serial { get; set; }
+ public void BeforeSerialize()
+ {
+ }
+
[AfterDeserialization]
private void AfterDeserialization()
{
@@ -348,77 +356,6 @@ namespace Server.Accounting
}
}
- private void Deserialize(IGenericReader reader, int version)
- {
- if (version != 2)
- {
- // Due to a bug where we were not versioning at all, reset so we don't have an issue deserializing
- reader.Seek(0, SeekOrigin.Begin);
- }
-
- _username = reader.ReadString();
- _passwordAlgorithm = version < 2 ? (PasswordProtectionAlgorithm)reader.ReadInt() : reader.ReadEnum();
- _password = reader.ReadString();
- _accessLevel = version < 2 ? (AccessLevel)reader.ReadInt() : reader.ReadEnum();
- _flags = reader.ReadInt();
- _created = reader.ReadDateTime();
- _lastLogin = reader.ReadDateTime();
-
- _totalGold = reader.ReadInt();
- _totalPlat = reader.ReadInt();
-
- var length = reader.ReadInt();
- _mobiles = new Mobile[length];
- for (int i = 0; i < length; i++)
- {
- _mobiles[i] = reader.ReadEntity();
- }
-
- length = reader.ReadInt();
- _comments = length > 0 ? new List(length) : null;
- for (int i = 0; i < length; i++)
- {
- _comments!.Add(new AccountComment(reader));
- }
-
- length = reader.ReadInt();
- _tags = length > 0 ? new List(length) : null;
- for (int i = 0; i < length; i++)
- {
- _tags!.Add(new AccountTag(reader));
- }
-
- length = reader.ReadInt();
- _loginIPs = new IPAddress[length];
- for (int i = 0; i < length; i++)
- {
- if (version < 2)
- {
- if (IPAddress.TryParse(reader.ReadString(), out var address))
- {
- _loginIPs[i] = Utility.Intern(address);
- }
- }
- else
- {
- _loginIPs[i] = reader.ReadIPAddress();
- }
- }
-
- length = reader.ReadInt();
- _ipRestrictions = new string[length];
- for (int i = 0; i < length; i++)
- {
- _ipRestrictions[i] = reader.ReadString();
- }
-
- _totalGameTime = reader.ReadTimeSpan();
-
- _email = reader.ReadString();
-
- Timer.StartTimer(AfterDeserialization);
- }
-
///
/// Deletes the account, all characters of the account, and all houses of those characters
///
diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs
index 91a5a4096..a115d993a 100644
--- a/Projects/UOContent/Accounting/AccountHandler.cs
+++ b/Projects/UOContent/Accounting/AccountHandler.cs
@@ -241,7 +241,7 @@ namespace Server.Misc
{
res = DeleteResultType.CharBeingPlayed;
}
- else if (RestrictDeletion && Core.Now < m.CreationTime + DeleteDelay)
+ else if (RestrictDeletion && Core.Now < m.Created + DeleteDelay)
{
res = DeleteResultType.CharTooYoung;
}
diff --git a/Projects/UOContent/Commands/Batch.cs b/Projects/UOContent/Commands/Batch.cs
index fcc5bdab6..84aeca315 100644
--- a/Projects/UOContent/Commands/Batch.cs
+++ b/Projects/UOContent/Commands/Batch.cs
@@ -76,67 +76,57 @@ namespace Server.Commands
CommandLogging.Enabled = false;
}
- List