diff --git a/data/XML/mounts.xml b/data/XML/mounts.xml
index 625190c465..8f4e3a653d 100644
--- a/data/XML/mounts.xml
+++ b/data/XML/mounts.xml
@@ -102,4 +102,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/XML/outfits.xml b/data/XML/outfits.xml
index e6c43299de..3e0a5a6138 100644
--- a/data/XML/outfits.xml
+++ b/data/XML/outfits.xml
@@ -56,7 +56,56 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -113,4 +162,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/items/items.otb b/data/items/items.otb
index da589f23e0..bf21c3215d 100644
Binary files a/data/items/items.otb and b/data/items/items.otb differ
diff --git a/data/items/items.xml b/data/items/items.xml
index c7c877da76..b095032d07 100644
--- a/data/items/items.xml
+++ b/data/items/items.xml
@@ -7,6 +7,7 @@
+
@@ -11515,7 +11516,7 @@
- -
+
-
@@ -17346,15 +17347,43 @@
- -
+
-
- -
+
-
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
- -
+
-
+
+
+
+
+
+ -
+
+
+
+
+
+ -
@@ -17366,7 +17395,19 @@
- -
+
-
+
+
+
+
+
+ -
+
+
+
+
+
+ -
@@ -37367,4 +37408,3782 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/migrations/30.lua b/data/migrations/30.lua
index d0ffd9c0cb..e66f37992e 100644
--- a/data/migrations/30.lua
+++ b/data/migrations/30.lua
@@ -1,3 +1,12 @@
function onUpdateDatabase()
- return false
+ print("> Updating database to version 30 (mount colors)")
+ db.query([[
+ ALTER TABLE `players`
+ ADD COLUMN `lookmount` int DEFAULT 0 NOT NULL AFTER `lookaddons`,
+ ADD COLUMN `lookmounthead` int DEFAULT 0 NOT NULL AFTER `lookmount`,
+ ADD COLUMN `lookmountbody` int DEFAULT 0 NOT NULL AFTER `lookmounthead`,
+ ADD COLUMN `lookmountlegs` int DEFAULT 0 NOT NULL AFTER `lookmountbody`,
+ ADD COLUMN `lookmountfeet` int DEFAULT 0 NOT NULL AFTER `lookmountlegs`;
+ ]])
+ return true
end
diff --git a/data/migrations/31.lua b/data/migrations/31.lua
new file mode 100644
index 0000000000..d0ffd9c0cb
--- /dev/null
+++ b/data/migrations/31.lua
@@ -0,0 +1,3 @@
+function onUpdateDatabase()
+ return false
+end
diff --git a/data/talkactions/scripts/attributes.lua b/data/talkactions/scripts/attributes.lua
index 2c8b21f3f7..ec47cfa568 100644
--- a/data/talkactions/scripts/attributes.lua
+++ b/data/talkactions/scripts/attributes.lua
@@ -8,19 +8,19 @@ function onSay(player, words, param)
local tile = Tile(position)
if not tile then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "There is no tile in front of you.")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "There is no tile in front of you.")
return false
end
local thing = tile:getTopVisibleThing(player)
if not thing then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "There is an empty tile in front of you.")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "There is an empty tile in front of you.")
return false
end
local separatorPos = param:find(',')
if not separatorPos then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, string.format("Usage: %s attribute, value.", words))
+ player:sendTextMessage(MESSAGE_INFO_DESCR, string.format("Usage: %s attribute, value.", words))
return false
end
@@ -30,19 +30,19 @@ function onSay(player, words, param)
if thing:isItem() then
local attributeId = Game.getItemAttributeByName(attribute)
if attributeId == ITEM_ATTRIBUTE_NONE then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Invalid attribute name.")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Invalid attribute name.")
return false
end
if not thing:setAttribute(attribute, value) then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Could not set attribute.")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Could not set attribute.")
return false
end
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, string.format("Attribute %s set to: %s", attribute, thing:getAttribute(attributeId)))
+ player:sendTextMessage(MESSAGE_INFO_DESCR, string.format("Attribute %s set to: %s", attribute, thing:getAttribute(attributeId)))
position:sendMagicEffect(CONST_ME_MAGIC_GREEN)
else
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Thing in front of you is not supported.")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Thing in front of you is not supported.")
return false
end
end
diff --git a/data/talkactions/scripts/closeserver.lua b/data/talkactions/scripts/closeserver.lua
index 2f7c95ec3e..b09af02f2e 100644
--- a/data/talkactions/scripts/closeserver.lua
+++ b/data/talkactions/scripts/closeserver.lua
@@ -11,7 +11,7 @@ function onSay(player, words, param)
Game.setGameState(GAME_STATE_SHUTDOWN)
else
Game.setGameState(GAME_STATE_CLOSED)
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Server is now closed.")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Server is now closed.")
end
return false
end
diff --git a/data/talkactions/scripts/force_raid.lua b/data/talkactions/scripts/force_raid.lua
index ebdf16230d..99dd2e9a4f 100644
--- a/data/talkactions/scripts/force_raid.lua
+++ b/data/talkactions/scripts/force_raid.lua
@@ -11,9 +11,9 @@ function onSay(player, words, param)
local returnValue = Game.startRaid(param)
if returnValue ~= RETURNVALUE_NOERROR then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, Game.getReturnMessage(returnValue))
+ player:sendTextMessage(MESSAGE_INFO_DESCR, Game.getReturnMessage(returnValue))
else
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Raid started.")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Raid started.")
end
return false
diff --git a/data/talkactions/scripts/info.lua b/data/talkactions/scripts/info.lua
index 8969b1cae8..f311bc6009 100644
--- a/data/talkactions/scripts/info.lua
+++ b/data/talkactions/scripts/info.lua
@@ -15,13 +15,13 @@ function onSay(player, words, param)
end
local targetIp = target:getIp()
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Name: " .. target:getName())
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Access: " .. (target:getGroup():getAccess() and "1" or "0"))
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Level: " .. target:getLevel())
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Magic Level: " .. target:getMagicLevel())
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Speed: " .. target:getSpeed())
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Position: " .. string.format("(%0.5d / %0.5d / %0.3d)", target:getPosition().x, target:getPosition().y, target:getPosition().z))
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "IP: " .. Game.convertIpToString(targetIp))
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Name: " .. target:getName())
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Access: " .. (target:getGroup():getAccess() and "1" or "0"))
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Level: " .. target:getLevel())
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Magic Level: " .. target:getMagicLevel())
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Speed: " .. target:getSpeed())
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Position: " .. string.format("(%0.5d / %0.5d / %0.3d)", target:getPosition().x, target:getPosition().y, target:getPosition().z))
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "IP: " .. Game.convertIpToString(targetIp))
local players = {}
for _, targetPlayer in ipairs(Game.getPlayers()) do
@@ -31,7 +31,7 @@ function onSay(player, words, param)
end
if #players > 0 then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Other players on same IP: " .. table.concat(players, ", ") .. ".")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Other players on same IP: " .. table.concat(players, ", ") .. ".")
end
return false
end
diff --git a/data/talkactions/scripts/kills.lua b/data/talkactions/scripts/kills.lua
index 360bf62d6e..d04df77d08 100644
--- a/data/talkactions/scripts/kills.lua
+++ b/data/talkactions/scripts/kills.lua
@@ -1,13 +1,13 @@
function onSay(player, words, param)
local fragTime = configManager.getNumber(configKeys.FRAG_TIME)
if fragTime <= 0 then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "You do not have any unjustified kill.")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "You do not have any unjustified kill.")
return false
end
local skullTime = player:getSkullTime()
if skullTime <= 0 then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "You do not have any unjustified kill.")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "You do not have any unjustified kill.")
return false
end
@@ -41,6 +41,6 @@ function onSay(player, words, param)
message = message .. seconds .. " seconds."
end
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, message)
+ player:sendTextMessage(MESSAGE_INFO_DESCR, message)
return false
end
diff --git a/data/talkactions/scripts/looktype.lua b/data/talkactions/scripts/looktype.lua
index 05d677d4ba..631c08fec9 100644
--- a/data/talkactions/scripts/looktype.lua
+++ b/data/talkactions/scripts/looktype.lua
@@ -14,7 +14,17 @@ local invalidTypes = {
808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822,
823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837,
838, 839, 840, 841, 847, 864, 865, 866, 867, 871, 872, 880, 891, 892, 893,
- 894, 895, 896, 897, 898
+ 894, 895, 896, 897, 898, 911, 912, 917, 930, 941, 942, 946, 953, 954, 983,
+ 995, 996, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008,
+ 1009, 1010, 1012, 1014, 1015, 1022, 1028, 1074, 1075, 1080, 1081, 1082, 1083,
+ 1084, 1085, 1086, 1087, 1089, 1090, 1096, 1097, 1098, 1099, 1100, 1141, 1145,
+ 1153, 1154, 1155, 1156, 1160, 1170, 1171, 1172, 1176, 1177, 1178, 1182, 1192,
+ 1193, 1194, 1198, 1215, 1216, 1225, 1226, 1227, 1228, 1235, 1236, 1237, 1238,
+ 1239, 1240, 1241, 1242, 1250, 1254, 1263, 1267, 1273, 1274, 1287, 1302, 1318,
+ 1319, 1320, 1327, 1328, 1329, 1330, 1340, 1343, 1345, 1347, 1348, 1349, 1350,
+ 1351, 1352, 1353, 1354, 1355, 1356, 1357, 1358, 1359, 1360, 1361, 1362, 1368,
+ 1369, 1370, 1374, 1375, 1376, 1388, 1392, 1395, 1400, 1402, 1404, 1409, 1410,
+ 1411, 1420, 1421, 1427, 1429, 1432, 1433, 1434, 1435, 1438, 1442, 1443
}
function onSay(player, words, param)
@@ -23,7 +33,7 @@ function onSay(player, words, param)
end
local lookType = tonumber(param)
- if lookType >= 0 and lookType < 903 and not table.contains(invalidTypes, lookType) then
+ if lookType >= 0 and lookType < 1450 and not table.contains(invalidTypes, lookType) then
local playerOutfit = player:getOutfit()
playerOutfit.lookType = lookType
player:setOutfit(playerOutfit)
diff --git a/data/talkactions/scripts/mccheck.lua b/data/talkactions/scripts/mccheck.lua
index 13ffbc970e..6da986ced5 100644
--- a/data/talkactions/scripts/mccheck.lua
+++ b/data/talkactions/scripts/mccheck.lua
@@ -7,7 +7,7 @@ function onSay(player, words, param)
return false
end
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Multiclient Check List:")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Multiclient Check List:")
local ipList = {}
local players = Game.getPlayers()
@@ -33,7 +33,7 @@ function onSay(player, words, param)
tmpPlayer = list[i]
message = ("%s, %s [%d]"):format(message, tmpPlayer:getName(), tmpPlayer:getLevel())
end
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, message .. ".")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, message .. ".")
end
end
return false
diff --git a/data/talkactions/scripts/online.lua b/data/talkactions/scripts/online.lua
index db7cceb325..09c79c9dbf 100644
--- a/data/talkactions/scripts/online.lua
+++ b/data/talkactions/scripts/online.lua
@@ -11,12 +11,12 @@ function onSay(player, words, param)
end
local playersOnline = #onlineList
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, ("%d players online."):format(playersOnline))
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, ("%d players online."):format(playersOnline))
for i = 1, playersOnline, maxPlayersPerMessage do
local j = math.min(i + maxPlayersPerMessage - 1, playersOnline)
local msg = table.concat(onlineList, ", ", i, j) .. "."
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, msg)
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, msg)
end
return false
end
diff --git a/data/talkactions/scripts/openserver.lua b/data/talkactions/scripts/openserver.lua
index c3896e7280..c9e0a27e86 100644
--- a/data/talkactions/scripts/openserver.lua
+++ b/data/talkactions/scripts/openserver.lua
@@ -8,6 +8,6 @@ function onSay(player, words, param)
end
Game.setGameState(GAME_STATE_NORMAL)
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Server is now open.")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Server is now open.")
return false
end
diff --git a/data/talkactions/scripts/position.lua b/data/talkactions/scripts/position.lua
index 299ce6e566..b5f373ecc9 100644
--- a/data/talkactions/scripts/position.lua
+++ b/data/talkactions/scripts/position.lua
@@ -4,7 +4,7 @@ function onSay(player, words, param)
player:teleportTo(Position(split[1], split[2], split[3]))
else
local position = player:getPosition()
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Your current position is: " .. position.x .. ", " .. position.y .. ", " .. position.z .. ".")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Your current position is: " .. position.x .. ", " .. position.y .. ", " .. position.z .. ".")
end
return false
end
diff --git a/data/talkactions/scripts/reload.lua b/data/talkactions/scripts/reload.lua
index f11e9d51e8..6899f7c3ac 100644
--- a/data/talkactions/scripts/reload.lua
+++ b/data/talkactions/scripts/reload.lua
@@ -69,7 +69,7 @@ function onSay(player, words, param)
local reloadType = reloadTypes[param:lower()]
if not reloadType then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Reload type not found.")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Reload type not found.")
return false
end
@@ -79,6 +79,6 @@ function onSay(player, words, param)
end
Game.reload(reloadType)
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, string.format("Reloaded %s.", param:lower()))
+ player:sendTextMessage(MESSAGE_INFO_DESCR, string.format("Reloaded %s.", param:lower()))
return false
end
diff --git a/data/talkactions/scripts/serverinfo.lua b/data/talkactions/scripts/serverinfo.lua
index 6b26b9814d..fc6cc7a713 100644
--- a/data/talkactions/scripts/serverinfo.lua
+++ b/data/talkactions/scripts/serverinfo.lua
@@ -1,5 +1,5 @@
function onSay(player, words, param)
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Server Info:"
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Server Info:"
.. "\nExp rate: " .. Game.getExperienceStage(player:getLevel())
.. "\nSkill rate: " .. configManager.getNumber(configKeys.RATE_SKILL)
.. "\nMagic rate: " .. configManager.getNumber(configKeys.RATE_MAGIC)
diff --git a/data/talkactions/scripts/uptime.lua b/data/talkactions/scripts/uptime.lua
index 7c0e291ede..b77f76daac 100644
--- a/data/talkactions/scripts/uptime.lua
+++ b/data/talkactions/scripts/uptime.lua
@@ -3,6 +3,6 @@ function onSay(player, words, param)
local hours = math.floor(uptime / 3600)
local minutes = math.floor((uptime - (3600 * hours)) / 60)
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Uptime: " .. hours .. " hours and " .. minutes .. " minutes.")
+ player:sendTextMessage(MESSAGE_INFO_DESCR, "Uptime: " .. hours .. " hours and " .. minutes .. " minutes.")
return false
end
diff --git a/schema.sql b/schema.sql
index 9605da961f..4ff4cd3571 100644
--- a/schema.sql
+++ b/schema.sql
@@ -27,6 +27,11 @@ CREATE TABLE IF NOT EXISTS `players` (
`looklegs` int NOT NULL DEFAULT '0',
`looktype` int NOT NULL DEFAULT '136',
`lookaddons` int NOT NULL DEFAULT '0',
+ `lookmount` int NOT NULL DEFAULT '0',
+ `lookmounthead` int NOT NULL DEFAULT '0',
+ `lookmountbody` int NOT NULL DEFAULT '0',
+ `lookmountlegs` int NOT NULL DEFAULT '0',
+ `lookmountfeet` int NOT NULL DEFAULT '0',
`direction` tinyint unsigned NOT NULL DEFAULT '2',
`maglevel` int NOT NULL DEFAULT '0',
`mana` int NOT NULL DEFAULT '0',
@@ -357,7 +362,7 @@ CREATE TABLE IF NOT EXISTS `towns` (
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8;
-INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '29'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0');
+INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '30'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0');
DROP TRIGGER IF EXISTS `ondelete_players`;
DROP TRIGGER IF EXISTS `oncreate_guilds`;
diff --git a/src/connection.cpp b/src/connection.cpp
index a2d294e14f..6cfbfc0419 100644
--- a/src/connection.cpp
+++ b/src/connection.cpp
@@ -67,14 +67,10 @@ void Connection::close(bool force)
ConnectionManager::getInstance().releaseConnection(shared_from_this());
std::lock_guard lockClass(connectionLock);
- if (closed) {
- return;
- }
- closed = true;
+ connectionState = CONNECTION_STATE_DISCONNECTED;
if (protocol) {
- g_dispatcher.addTask(
- createTask(std::bind(&Protocol::release, protocol)));
+ g_dispatcher.addTask(createTask(std::bind(&Protocol::release, protocol)));
}
if (messageQueue.empty() || force) {
@@ -108,21 +104,26 @@ void Connection::accept(Protocol_ptr protocol)
{
this->protocol = protocol;
g_dispatcher.addTask(createTask(std::bind(&Protocol::onConnect, protocol)));
-
+ connectionState = CONNECTION_STATE_GAMEWORLD_AUTH;
accept();
}
void Connection::accept()
{
+ if (connectionState == CONNECTION_STATE_PENDING) {
+ connectionState = CONNECTION_STATE_REQUEST_CHARLIST;
+ }
+
std::lock_guard lockClass(connectionLock);
try {
readTimer.expires_from_now(std::chrono::seconds(CONNECTION_READ_TIMEOUT));
readTimer.async_wait(std::bind(&Connection::handleTimeout, std::weak_ptr(shared_from_this()), std::placeholders::_1));
// Read size of the first packet
+ auto bufferLength = !receivedLastChar && receivedName && connectionState == CONNECTION_STATE_GAMEWORLD_AUTH ? 1 : NetworkMessage::HEADER_LENGTH;
boost::asio::async_read(socket,
- boost::asio::buffer(msg.getBuffer(), NetworkMessage::HEADER_LENGTH),
- std::bind(&Connection::parseHeader, shared_from_this(), std::placeholders::_1));
+ boost::asio::buffer(msg.getBuffer(), bufferLength),
+ std::bind(&Connection::parseHeader, shared_from_this(), std::placeholders::_1));
} catch (boost::system::system_error& e) {
std::cout << "[Network error - Connection::accept] " << e.what() << std::endl;
close(FORCE_CLOSE);
@@ -137,7 +138,7 @@ void Connection::parseHeader(const boost::system::error_code& error)
if (error) {
close(FORCE_CLOSE);
return;
- } else if (closed) {
+ } else if (connectionState == CONNECTION_STATE_DISCONNECTED) {
return;
}
@@ -148,6 +149,32 @@ void Connection::parseHeader(const boost::system::error_code& error)
return;
}
+ if (!receivedLastChar && connectionState == CONNECTION_STATE_GAMEWORLD_AUTH) {
+ uint8_t* msgBuffer = msg.getBuffer();
+
+ if (!receivedName && msgBuffer[1] == 0x00) {
+ receivedLastChar = true;
+ } else {
+ if (!receivedName) {
+ receivedName = true;
+
+ accept();
+ return;
+ }
+
+ if (msgBuffer[0] == 0x0A) {
+ receivedLastChar = true;
+ }
+
+ accept();
+ return;
+ }
+ }
+
+ if (receivedLastChar && connectionState == CONNECTION_STATE_GAMEWORLD_AUTH) {
+ connectionState = CONNECTION_STATE_GAME;
+ }
+
if (timePassed > 2) {
timeConnected = time(nullptr);
packetsSent = 0;
@@ -162,12 +189,12 @@ void Connection::parseHeader(const boost::system::error_code& error)
try {
readTimer.expires_from_now(std::chrono::seconds(CONNECTION_READ_TIMEOUT));
readTimer.async_wait(std::bind(&Connection::handleTimeout, std::weak_ptr(shared_from_this()),
- std::placeholders::_1));
+ std::placeholders::_1));
// Read packet content
msg.setLength(size + NetworkMessage::HEADER_LENGTH);
boost::asio::async_read(socket, boost::asio::buffer(msg.getBodyBuffer(), size),
- std::bind(&Connection::parsePacket, shared_from_this(), std::placeholders::_1));
+ std::bind(&Connection::parsePacket, shared_from_this(), std::placeholders::_1));
} catch (boost::system::system_error& e) {
std::cout << "[Network error - Connection::parseHeader] " << e.what() << std::endl;
close(FORCE_CLOSE);
@@ -182,32 +209,25 @@ void Connection::parsePacket(const boost::system::error_code& error)
if (error) {
close(FORCE_CLOSE);
return;
- } else if (closed) {
+ } else if (connectionState == CONNECTION_STATE_DISCONNECTED) {
return;
}
- //Check packet checksum
- uint32_t checksum;
- int32_t len = msg.getLength() - msg.getBufferPosition() - NetworkMessage::CHECKSUM_LENGTH;
- if (len > 0) {
- checksum = adlerChecksum(msg.getBuffer() + msg.getBufferPosition() + NetworkMessage::CHECKSUM_LENGTH, len);
- } else {
- checksum = 0;
- }
-
- uint32_t recvChecksum = msg.get();
- if (recvChecksum != checksum) {
- // it might not have been the checksum, step back
- msg.skipBytes(-NetworkMessage::CHECKSUM_LENGTH);
- }
+ // Read potential checksum bytes
+ msg.get();
if (!receivedFirst) {
- // First message received
receivedFirst = true;
if (!protocol) {
+ // Skip deprecated checksum bytes (with clients that aren't using it in mind)
+ uint16_t len = msg.getLength();
+ if (len < 280 && len != 151) {
+ msg.skipBytes(-NetworkMessage::CHECKSUM_LENGTH);
+ }
+
// Game protocol has already been created at this point
- protocol = service_port->make_protocol(recvChecksum == checksum, msg, shared_from_this());
+ protocol = service_port->make_protocol(msg, shared_from_this());
if (!protocol) {
close(FORCE_CLOSE);
return;
@@ -224,12 +244,12 @@ void Connection::parsePacket(const boost::system::error_code& error)
try {
readTimer.expires_from_now(std::chrono::seconds(CONNECTION_READ_TIMEOUT));
readTimer.async_wait(std::bind(&Connection::handleTimeout, std::weak_ptr(shared_from_this()),
- std::placeholders::_1));
+ std::placeholders::_1));
// Wait to the next packet
boost::asio::async_read(socket,
- boost::asio::buffer(msg.getBuffer(), NetworkMessage::HEADER_LENGTH),
- std::bind(&Connection::parseHeader, shared_from_this(), std::placeholders::_1));
+ boost::asio::buffer(msg.getBuffer(), NetworkMessage::HEADER_LENGTH),
+ std::bind(&Connection::parseHeader, shared_from_this(), std::placeholders::_1));
} catch (boost::system::system_error& e) {
std::cout << "[Network error - Connection::parsePacket] " << e.what() << std::endl;
close(FORCE_CLOSE);
@@ -239,7 +259,7 @@ void Connection::parsePacket(const boost::system::error_code& error)
void Connection::send(const OutputMessage_ptr& msg)
{
std::lock_guard lockClass(connectionLock);
- if (closed) {
+ if (connectionState == CONNECTION_STATE_DISCONNECTED) {
return;
}
@@ -256,11 +276,11 @@ void Connection::internalSend(const OutputMessage_ptr& msg)
try {
writeTimer.expires_from_now(std::chrono::seconds(CONNECTION_WRITE_TIMEOUT));
writeTimer.async_wait(std::bind(&Connection::handleTimeout, std::weak_ptr(shared_from_this()),
- std::placeholders::_1));
+ std::placeholders::_1));
boost::asio::async_write(socket,
- boost::asio::buffer(msg->getOutputBuffer(), msg->getLength()),
- std::bind(&Connection::onWriteOperation, shared_from_this(), std::placeholders::_1));
+ boost::asio::buffer(msg->getOutputBuffer(), msg->getLength()),
+ std::bind(&Connection::onWriteOperation, shared_from_this(), std::placeholders::_1));
} catch (boost::system::system_error& e) {
std::cout << "[Network error - Connection::internalSend] " << e.what() << std::endl;
close(FORCE_CLOSE);
@@ -295,7 +315,7 @@ void Connection::onWriteOperation(const boost::system::error_code& error)
if (!messageQueue.empty()) {
internalSend(messageQueue.front());
- } else if (closed) {
+ } else if (connectionState == CONNECTION_STATE_DISCONNECTED) {
closeSocket();
}
}
@@ -303,7 +323,7 @@ void Connection::onWriteOperation(const boost::system::error_code& error)
void Connection::handleTimeout(ConnectionWeak_ptr connectionWeak, const boost::system::error_code& error)
{
if (error == boost::asio::error::operation_aborted) {
- //The timer has been manually canceled
+ // The timer has been cancelled manually
return;
}
diff --git a/src/connection.h b/src/connection.h
index 6b847cfaf9..44ede515e6 100644
--- a/src/connection.h
+++ b/src/connection.h
@@ -24,6 +24,20 @@
#include "networkmessage.h"
+enum ConnectionState_t {
+ CONNECTION_STATE_DISCONNECTED,
+ CONNECTION_STATE_REQUEST_CHARLIST,
+ CONNECTION_STATE_GAMEWORLD_AUTH,
+ CONNECTION_STATE_GAME,
+ CONNECTION_STATE_PENDING
+};
+
+enum checksumMode_t {
+ CHECKSUM_DISABLED,
+ CHECKSUM_ADLER,
+ CHECKSUM_SEQUENCE
+};
+
static constexpr int32_t CONNECTION_WRITE_TIMEOUT = 30;
static constexpr int32_t CONNECTION_READ_TIMEOUT = 30;
@@ -121,8 +135,10 @@ class Connection : public std::enable_shared_from_this
time_t timeConnected;
uint32_t packetsSent = 0;
- bool closed = false;
+ ConnectionState_t connectionState = CONNECTION_STATE_PENDING;
bool receivedFirst = false;
+ bool receivedName = false;
+ bool receivedLastChar = false;
};
#endif
diff --git a/src/const.h b/src/const.h
index 7a6836b99e..61e3076ef3 100644
--- a/src/const.h
+++ b/src/const.h
@@ -22,6 +22,15 @@
static constexpr int32_t NETWORKMESSAGE_MAXSIZE = 24590;
+enum MagicEffectsType_t : uint8_t {
+ MAGIC_EFFECTS_END_LOOP = 0, // ends the magic effect loop
+ MAGIC_EFFECTS_DELTA = 1, // needs uint8_t delta after type to adjust position
+ MAGIC_EFFECTS_DELAY = 2, // needs uint16_t delay after type to delay in miliseconds effect display
+ MAGIC_EFFECTS_CREATE_EFFECT = 3, // needs uint8_t effectid after type
+ MAGIC_EFFECTS_CREATE_DISTANCEEFFECT = 4, // needs uint8_t and deltaX(int8_t), deltaY(int8_t) after type
+ MAGIC_EFFECTS_CREATE_DISTANCEEFFECT_REVERSED = 5, // needs uint8_t and deltaX(int8_t), deltaY(int8_t) after type
+};
+
enum MagicEffectClasses : uint8_t {
CONST_ME_NONE,
@@ -113,6 +122,57 @@ enum MagicEffectClasses : uint8_t {
CONST_ME_CRITICAL_DAMAGE = 173,
// 174 is empty
CONST_ME_PLUNGING_FISH = 175,
+ CONST_ME_BLUECHAIN = 176,
+ CONST_ME_ORANGECHAIN = 177,
+ CONST_ME_GREENCHAIN = 178,
+ CONST_ME_PURPLECHAIN = 179,
+ CONST_ME_GREYCHAIN = 180,
+ CONST_ME_YELLOWCHAIN = 181,
+ CONST_ME_YELLOWSPARKLES = 182,
+ // 183 is empty
+ CONST_ME_FAEEXPLOSION = 184,
+ CONST_ME_FAECOMING = 185,
+ CONST_ME_FAEGOING = 186,
+ // 187 is empty
+ CONST_ME_BIGCLOUDSSINGLESPACE = 188,
+ CONST_ME_STONESSINGLESPACE = 189,
+ // 190 is empty
+ CONST_ME_BLUEGHOST = 191,
+ // 192 is empty
+ CONST_ME_POINTOFINTEREST = 193,
+ CONST_ME_MAPEFFECT = 194,
+ CONST_ME_PINKSPARK = 195,
+ CONST_ME_FIREWORK_GREEN = 196,
+ CONST_ME_FIREWORK_ORANGE = 197,
+ CONST_ME_FIREWORK_PURPLE = 198,
+ CONST_ME_FIREWORK_TURQUOISE = 199,
+ // 200 us empty
+ CONST_ME_THECUBE = 201,
+ CONST_ME_DRAWINK = 202,
+ CONST_ME_PRISMATICSPARKLES = 203,
+ CONST_ME_THAIAN = 204,
+ CONST_ME_THAIANGHOST = 205,
+ CONST_ME_GHOSTSMOKE = 206,
+ // 207 is empty
+ CONST_ME_FLOATINGBLOCK = 208,
+ CONST_ME_BLOCK = 209,
+ CONST_ME_ROOTING = 210,
+ CONST_ME_SUNPRIEST = 211,
+ CONST_ME_WERELION = 212,
+ CONST_ME_GHOSTLYSCRATCH = 213,
+ CONST_ME_GHOSTLYBITE = 214,
+ CONST_ME_BIGSCRATCHING = 215,
+ CONST_ME_SLASH = 216,
+ CONST_ME_BITE = 217,
+ // 218 is empty
+ CONST_ME_CHIVALRIOUSCHALLENGE = 219,
+ CONST_ME_DIVINEDAZZLE = 220,
+ CONST_ME_ELECTRICALSPARK = 221,
+ CONST_ME_PURPLETELEPORT = 222,
+ CONST_ME_REDTELEPORT = 223,
+ CONST_ME_ORANGETELEPORT = 224,
+ CONST_ME_GREYTELEPORT = 225,
+ CONST_ME_LIGHTBLUETELEPORT = 226,
};
enum ShootType_t : uint8_t {
@@ -172,6 +232,11 @@ enum ShootType_t : uint8_t {
CONST_ANI_GLOOTHSPEAR = 53,
CONST_ANI_SIMPLEARROW = 54,
+ CONST_ANI_LEAFSTAR = 56,
+ CONST_ANI_DIAMONDARROW = 57,
+ CONST_ANI_SPECTRALBOLT = 58,
+ CONST_ANI_ROYALSTAR = 59,
+
// for internal use, don't send to client
CONST_ANI_WEAPONTYPE = 0xFE, // 254
};
@@ -180,31 +245,33 @@ enum SpeakClasses : uint8_t {
TALKTYPE_SAY = 1,
TALKTYPE_WHISPER = 2,
TALKTYPE_YELL = 3,
- TALKTYPE_PRIVATE_FROM = 4,
- TALKTYPE_PRIVATE_TO = 5,
+ TALKTYPE_PRIVATE_FROM = 4, // Received private message
+ TALKTYPE_PRIVATE_TO = 5, // Sent private message
+ //TALKTYPE_CHANNEL_M = 6 // not working (?)
TALKTYPE_CHANNEL_Y = 7,
TALKTYPE_CHANNEL_O = 8,
- TALKTYPE_PRIVATE_NP = 10,
- TALKTYPE_PRIVATE_PN = 12,
+ TALKTYPE_SPELL = 9, // Like SAY but with "casts" instead of "says"
+ TALKTYPE_PRIVATE_NP = 10, // NPC speaking to player
+ TALKTYPE_PRIVATE_NP_CONSOLE = 11, // NPC channel message, no text on game screen, for sendPrivateMessage use only
+ TALKTYPE_PRIVATE_PN = 12, // Player speaking to NPC
TALKTYPE_BROADCAST = 13,
- TALKTYPE_CHANNEL_R1 = 14, //red - #c text
- TALKTYPE_PRIVATE_RED_FROM = 15, //@name@text
- TALKTYPE_PRIVATE_RED_TO = 16, //@name@text
+ TALKTYPE_CHANNEL_R1 = 14, // red - #c text
+ TALKTYPE_PRIVATE_RED_FROM = 15, // @name@text
+ TALKTYPE_PRIVATE_RED_TO = 16, // @name@text
TALKTYPE_MONSTER_SAY = 36,
TALKTYPE_MONSTER_YELL = 37,
+ TALKTYPE_POTION = 52, // Like MONSTER_SAY but can be disabled in client settings
};
enum MessageClasses : uint8_t {
- MESSAGE_STATUS_CONSOLE_BLUE = 4, /*FIXME Blue message in the console*/
-
- MESSAGE_STATUS_CONSOLE_RED = 13, /*Red message in the console*/
-
- MESSAGE_STATUS_DEFAULT = 17, /*White message at the bottom of the game window and in the console*/
- MESSAGE_STATUS_WARNING = 18, /*Red message in game window and in the console*/
- MESSAGE_EVENT_ADVANCE = 19, /*White message in game window and in the console*/
-
- MESSAGE_STATUS_SMALL = 21, /*White message at the bottom of the game window"*/
- MESSAGE_INFO_DESCR = 22, /*Green message in game window and in the console*/
+ MESSAGE_STATUS_DEFAULT = 17, // White, bottom + console
+ MESSAGE_STATUS_WARNING = 18, // Red, over player + console
+ MESSAGE_EVENT_ADVANCE = 19, // White, over player + console
+ MESSAGE_STATUS_WARNING2 = 20, // Red, over player + console
+ MESSAGE_STATUS_SMALL = 21, // White, bottom of the screen
+ MESSAGE_INFO_DESCR = 22, // Green, over player + console
+
+ // White, console
MESSAGE_DAMAGE_DEALT = 23,
MESSAGE_DAMAGE_RECEIVED = 24,
MESSAGE_HEALED = 25,
@@ -212,14 +279,30 @@ enum MessageClasses : uint8_t {
MESSAGE_DAMAGE_OTHERS = 27,
MESSAGE_HEALED_OTHERS = 28,
MESSAGE_EXPERIENCE_OTHERS = 29,
- MESSAGE_EVENT_DEFAULT = 30, /*White message at the bottom of the game window and in the console*/
- MESSAGE_LOOT = 31,
-
- MESSAGE_GUILD = 33, /*White message in channel (+ channelId)*/
- MESSAGE_PARTY_MANAGEMENT = 34, /*White message in channel (+ channelId)*/
- MESSAGE_PARTY = 35, /*White message in channel (+ channelId)*/
- MESSAGE_EVENT_ORANGE = 36, /*Orange message in the console*/
- MESSAGE_STATUS_CONSOLE_ORANGE = 37, /*Orange message in the console*/
+
+ MESSAGE_EVENT_DEFAULT = 30, // White, bottom + console
+ MESSAGE_LOOT = 31, // White, over player + console, supports colors as {text|itemClientId}
+ MESSAGE_TRADE = 32, // Green, over player + console
+
+ // White, in channel (needs channel Id)
+ MESSAGE_GUILD = 33,
+ MESSAGE_PARTY_MANAGEMENT = 34,
+ MESSAGE_PARTY = 35,
+
+ MESSAGE_REPORT = 38, // White, over player + conosle
+ MESSAGE_HOTKEY_PRESSED = 39, // Green, over player + console
+ //MESSAGE_TUTORIAL_HINT = 40, // not working (?)
+ //MESSAGE_THANK_YOU = 41, // not working (?)
+ MESSAGE_MARKET = 42, // Window "Market Message" + "Ok" button
+ //MESSAGE_MANA = 43, // not working (?)
+ MESSAGE_BEYOND_LAST = 44, // White, console only
+ MESSAGE_TOURNAMENT_INFO = 45, // Window "Tournament" + "Ok" button
+ // unused 46?
+ // unused 47?
+ MESSAGE_ATTENTION = 48, // White, console only
+ MESSAGE_BOOSTED_CREATURE = 49, // White, console only
+ MESSAGE_OFFLINE_TRAINING = 50, // White, over player + console
+ MESSAGE_TRANSACTION = 51, // White, console only
};
enum FluidColors_t : uint8_t {
@@ -358,6 +441,17 @@ enum Icons_t {
ICON_REDSWORDS = 1 << 13,
ICON_PIGEON = 1 << 14,
ICON_BLEEDING = 1 << 15,
+ ICON_LESSERHEX = 1 << 16,
+ ICON_INTENSEHEX = 1 << 17,
+ ICON_GREATERHEX = 1 << 18,
+ ICON_ROOT = 1 << 19,
+ ICON_FEAR = 1 << 20,
+ ICON_GOSHNAR1 = 1 << 21,
+ ICON_GOSHNAR2 = 1 << 22,
+ ICON_GOSHNAR3 = 1 << 23,
+ ICON_GOSHNAR4 = 1 << 24,
+ ICON_GOSHNAR5 = 1 << 25,
+ ICON_MANASHIELD_BREAKABLE = 1 << 26,
};
enum WeaponType_t : uint8_t {
@@ -507,6 +601,14 @@ enum item_t : uint16_t {
ITEM_DOCUMENT_RO = 1968, //read-only
};
+enum ResourceTypes_t: uint8_t {
+ RESOURCE_BANK_BALANCE = 0x00,
+ RESOURCE_GOLD_EQUIPPED = 0x01,
+ RESOURCE_PREY_WILDCARDS = 0x0A,
+ RESOURCE_DAILYREWARD_STREAK = 0x14,
+ RESOURCE_DAILYREWARD_JOKERS = 0x15,
+};
+
enum PlayerFlags : uint64_t {
PlayerFlag_CannotUseCombat = 1 << 0,
PlayerFlag_CannotAttackPlayer = 1 << 1,
diff --git a/src/creature.h b/src/creature.h
index ac4b4266be..eab9b949e9 100644
--- a/src/creature.h
+++ b/src/creature.h
@@ -71,6 +71,8 @@ class Tile;
static constexpr int32_t EVENT_CREATURECOUNT = 10;
static constexpr int32_t EVENT_CREATURE_THINK_INTERVAL = 1000;
static constexpr int32_t EVENT_CHECK_CREATURE_INTERVAL = (EVENT_CREATURE_THINK_INTERVAL / EVENT_CREATURECOUNT);
+static constexpr uint32_t CREATURE_ID_MIN = 0x10000000;
+static constexpr uint32_t CREATURE_ID_MAX = std::numeric_limits::max();
class FrozenPathingConditionCall
{
diff --git a/src/definitions.h b/src/definitions.h
index 274a1eb84a..5c3f9b70be 100644
--- a/src/definitions.h
+++ b/src/definitions.h
@@ -24,9 +24,9 @@ static constexpr auto STATUS_SERVER_NAME = "The Forgotten Server";
static constexpr auto STATUS_SERVER_VERSION = "1.4";
static constexpr auto STATUS_SERVER_DEVELOPERS = "Mark Samman";
-static constexpr auto CLIENT_VERSION_MIN = 1097;
-static constexpr auto CLIENT_VERSION_MAX = 1098;
-static constexpr auto CLIENT_VERSION_STR = "10.98";
+static constexpr auto CLIENT_VERSION_MIN = 1270;
+static constexpr auto CLIENT_VERSION_MAX = 1272;
+static constexpr auto CLIENT_VERSION_STR = "12.71";
static constexpr auto AUTHENTICATOR_DIGITS = 6U;
static constexpr auto AUTHENTICATOR_PERIOD = 30U;
diff --git a/src/enums.h b/src/enums.h
index 21b7fd9d3d..a9498ebcc9 100644
--- a/src/enums.h
+++ b/src/enums.h
@@ -93,6 +93,7 @@ enum itemAttrTypes : uint32_t {
ITEM_ATTRIBUTE_WRAPID = 1 << 24,
ITEM_ATTRIBUTE_STOREITEM = 1 << 25,
ITEM_ATTRIBUTE_ATTACK_SPEED = 1 << 26,
+ ITEM_ATTRIBUTE_OPENCONTAINER = 1 << 27,
ITEM_ATTRIBUTE_CUSTOM = 1U << 31
};
@@ -135,6 +136,7 @@ enum CreatureType_t : uint8_t {
CREATURETYPE_NPC = 2,
CREATURETYPE_SUMMON_OWN = 3,
CREATURETYPE_SUMMON_OTHERS = 4,
+ CREATURETYPE_HIDDEN = 5,
};
enum OperatingSystem_t : uint8_t {
@@ -143,6 +145,9 @@ enum OperatingSystem_t : uint8_t {
CLIENTOS_LINUX = 1,
CLIENTOS_WINDOWS = 2,
CLIENTOS_FLASH = 3,
+ CLIENTOS_QT_LINUX = 4,
+ CLIENTOS_QT_WINDOWS = 5,
+ CLIENTOS_QT_MAC = 6,
CLIENTOS_OTCLIENT_LINUX = 10,
CLIENTOS_OTCLIENT_WINDOWS = 11,
@@ -155,6 +160,10 @@ enum SpellGroup_t : uint8_t {
SPELLGROUP_HEALING = 2,
SPELLGROUP_SUPPORT = 3,
SPELLGROUP_SPECIAL = 4,
+ //SPELLGROUP_CONJURE = 5,
+ SPELLGROUP_CRIPPLING = 6,
+ SPELLGROUP_FOCUS = 7,
+ SPELLGROUP_ULTIMATESTRIKES = 8,
};
enum SpellType_t : uint8_t {
@@ -473,7 +482,10 @@ enum SpeechBubble_t
SPEECHBUBBLE_NORMAL = 1,
SPEECHBUBBLE_TRADE = 2,
SPEECHBUBBLE_QUEST = 3,
- SPEECHBUBBLE_QUESTTRADER = 4,
+ SPEECHBUBBLE_COMPASS = 4,
+ SPEECHBUBBLE_NORMAL2 = 5,
+ SPEECHBUBBLE_NORMAL3 = 6,
+ SPEECHBUBBLE_HIRELING = 7,
};
enum MapMark_t
@@ -503,12 +515,16 @@ enum MapMark_t
struct Outfit_t {
uint16_t lookType = 0;
uint16_t lookTypeEx = 0;
- uint16_t lookMount = 0;
uint8_t lookHead = 0;
uint8_t lookBody = 0;
uint8_t lookLegs = 0;
uint8_t lookFeet = 0;
uint8_t lookAddons = 0;
+ uint16_t lookMount = 0;
+ uint8_t lookMountHead = 0;
+ uint8_t lookMountBody = 0;
+ uint8_t lookMountLegs = 0;
+ uint8_t lookMountFeet = 0;
};
struct LightInfo {
diff --git a/src/game.cpp b/src/game.cpp
index 1584b64732..b33dceefff 100644
--- a/src/game.cpp
+++ b/src/game.cpp
@@ -374,12 +374,12 @@ void Game::internalGetPosition(Item* item, Position& pos, uint8_t& stackpos)
Creature* Game::getCreatureByID(uint32_t id)
{
- if (id <= Player::playerAutoID) {
+ if (id <= Player::playerIDLimit) {
return getPlayerByID(id);
- } else if (id <= Monster::monsterAutoID) {
- return getMonsterByID(id);
} else if (id <= Npc::npcAutoID) {
return getNpcByID(id);
+ } else if (id <= Monster::monsterAutoID) {
+ return getMonsterByID(id);
}
return nullptr;
}
@@ -2804,7 +2804,7 @@ void Game::playerAcceptTrade(uint32_t playerId)
if (player->getInventoryItem(CONST_SLOT_BACKPACK) == playerTradeItem && tradePartner->getInventoryItem(CONST_SLOT_BACKPACK) == partnerTradeItem) {
playerRet = RETURNVALUE_NOTENOUGHROOM;
}
-
+
if (tradePartnerRet == RETURNVALUE_NOERROR && playerRet == RETURNVALUE_NOERROR) {
tradePartnerRet = internalAddItem(tradePartner, playerTradeItem, INDEX_WHEREEVER, 0, true);
playerRet = internalAddItem(player, partnerTradeItem, INDEX_WHEREEVER, 0, true);
@@ -3357,17 +3357,16 @@ void Game::playerChangeOutfit(uint32_t playerId, Outfit_t outfit)
return;
}
+ int32_t speedChange = mount->speed;
if (player->isMounted()) {
Mount* prevMount = mounts.getMountByID(player->getCurrentMount());
if (prevMount) {
- changeSpeed(player, mount->speed - prevMount->speed);
+ speedChange -= prevMount->speed;
}
-
- player->setCurrentMount(mount->id);
- } else {
- player->setCurrentMount(mount->id);
- outfit.lookMount = 0;
}
+
+ changeSpeed(player, speedChange);
+ player->setCurrentMount(mount->id);
} else if (player->isMounted()) {
player->dismount();
}
@@ -3484,7 +3483,7 @@ bool Game::playerSaySpell(Player* player, SpeakClasses type, const std::string&
result = g_spells->playerSaySpell(player, words);
if (result == TALKACTION_BREAK) {
if (!g_config.getBoolean(ConfigManager::EMOTE_SPELLS)) {
- return internalCreatureSay(player, TALKTYPE_SAY, words, false);
+ return internalCreatureSay(player, TALKTYPE_SPELL, words, false);
} else {
return internalCreatureSay(player, TALKTYPE_MONSTER_SAY, words, false);
}
@@ -4716,54 +4715,6 @@ void Game::updatePlayerShield(Player* player)
}
}
-void Game::updatePlayerHelpers(const Player& player)
-{
- uint32_t creatureId = player.getID();
- uint16_t helpers = player.getHelpers();
-
- SpectatorVec spectators;
- map.getSpectators(spectators, player.getPosition(), true, true);
- for (Creature* spectator : spectators) {
- spectator->getPlayer()->sendCreatureHelpers(creatureId, helpers);
- }
-}
-
-void Game::updateCreatureType(Creature* creature)
-{
- const Player* masterPlayer = nullptr;
-
- uint32_t creatureId = creature->getID();
- CreatureType_t creatureType = creature->getType();
- if (creatureType == CREATURETYPE_MONSTER) {
- const Creature* master = creature->getMaster();
- if (master) {
- masterPlayer = master->getPlayer();
- if (masterPlayer) {
- creatureType = CREATURETYPE_SUMMON_OTHERS;
- }
- }
- }
-
- //send to clients
- SpectatorVec spectators;
- map.getSpectators(spectators, creature->getPosition(), true, true);
-
- if (creatureType == CREATURETYPE_SUMMON_OTHERS) {
- for (Creature* spectator : spectators) {
- Player* player = spectator->getPlayer();
- if (masterPlayer == player) {
- player->sendCreatureType(creatureId, CREATURETYPE_SUMMON_OWN);
- } else {
- player->sendCreatureType(creatureId, creatureType);
- }
- }
- } else {
- for (Creature* spectator : spectators) {
- spectator->getPlayer()->sendCreatureType(creatureId, creatureType);
- }
- }
-}
-
void Game::loadMotdNum()
{
Database& db = Database::getInstance();
diff --git a/src/game.h b/src/game.h
index 8c11db2197..7799dab880 100644
--- a/src/game.h
+++ b/src/game.h
@@ -437,8 +437,6 @@ class Game
void changeLight(const Creature* creature);
void updateCreatureSkull(const Creature* creature);
void updatePlayerShield(Player* player);
- void updatePlayerHelpers(const Player& player);
- void updateCreatureType(Creature* creature);
void updateCreatureWalkthrough(const Creature* creature);
GameState_t getGameState() const;
diff --git a/src/guild.cpp b/src/guild.cpp
index dd211103bd..1efddd4b97 100644
--- a/src/guild.cpp
+++ b/src/guild.cpp
@@ -28,18 +28,11 @@ extern Game g_game;
void Guild::addMember(Player* player)
{
membersOnline.push_back(player);
- for (Player* member : membersOnline) {
- g_game.updatePlayerHelpers(*member);
- }
}
void Guild::removeMember(Player* player)
{
membersOnline.remove(player);
- for (Player* member : membersOnline) {
- g_game.updatePlayerHelpers(*member);
- }
- g_game.updatePlayerHelpers(*player);
if (membersOnline.empty()) {
g_game.removeGuild(id);
diff --git a/src/iologindata.cpp b/src/iologindata.cpp
index 0cabd76009..68d0e6e8f7 100644
--- a/src/iologindata.cpp
+++ b/src/iologindata.cpp
@@ -77,7 +77,7 @@ bool IOLoginData::loginserverAuthentication(const std::string& name, const std::
{
Database& db = Database::getInstance();
- DBResult_ptr result = db.storeQuery(fmt::format("SELECT `id`, `name`, `password`, `secret`, `type`, `premium_ends_at` FROM `accounts` WHERE `name` = {:s}", db.escapeString(name)));
+ DBResult_ptr result = db.storeQuery(fmt::format("SELECT `id`, `name`, `password`, `secret`, `type`, `premium_ends_at` FROM `accounts` WHERE `name` = {:s} OR `email` = {:s}", db.escapeString(name), db.escapeString(name)));
if (!result) {
return false;
}
@@ -104,8 +104,7 @@ bool IOLoginData::loginserverAuthentication(const std::string& name, const std::
uint32_t IOLoginData::gameworldAuthentication(const std::string& accountName, const std::string& password, std::string& characterName, std::string& token, uint32_t tokenTime)
{
Database& db = Database::getInstance();
-
- DBResult_ptr result = db.storeQuery(fmt::format("SELECT `id`, `password`, `secret` FROM `accounts` WHERE `name` = {:s}", db.escapeString(accountName)));
+ DBResult_ptr result = db.storeQuery(fmt::format("SELECT `id`, `password`, `secret` FROM `accounts` WHERE `name` = {:s} OR `email` = {:s}", db.escapeString(accountName), db.escapeString(accountName)));
if (!result) {
return 0;
}
@@ -211,13 +210,13 @@ bool IOLoginData::preloadPlayer(Player* player, const std::string& name)
bool IOLoginData::loadPlayerById(Player* player, uint32_t id)
{
Database& db = Database::getInstance();
- return loadPlayer(player, db.storeQuery(fmt::format("SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries`, `direction` FROM `players` WHERE `id` = {:d}", id)));
+ return loadPlayer(player, db.storeQuery(fmt::format("SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `lookmount`, `lookmounthead`, `lookmountbody`, `lookmountlegs`, `lookmountfeet`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries`, `direction` FROM `players` WHERE `id` = {:d}", id)));
}
bool IOLoginData::loadPlayerByName(Player* player, const std::string& name)
{
Database& db = Database::getInstance();
- return loadPlayer(player, db.storeQuery(fmt::format("SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries`, `direction` FROM `players` WHERE `name` = {:s}", db.escapeString(name))));
+ return loadPlayer(player, db.storeQuery(fmt::format("SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `lookmount`, `lookmounthead`, `lookmountbody`, `lookmountlegs`, `lookmountfeet`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries`, `direction` FROM `players` WHERE `name` = {:s}", db.escapeString(name))));
}
bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result)
@@ -313,6 +312,11 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result)
player->defaultOutfit.lookLegs = result->getNumber("looklegs");
player->defaultOutfit.lookFeet = result->getNumber("lookfeet");
player->defaultOutfit.lookAddons = result->getNumber("lookaddons");
+ player->defaultOutfit.lookMount = result->getNumber("lookmount");
+ player->defaultOutfit.lookMountHead = result->getNumber("lookmounthead");
+ player->defaultOutfit.lookMountBody = result->getNumber("lookmountbody");
+ player->defaultOutfit.lookMountLegs = result->getNumber("lookmountlegs");
+ player->defaultOutfit.lookMountFeet = result->getNumber("lookmountfeet");
player->currentOutfit = player->defaultOutfit;
player->direction = static_cast (result->getNumber("direction"));
@@ -419,6 +423,7 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result)
//load inventory items
ItemMap itemMap;
+ std::map openContainersList;
if ((result = db.storeQuery(fmt::format("SELECT `pid`, `sid`, `itemtype`, `count`, `attributes` FROM `player_items` WHERE `player_id` = {:d} ORDER BY `sid` DESC", player->getGUID())))) {
loadItems(itemMap, result);
@@ -427,6 +432,15 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result)
const std::pair- & pair = it->second;
Item* item = pair.first;
int32_t pid = pair.second;
+
+ Container* itemContainer = item->getContainer();
+ if (itemContainer) {
+ uint8_t cid = item->getIntAttr(ITEM_ATTRIBUTE_OPENCONTAINER);
+ if (cid > 0) {
+ openContainersList.emplace(cid, itemContainer);
+ }
+ }
+
if (pid >= CONST_SLOT_FIRST && pid <= CONST_SLOT_LAST) {
player->internalAddThing(pid, item);
} else {
@@ -443,6 +457,11 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result)
}
}
+ for (auto& it : openContainersList) {
+ player->addContainer(it.first - 1, it.second);
+ player->onSendContainer(it.second);
+ }
+
//load depot items
itemMap.clear();
@@ -556,6 +575,7 @@ bool IOLoginData::saveItems(const Player* player, const ItemBlockList& itemList,
containers.reserve(32);
int32_t runningId = 100;
+ const auto& openContainers = player->getOpenContainers();
Database& db = Database::getInstance();
for (const auto& it : itemList) {
@@ -563,6 +583,26 @@ bool IOLoginData::saveItems(const Player* player, const ItemBlockList& itemList,
Item* item = it.second;
++runningId;
+ if (Container* container = item->getContainer()) {
+ if (container->getIntAttr(ITEM_ATTRIBUTE_OPENCONTAINER)) {
+ container->setIntAttr(ITEM_ATTRIBUTE_OPENCONTAINER, 0);
+ }
+
+ if (!openContainers.empty()) {
+ for (const auto& its : openContainers) {
+ auto openContainer = its.second;
+ auto opcontainer = openContainer.container;
+
+ if (opcontainer == container) {
+ container->setIntAttr(ITEM_ATTRIBUTE_OPENCONTAINER, static_cast(its.first) + 1);
+ break;
+ }
+ }
+ }
+
+ containers.emplace_back(container, runningId);
+ }
+
propWriteStream.clear();
item->serializeAttr(propWriteStream);
@@ -572,10 +612,6 @@ bool IOLoginData::saveItems(const Player* player, const ItemBlockList& itemList,
if (!query_insert.addRow(fmt::format("{:d}, {:d}, {:d}, {:d}, {:d}, {:s}", player->getGUID(), pid, runningId, item->getID(), item->getSubType(), db.escapeBlob(attributes, attributesSize)))) {
return false;
}
-
- if (Container* container = item->getContainer()) {
- containers.emplace_back(container, runningId);
- }
}
for (size_t i = 0; i < containers.size(); i++) {
@@ -589,6 +625,22 @@ bool IOLoginData::saveItems(const Player* player, const ItemBlockList& itemList,
Container* subContainer = item->getContainer();
if (subContainer) {
containers.emplace_back(subContainer, runningId);
+
+ if (subContainer->getIntAttr(ITEM_ATTRIBUTE_OPENCONTAINER)) {
+ subContainer->setIntAttr(ITEM_ATTRIBUTE_OPENCONTAINER, 0);
+ }
+
+ if (!openContainers.empty()) {
+ for (const auto& it : openContainers) {
+ auto openContainer = it.second;
+ auto opcontainer = openContainer.container;
+
+ if (opcontainer == subContainer) {
+ subContainer->setIntAttr(ITEM_ATTRIBUTE_OPENCONTAINER, it.first + 1);
+ break;
+ }
+ }
+ }
}
propWriteStream.clear();
@@ -649,6 +701,11 @@ bool IOLoginData::savePlayer(Player* player)
query << "`looklegs` = " << static_cast(player->defaultOutfit.lookLegs) << ',';
query << "`looktype` = " << player->defaultOutfit.lookType << ',';
query << "`lookaddons` = " << static_cast(player->defaultOutfit.lookAddons) << ',';
+ query << "`lookmount` = " << player->defaultOutfit.lookMount << ',';
+ query << "`lookmounthead` = " << static_cast(player->defaultOutfit.lookMountHead) << ',';
+ query << "`lookmountbody` = " << static_cast(player->defaultOutfit.lookMountBody) << ',';
+ query << "`lookmountlegs` = " << static_cast(player->defaultOutfit.lookMountLegs) << ',';
+ query << "`lookmountfeet` = " << static_cast(player->defaultOutfit.lookMountFeet) << ',';
query << "`maglevel` = " << player->magLevel << ',';
query << "`mana` = " << player->mana << ',';
query << "`manamax` = " << player->manaMax << ',';
diff --git a/src/item.cpp b/src/item.cpp
index 72f7290dcb..f768c125ea 100644
--- a/src/item.cpp
+++ b/src/item.cpp
@@ -616,6 +616,16 @@ Attr_ReadValue Item::readAttr(AttrTypes_t attr, PropStream& propStream)
break;
}
+ case ATTR_OPENCONTAINER: {
+ uint8_t openContainer;
+ if (!propStream.read(openContainer)) {
+ return ATTR_READ_ERROR;
+ }
+
+ setIntAttr(ITEM_ATTRIBUTE_OPENCONTAINER, openContainer);
+ break;
+ }
+
//these should be handled through derived classes
//If these are called then something has changed in the items.xml since the map was saved
//just read the values
@@ -841,6 +851,11 @@ void Item::serializeAttr(PropWriteStream& propWriteStream) const
propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_STOREITEM));
}
+ if (hasAttribute(ITEM_ATTRIBUTE_OPENCONTAINER)) {
+ propWriteStream.write(ATTR_OPENCONTAINER);
+ propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_OPENCONTAINER));
+ }
+
if (hasAttribute(ITEM_ATTRIBUTE_CUSTOM)) {
const ItemAttributes::CustomAttributeMap* customAttrMap = attributes->getCustomAttributeMap();
propWriteStream.write(ATTR_CUSTOM_ATTRIBUTES);
diff --git a/src/item.h b/src/item.h
index 5a4c6896ab..0ffb61f281 100644
--- a/src/item.h
+++ b/src/item.h
@@ -106,6 +106,7 @@ enum AttrTypes_t {
ATTR_WRAPID = 36,
ATTR_STOREITEM = 37,
ATTR_ATTACK_SPEED = 38,
+ ATTR_OPENCONTAINER = 39,
};
enum Attr_ReadValue {
@@ -503,7 +504,7 @@ class ItemAttributes
| ITEM_ATTRIBUTE_ARMOR | ITEM_ATTRIBUTE_HITCHANCE | ITEM_ATTRIBUTE_SHOOTRANGE | ITEM_ATTRIBUTE_OWNER
| ITEM_ATTRIBUTE_DURATION | ITEM_ATTRIBUTE_DECAYSTATE | ITEM_ATTRIBUTE_CORPSEOWNER | ITEM_ATTRIBUTE_CHARGES
| ITEM_ATTRIBUTE_FLUIDTYPE | ITEM_ATTRIBUTE_DOORID | ITEM_ATTRIBUTE_DECAYTO | ITEM_ATTRIBUTE_WRAPID | ITEM_ATTRIBUTE_STOREITEM
- | ITEM_ATTRIBUTE_ATTACK_SPEED;
+ | ITEM_ATTRIBUTE_ATTACK_SPEED | ITEM_ATTRIBUTE_OPENCONTAINER;
const static uint32_t stringAttributeTypes = ITEM_ATTRIBUTE_DESCRIPTION | ITEM_ATTRIBUTE_TEXT | ITEM_ATTRIBUTE_WRITER
| ITEM_ATTRIBUTE_NAME | ITEM_ATTRIBUTE_ARTICLE | ITEM_ATTRIBUTE_PLURALNAME;
diff --git a/src/itemloader.h b/src/itemloader.h
index 159ee2a970..5858756b67 100644
--- a/src/itemloader.h
+++ b/src/itemloader.h
@@ -39,6 +39,7 @@ enum itemgroup_t {
ITEM_GROUP_FLUID,
ITEM_GROUP_DOOR, //deprecated
ITEM_GROUP_DEPRECATED,
+ ITEM_GROUP_PODIUM,
ITEM_GROUP_LAST
};
@@ -103,6 +104,10 @@ enum clientVersion_t {
CLIENT_VERSION_1035 = 55,
CLIENT_VERSION_1076 = 56,
CLIENT_VERSION_1098 = 57,
+ CLIENT_VERSION_1100 = 58,
+ CLIENT_VERSION_1271 = 59,
+
+ CLIENT_VERSION_LAST = CLIENT_VERSION_1271
};
enum rootattrib_ {
@@ -176,6 +181,8 @@ enum itemflags_t {
FLAG_ANIMATION = 1 << 24,
FLAG_FULLTILE = 1 << 25, // unused
FLAG_FORCEUSE = 1 << 26,
+ FLAG_AMMO = 1 << 27, // unused
+ FLAG_REPORTABLE = 1 << 28, // unused
};
//1-byte aligned structs
diff --git a/src/items.cpp b/src/items.cpp
index d310e8c831..e51bc91d4b 100644
--- a/src/items.cpp
+++ b/src/items.cpp
@@ -217,8 +217,8 @@ const std::unordered_map FluidTypesMap = {
Items::Items()
{
- items.reserve(30000);
- nameToItems.reserve(30000);
+ items.reserve(45000);
+ nameToItems.reserve(45000);
}
void Items::clear()
@@ -293,7 +293,7 @@ bool Items::loadFromOtb(const std::string& file)
} else if (majorVersion != 3) {
std::cout << "Old version detected, a newer version of items.otb is required." << std::endl;
return false;
- } else if (minorVersion < CLIENT_VERSION_1098) {
+ } else if (minorVersion < CLIENT_VERSION_LAST) {
std::cout << "A newer version of items.otb is required." << std::endl;
return false;
}
@@ -436,6 +436,7 @@ bool Items::loadFromOtb(const std::string& file)
case ITEM_GROUP_FLUID:
case ITEM_GROUP_CHARGES:
case ITEM_GROUP_DEPRECATED:
+ case ITEM_GROUP_PODIUM:
break;
default:
return false;
@@ -510,32 +511,9 @@ bool Items::loadFromXml()
}
}
- buildInventoryList();
return true;
}
-void Items::buildInventoryList()
-{
- inventory.reserve(items.size());
- for (const auto& type: items) {
- if (type.weaponType != WEAPON_NONE || type.ammoType != AMMO_NONE ||
- type.attack != 0 || type.defense != 0 ||
- type.extraDefense != 0 || type.armor != 0 ||
- type.slotPosition & SLOTP_NECKLACE ||
- type.slotPosition & SLOTP_RING ||
- type.slotPosition & SLOTP_AMMO ||
- type.slotPosition & SLOTP_FEET ||
- type.slotPosition & SLOTP_HEAD ||
- type.slotPosition & SLOTP_ARMOR ||
- type.slotPosition & SLOTP_LEGS)
- {
- inventory.push_back(type.clientId);
- }
- }
- inventory.shrink_to_fit();
- std::sort(inventory.begin(), inventory.end());
-}
-
void Items::parseItemNode(const pugi::xml_node& itemNode, uint16_t id)
{
if (id > 0 && id < 100) {
diff --git a/src/items.h b/src/items.h
index 5640b1fd99..d7c1858187 100644
--- a/src/items.h
+++ b/src/items.h
@@ -423,11 +423,6 @@ class Items
bool loadFromXml();
void parseItemNode(const pugi::xml_node& itemNode, uint16_t id);
- void buildInventoryList();
- const InventoryVector& getInventory() const {
- return inventory;
- }
-
size_t size() const {
return items.size();
}
@@ -441,7 +436,7 @@ class Items
{
public:
ClientIdToServerIdMap() {
- vec.reserve(30000);
+ vec.reserve(45000);
}
void emplace(uint16_t clientId, uint16_t serverId) {
diff --git a/src/luascript.cpp b/src/luascript.cpp
index 9a1955ccd1..1dc9a61c27 100644
--- a/src/luascript.cpp
+++ b/src/luascript.cpp
@@ -154,7 +154,7 @@ void ScriptEnvironment::insertItem(uint32_t uid, Item* item)
Thing* ScriptEnvironment::getThingByUID(uint32_t uid)
{
- if (uid >= 0x10000000) {
+ if (uid >= CREATURE_ID_MIN) {
return g_game.getCreatureByID(uid);
}
@@ -773,18 +773,23 @@ Position LuaScriptInterface::getPosition(lua_State* L, int32_t arg)
Outfit_t LuaScriptInterface::getOutfit(lua_State* L, int32_t arg)
{
Outfit_t outfit;
- outfit.lookMount = getField(L, arg, "lookMount");
- outfit.lookAddons = getField(L, arg, "lookAddons");
- outfit.lookFeet = getField(L, arg, "lookFeet");
- outfit.lookLegs = getField(L, arg, "lookLegs");
- outfit.lookBody = getField(L, arg, "lookBody");
+ outfit.lookType = getField(L, arg, "lookType");
+ outfit.lookTypeEx = getField(L, arg, "lookTypeEx");
+
outfit.lookHead = getField(L, arg, "lookHead");
+ outfit.lookBody = getField(L, arg, "lookBody");
+ outfit.lookLegs = getField(L, arg, "lookLegs");
+ outfit.lookFeet = getField(L, arg, "lookFeet");
+ outfit.lookAddons = getField(L, arg, "lookAddons");
- outfit.lookTypeEx = getField(L, arg, "lookTypeEx");
- outfit.lookType = getField(L, arg, "lookType");
+ outfit.lookMount = getField(L, arg, "lookMount");
+ outfit.lookMountHead = getField(L, arg, "lookMountHead");
+ outfit.lookMountBody = getField(L, arg, "lookMountBody");
+ outfit.lookMountLegs = getField(L, arg, "lookMountLegs");
+ outfit.lookMountFeet = getField(L, arg, "lookMountFeet");
- lua_pop(L, 8);
+ lua_pop(L, 12);
return outfit;
}
@@ -952,7 +957,7 @@ void LuaScriptInterface::pushPosition(lua_State* L, const Position& position, in
void LuaScriptInterface::pushOutfit(lua_State* L, const Outfit_t& outfit)
{
- lua_createtable(L, 0, 8);
+ lua_createtable(L, 0, 12);
setField(L, "lookType", outfit.lookType);
setField(L, "lookTypeEx", outfit.lookTypeEx);
setField(L, "lookHead", outfit.lookHead);
@@ -961,6 +966,10 @@ void LuaScriptInterface::pushOutfit(lua_State* L, const Outfit_t& outfit)
setField(L, "lookFeet", outfit.lookFeet);
setField(L, "lookAddons", outfit.lookAddons);
setField(L, "lookMount", outfit.lookMount);
+ setField(L, "lookMountHead", outfit.lookMountHead);
+ setField(L, "lookMountBody", outfit.lookMountBody);
+ setField(L, "lookMountLegs", outfit.lookMountLegs);
+ setField(L, "lookMountFeet", outfit.lookMountFeet);
}
void LuaScriptInterface::pushOutfit(lua_State* L, const Outfit* outfit)
@@ -1355,6 +1364,50 @@ void LuaScriptInterface::registerFunctions()
registerEnum(CONST_ME_RAGIAZ_BONECAPSULE)
registerEnum(CONST_ME_CRITICAL_DAMAGE)
registerEnum(CONST_ME_PLUNGING_FISH)
+ registerEnum(CONST_ME_BLUECHAIN)
+ registerEnum(CONST_ME_ORANGECHAIN)
+ registerEnum(CONST_ME_GREENCHAIN)
+ registerEnum(CONST_ME_PURPLECHAIN)
+ registerEnum(CONST_ME_GREYCHAIN)
+ registerEnum(CONST_ME_YELLOWCHAIN)
+ registerEnum(CONST_ME_YELLOWSPARKLES)
+ registerEnum(CONST_ME_FAEEXPLOSION)
+ registerEnum(CONST_ME_FAECOMING)
+ registerEnum(CONST_ME_FAEGOING)
+ registerEnum(CONST_ME_BIGCLOUDSSINGLESPACE)
+ registerEnum(CONST_ME_STONESSINGLESPACE)
+ registerEnum(CONST_ME_BLUEGHOST)
+ registerEnum(CONST_ME_POINTOFINTEREST)
+ registerEnum(CONST_ME_MAPEFFECT)
+ registerEnum(CONST_ME_PINKSPARK)
+ registerEnum(CONST_ME_FIREWORK_GREEN)
+ registerEnum(CONST_ME_FIREWORK_ORANGE)
+ registerEnum(CONST_ME_FIREWORK_PURPLE)
+ registerEnum(CONST_ME_FIREWORK_TURQUOISE)
+ registerEnum(CONST_ME_THECUBE)
+ registerEnum(CONST_ME_DRAWINK)
+ registerEnum(CONST_ME_PRISMATICSPARKLES)
+ registerEnum(CONST_ME_THAIAN)
+ registerEnum(CONST_ME_THAIANGHOST)
+ registerEnum(CONST_ME_GHOSTSMOKE)
+ registerEnum(CONST_ME_FLOATINGBLOCK)
+ registerEnum(CONST_ME_BLOCK)
+ registerEnum(CONST_ME_ROOTING)
+ registerEnum(CONST_ME_SUNPRIEST)
+ registerEnum(CONST_ME_WERELION)
+ registerEnum(CONST_ME_GHOSTLYSCRATCH)
+ registerEnum(CONST_ME_GHOSTLYBITE)
+ registerEnum(CONST_ME_BIGSCRATCHING)
+ registerEnum(CONST_ME_SLASH)
+ registerEnum(CONST_ME_BITE)
+ registerEnum(CONST_ME_CHIVALRIOUSCHALLENGE)
+ registerEnum(CONST_ME_DIVINEDAZZLE)
+ registerEnum(CONST_ME_ELECTRICALSPARK)
+ registerEnum(CONST_ME_PURPLETELEPORT)
+ registerEnum(CONST_ME_REDTELEPORT)
+ registerEnum(CONST_ME_ORANGETELEPORT)
+ registerEnum(CONST_ME_GREYTELEPORT)
+ registerEnum(CONST_ME_LIGHTBLUETELEPORT)
registerEnum(CONST_ANI_NONE)
registerEnum(CONST_ANI_SPEAR)
@@ -1407,6 +1460,10 @@ void LuaScriptInterface::registerFunctions()
registerEnum(CONST_ANI_ENVENOMEDARROW)
registerEnum(CONST_ANI_GLOOTHSPEAR)
registerEnum(CONST_ANI_SIMPLEARROW)
+ registerEnum(CONST_ANI_LEAFSTAR)
+ registerEnum(CONST_ANI_DIAMONDARROW)
+ registerEnum(CONST_ANI_SPECTRALBOLT)
+ registerEnum(CONST_ANI_ROYALSTAR)
registerEnum(CONST_ANI_WEAPONTYPE)
registerEnum(CONST_PROP_BLOCKSOLID)
@@ -1447,6 +1504,9 @@ void LuaScriptInterface::registerFunctions()
registerEnum(CREATURE_EVENT_MANACHANGE)
registerEnum(CREATURE_EVENT_EXTENDED_OPCODE)
+ registerEnum(CREATURE_ID_MIN)
+ registerEnum(CREATURE_ID_MAX)
+
registerEnum(GAME_STATE_STARTUP)
registerEnum(GAME_STATE_INIT)
registerEnum(GAME_STATE_NORMAL)
@@ -1455,11 +1515,10 @@ void LuaScriptInterface::registerFunctions()
registerEnum(GAME_STATE_CLOSING)
registerEnum(GAME_STATE_MAINTAIN)
- registerEnum(MESSAGE_STATUS_CONSOLE_BLUE)
- registerEnum(MESSAGE_STATUS_CONSOLE_RED)
registerEnum(MESSAGE_STATUS_DEFAULT)
registerEnum(MESSAGE_STATUS_WARNING)
registerEnum(MESSAGE_EVENT_ADVANCE)
+ registerEnum(MESSAGE_STATUS_WARNING2)
registerEnum(MESSAGE_STATUS_SMALL)
registerEnum(MESSAGE_INFO_DESCR)
registerEnum(MESSAGE_DAMAGE_DEALT)
@@ -1470,12 +1529,20 @@ void LuaScriptInterface::registerFunctions()
registerEnum(MESSAGE_HEALED_OTHERS)
registerEnum(MESSAGE_EXPERIENCE_OTHERS)
registerEnum(MESSAGE_EVENT_DEFAULT)
+ registerEnum(MESSAGE_LOOT)
+ registerEnum(MESSAGE_TRADE)
registerEnum(MESSAGE_GUILD)
registerEnum(MESSAGE_PARTY_MANAGEMENT)
registerEnum(MESSAGE_PARTY)
- registerEnum(MESSAGE_EVENT_ORANGE)
- registerEnum(MESSAGE_STATUS_CONSOLE_ORANGE)
- registerEnum(MESSAGE_LOOT)
+ registerEnum(MESSAGE_REPORT)
+ registerEnum(MESSAGE_HOTKEY_PRESSED)
+ registerEnum(MESSAGE_MARKET)
+ registerEnum(MESSAGE_BEYOND_LAST)
+ registerEnum(MESSAGE_TOURNAMENT_INFO)
+ registerEnum(MESSAGE_ATTENTION)
+ registerEnum(MESSAGE_BOOSTED_CREATURE)
+ registerEnum(MESSAGE_OFFLINE_TRAINING)
+ registerEnum(MESSAGE_TRANSACTION)
registerEnum(CREATURETYPE_PLAYER)
registerEnum(CREATURETYPE_MONSTER)
@@ -1522,6 +1589,7 @@ void LuaScriptInterface::registerFunctions()
registerEnum(ITEM_ATTRIBUTE_WRAPID)
registerEnum(ITEM_ATTRIBUTE_STOREITEM)
registerEnum(ITEM_ATTRIBUTE_ATTACK_SPEED)
+ registerEnum(ITEM_ATTRIBUTE_OPENCONTAINER)
registerEnum(ITEM_TYPE_DEPOT)
registerEnum(ITEM_TYPE_MAILBOX)
@@ -1708,7 +1776,9 @@ void LuaScriptInterface::registerFunctions()
registerEnum(TALKTYPE_PRIVATE_TO)
registerEnum(TALKTYPE_CHANNEL_Y)
registerEnum(TALKTYPE_CHANNEL_O)
+ registerEnum(TALKTYPE_SPELL)
registerEnum(TALKTYPE_PRIVATE_NP)
+ registerEnum(TALKTYPE_PRIVATE_NP_CONSOLE)
registerEnum(TALKTYPE_PRIVATE_PN)
registerEnum(TALKTYPE_BROADCAST)
registerEnum(TALKTYPE_CHANNEL_R1)
@@ -1716,6 +1786,7 @@ void LuaScriptInterface::registerFunctions()
registerEnum(TALKTYPE_PRIVATE_RED_TO)
registerEnum(TALKTYPE_MONSTER_SAY)
registerEnum(TALKTYPE_MONSTER_YELL)
+ registerEnum(TALKTYPE_POTION)
registerEnum(TEXTCOLOR_BLUE)
registerEnum(TEXTCOLOR_LIGHTGREEN)
@@ -1815,7 +1886,10 @@ void LuaScriptInterface::registerFunctions()
registerEnum(SPEECHBUBBLE_NORMAL)
registerEnum(SPEECHBUBBLE_TRADE)
registerEnum(SPEECHBUBBLE_QUEST)
- registerEnum(SPEECHBUBBLE_QUESTTRADER)
+ registerEnum(SPEECHBUBBLE_COMPASS)
+ registerEnum(SPEECHBUBBLE_NORMAL2)
+ registerEnum(SPEECHBUBBLE_NORMAL3)
+ registerEnum(SPEECHBUBBLE_HIRELING)
// Use with player:addMapMark
registerEnum(MAPMARK_TICK)
@@ -7514,7 +7588,14 @@ int LuaScriptInterface::luaCreatureSetMaster(lua_State* L)
}
pushBoolean(L, creature->setMaster(getCreature(L, 2)));
- g_game.updateCreatureType(creature);
+
+ // update summon icon
+ SpectatorVec spectators;
+ g_game.map.getSpectators(spectators, creature->getPosition(), true, true);
+
+ for (Creature* spectator : spectators) {
+ spectator->getPlayer()->sendUpdateTileCreature(creature);
+ }
return 1;
}
@@ -8177,7 +8258,7 @@ int LuaScriptInterface::luaPlayerCreate(lua_State* L)
Player* player;
if (isNumber(L, 2)) {
uint32_t id = getNumber(L, 2);
- if (id >= 0x10000000 && id <= Player::playerAutoID) {
+ if (id >= CREATURE_ID_MIN && id <= Player::playerIDLimit) {
player = g_game.getPlayerByID(id);
} else {
player = g_game.getPlayerByGUID(id);
diff --git a/src/monster.cpp b/src/monster.cpp
index a95b8f0cb9..a779b95c23 100644
--- a/src/monster.cpp
+++ b/src/monster.cpp
@@ -33,7 +33,7 @@ extern ConfigManager g_config;
int32_t Monster::despawnRange;
int32_t Monster::despawnRadius;
-uint32_t Monster::monsterAutoID = 0x40000000;
+uint32_t Monster::monsterAutoID = 0x21000000;
Monster* Monster::createMonster(const std::string& name)
{
diff --git a/src/networkmessage.cpp b/src/networkmessage.cpp
index e3f7f4a7b3..055a8cac32 100644
--- a/src/networkmessage.cpp
+++ b/src/networkmessage.cpp
@@ -101,16 +101,25 @@ void NetworkMessage::addItem(uint16_t id, uint8_t count)
add(it.clientId);
- addByte(0xFF); // MARK_UNMARKED
-
if (it.stackable) {
addByte(count);
} else if (it.isSplash() || it.isFluidContainer()) {
addByte(fluidMap[count & 7]);
+ } else if (it.isContainer()) {
+ addByte(0x00); // assigned loot container icon
+ addByte(0x00); // quiver ammo count
}
- if (it.isAnimation) {
- addByte(0xFE); // random phase (0xFF for async)
+ // podium (placeholder)
+ // to do: read podium flag from otb
+ if (it.clientId == 35973 || it.clientId == 35974) {
+ add(0); //looktype
+ //4x byte colors if not 0
+ add(0); //lookmount
+ //4x byte colors if not 0
+
+ addByte(2); //direction
+ addByte(0x01); //is visible (bool)
}
}
@@ -119,7 +128,6 @@ void NetworkMessage::addItem(const Item* item)
const ItemType& it = Item::items[item->getID()];
add(it.clientId);
- addByte(0xFF); // MARK_UNMARKED
if (it.stackable) {
addByte(std::min(0xFF, item->getItemCount()));
@@ -127,8 +135,21 @@ void NetworkMessage::addItem(const Item* item)
addByte(fluidMap[item->getFluidType() & 7]);
}
- if (it.isAnimation) {
- addByte(0xFE); // random phase (0xFF for async)
+ if (it.isContainer()) {
+ addByte(0x00); // assigned loot container icon
+ addByte(0x00); // quiver ammo count
+ }
+
+ // podium (placeholder)
+ // to do: read podium flag from otb
+ if (it.clientId == 35973 || it.clientId == 35974) {
+ add(0); //looktype
+ //4x byte colors if not 0
+ add(0); //lookmount
+ //4x byte colors if not 0
+
+ addByte(2); //direction
+ addByte(0x01); //is visible (bool)
}
}
diff --git a/src/npc.cpp b/src/npc.cpp
index 0479821f93..2b98d3602c 100644
--- a/src/npc.cpp
+++ b/src/npc.cpp
@@ -26,7 +26,7 @@
extern Game g_game;
extern LuaEnvironment g_luaEnvironment;
-uint32_t Npc::npcAutoID = 0x80000000;
+uint32_t Npc::npcAutoID = 0x20000000;
NpcScriptInterface* Npc::scriptInterface = nullptr;
void Npcs::reload()
diff --git a/src/outputmessage.h b/src/outputmessage.h
index 6e749243f3..17c3465da4 100644
--- a/src/outputmessage.h
+++ b/src/outputmessage.h
@@ -43,9 +43,11 @@ class OutputMessage : public NetworkMessage
add_header(info.length);
}
- void addCryptoHeader(bool addChecksum) {
- if (addChecksum) {
+ void addCryptoHeader(checksumMode_t mode, uint32_t& sequence) {
+ if (mode == CHECKSUM_ADLER) {
add_header(adlerChecksum(buffer + outputBufferStart, info.length));
+ } else if (mode == CHECKSUM_SEQUENCE) {
+ add_header(sequence++);
}
writeMessageLength();
diff --git a/src/party.cpp b/src/party.cpp
index 04281572bf..36ac4467ee 100644
--- a/src/party.cpp
+++ b/src/party.cpp
@@ -47,7 +47,6 @@ void Party::disband()
currentLeader->setParty(nullptr);
currentLeader->sendClosePrivate(CHANNEL_PARTY);
g_game.updatePlayerShield(currentLeader);
- g_game.updatePlayerHelpers(*currentLeader);
currentLeader->sendCreatureSkull(currentLeader);
currentLeader->sendTextMessage(MESSAGE_INFO_DESCR, "Your party has been disbanded.");
@@ -72,7 +71,6 @@ void Party::disband()
member->sendCreatureSkull(currentLeader);
currentLeader->sendCreatureSkull(member);
- g_game.updatePlayerHelpers(*member);
}
memberList.clear();
delete this;
@@ -114,12 +112,10 @@ bool Party::leaveParty(Player* player)
player->setParty(nullptr);
player->sendClosePrivate(CHANNEL_PARTY);
g_game.updatePlayerShield(player);
- g_game.updatePlayerHelpers(*player);
for (Player* member : memberList) {
member->sendCreatureSkull(player);
player->sendPlayerPartyIcons(member);
- g_game.updatePlayerHelpers(*member);
}
leader->sendCreatureSkull(player);
@@ -209,8 +205,6 @@ bool Party::joinParty(Player& player)
memberList.push_back(&player);
- g_game.updatePlayerHelpers(player);
-
player.removePartyInvitation(this);
updateSharedExperience();
@@ -237,12 +231,6 @@ bool Party::removeInvite(Player& player, bool removeFromPlayer/* = true*/)
if (empty()) {
disband();
- } else {
- for (Player* member : memberList) {
- g_game.updatePlayerHelpers(*member);
- }
-
- g_game.updatePlayerHelpers(*leader);
}
return true;
@@ -271,11 +259,6 @@ bool Party::invitePlayer(Player& player)
inviteList.push_back(&player);
- for (Player* member : memberList) {
- g_game.updatePlayerHelpers(*member);
- }
- g_game.updatePlayerHelpers(*leader);
-
leader->sendCreatureShield(&player);
player.sendCreatureShield(leader);
diff --git a/src/player.cpp b/src/player.cpp
index 7ebf6fb5be..682240687e 100644
--- a/src/player.cpp
+++ b/src/player.cpp
@@ -46,6 +46,7 @@ extern Events* g_events;
MuteCountMap Player::muteCountMap;
uint32_t Player::playerAutoID = 0x10000000;
+uint32_t Player::playerIDLimit = 0x20000000;
Player::Player(ProtocolGame_ptr p) :
Creature(), lastPing(OTSYS_TIME()), lastPong(lastPing), inbox(new Inbox(ITEM_INBOX)), storeInbox(new StoreInbox(ITEM_STORE_INBOX)), client(std::move(p))
@@ -78,6 +79,21 @@ Player::~Player()
setEditHouse(nullptr);
}
+void Player::setID() {
+ if (id == 0) {
+ // allowClones id assignment
+ if (g_config.getBoolean(ConfigManager::ALLOW_CLONES)) {
+ id = playerAutoID++;
+ return;
+ }
+
+ // normal id assignment
+ if (guid != 0) {
+ id = playerAutoID + guid;
+ }
+ }
+}
+
bool Player::setVocation(uint16_t vocId)
{
Vocation* voc = g_vocations.getVocation(vocId);
@@ -394,9 +410,9 @@ float Player::getDefenseFactor() const
}
}
-uint16_t Player::getClientIcons() const
+uint32_t Player::getClientIcons() const
{
- uint16_t icons = 0;
+ uint32_t icons = 0;
for (Condition* condition : conditions) {
if (!isSuppress(condition->getType())) {
icons |= condition->getIcons();
@@ -414,7 +430,7 @@ uint16_t Player::getClientIcons() const
icons &= ~ICON_SWORDS;
}
- // Game client debugs with 10 or more icons
+ // Game client crash with more than 10 icons
// so let's prevent that from happening.
std::bitset<20> icon_bitset(static_cast(icons));
for (size_t pos = 0, bits_set = icon_bitset.count(); bits_set >= 10; ++pos) {
@@ -1033,6 +1049,47 @@ void Player::sendRemoveContainerItem(const Container* container, uint16_t slot)
}
}
+void Player::openSavedContainers()
+{
+ std::map openContainersList;
+
+ for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; i++) {
+ Item* item = inventory[i];
+ if (!item) {
+ continue;
+ }
+
+ Container* itemContainer = item->getContainer();
+ if (itemContainer) {
+ uint8_t cid = item->getIntAttr(ITEM_ATTRIBUTE_OPENCONTAINER);
+ if (cid > 0) {
+ openContainersList.emplace(cid, itemContainer);
+ }
+ for (ContainerIterator it = itemContainer->iterator(); it.hasNext(); it.advance()) {
+ Container* subContainer = (*it)->getContainer();
+ if (subContainer) {
+ uint8_t subcid = (*it)->getIntAttr(ITEM_ATTRIBUTE_OPENCONTAINER);
+ if (subcid > 0) {
+ openContainersList.emplace(subcid, subContainer);
+ }
+ }
+ }
+ }
+ }
+
+ // fix broken containers when logged in from another location
+ for (uint8_t i = 0; i < 16; i++) {
+ client->sendEmptyContainer(i);
+ client->sendCloseContainer(i);
+ }
+
+ // send actual containers
+ for (auto& it : openContainersList) {
+ addContainer(it.first - 1, it.second);
+ onSendContainer(it.second);
+ }
+}
+
void Player::onUpdateTileItem(const Tile* tile, const Position& pos, const Item* oldItem,
const ItemType& oldType, const Item* newItem, const ItemType& newType)
{
@@ -1093,6 +1150,18 @@ void Player::onCreatureAppear(Creature* creature, bool isLogin)
bed->wakeUp(this);
}
+ // load mount speed bonus
+ uint8_t currentMountId = currentOutfit.lookMount;
+ if (currentMountId != 0) {
+ Mount* currentMount = g_game.mounts.getMountByID(currentMountId);
+ if (currentMount) {
+ g_game.changeSpeed(this, currentMount->speed);
+ }
+ }
+
+ // mounted player moved to pz on login, update mount status
+ onChangeZone(getZone());
+
Account account = IOLoginData::loadAccount(accountNumber);
if (g_config.getBoolean(ConfigManager::PLAYER_CONSOLE_LOGS)) {
@@ -3029,6 +3098,7 @@ void Player::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_
updateInventoryWeight();
updateItemsLight();
sendStats();
+ sendItems();
}
if (const Item* item = thing->getItem()) {
@@ -3083,6 +3153,7 @@ void Player::postRemoveNotification(Thing* thing, const Cylinder* newParent, int
updateInventoryWeight();
updateItemsLight();
sendStats();
+ sendItems();
}
if (const Item* item = thing->getItem()) {
diff --git a/src/player.h b/src/player.h
index a6f2b2721a..b035cc8446 100644
--- a/src/player.h
+++ b/src/player.h
@@ -130,11 +130,7 @@ class Player final : public Creature, public Cylinder
return this;
}
- void setID() override {
- if (id == 0) {
- id = playerAutoID++;
- }
- }
+ void setID() final;
static MuteCountMap muteCountMap;
@@ -257,7 +253,7 @@ class Player final : public Creature, public Cylinder
return storeInbox;
}
- uint16_t getClientIcons() const;
+ uint32_t getClientIcons() const;
const GuildWarVector& getGuildWarVector() const {
return guildWarVector;
@@ -833,16 +829,6 @@ class Player final : public Creature, public Cylinder
client->sendCreatureShield(creature);
}
}
- void sendCreatureType(uint32_t creatureId, uint8_t creatureType) {
- if (client) {
- client->sendCreatureType(creatureId, creatureType);
- }
- }
- void sendCreatureHelpers(uint32_t creatureId, uint16_t helpers) {
- if (client) {
- client->sendCreatureHelpers(creatureId, helpers);
- }
- }
void sendSpellCooldown(uint8_t spellId, uint32_t time) {
if (client) {
client->sendSpellCooldown(spellId, time);
@@ -876,6 +862,7 @@ class Player final : public Creature, public Cylinder
client->sendItems();
}
}
+ void openSavedContainers();
//event methods
void onUpdateTileItem(const Tile* tile, const Position& pos, const Item* oldItem,
@@ -1163,6 +1150,10 @@ class Player final : public Creature, public Cylinder
void updateRegeneration();
+ const std::map& getOpenContainers() const {
+ return openContainers;
+ }
+
private:
std::forward_list getMuteConditions() const;
@@ -1329,6 +1320,7 @@ class Player final : public Creature, public Cylinder
bool inventoryAbilities[CONST_SLOT_LAST + 1] = {};
static uint32_t playerAutoID;
+ static uint32_t playerIDLimit;
void updateItemsLight(bool internal = false);
int32_t getStepSpeed() const override {
diff --git a/src/protocol.cpp b/src/protocol.cpp
index 9a3470ca36..b1db3b5e1b 100644
--- a/src/protocol.cpp
+++ b/src/protocol.cpp
@@ -60,14 +60,14 @@ bool XTEA_decrypt(NetworkMessage& msg, const xtea::round_keys& key)
}
-void Protocol::onSendMessage(const OutputMessage_ptr& msg) const
+void Protocol::onSendMessage(const OutputMessage_ptr& msg)
{
if (!rawMessages) {
msg->writeMessageLength();
if (encryptionEnabled) {
XTEA_encrypt(*msg, key);
- msg->addCryptoHeader(checksumEnabled);
+ msg->addCryptoHeader(checksumMode, sequenceNumber);
}
}
}
diff --git a/src/protocol.h b/src/protocol.h
index 8bd7d6f240..13e874a636 100644
--- a/src/protocol.h
+++ b/src/protocol.h
@@ -35,7 +35,7 @@ class Protocol : public std::enable_shared_from_this
virtual void parsePacket(NetworkMessage&) {}
- virtual void onSendMessage(const OutputMessage_ptr& msg) const;
+ virtual void onSendMessage(const OutputMessage_ptr& msg);
void onRecvMessage(NetworkMessage& msg);
virtual void onRecvFirstMessage(NetworkMessage& msg) = 0;
virtual void onConnect() {}
@@ -75,8 +75,8 @@ class Protocol : public std::enable_shared_from_this
void setXTEAKey(const xtea::key& key) {
this->key = xtea::expand_key(key);
}
- void disableChecksum() {
- checksumEnabled = false;
+ void setChecksumMode(checksumMode_t newMode) {
+ checksumMode = newMode;
}
static bool RSA_decrypt(NetworkMessage& msg);
@@ -94,8 +94,9 @@ class Protocol : public std::enable_shared_from_this
const ConnectionWeak_ptr connection;
xtea::round_keys key;
+ uint32_t sequenceNumber = 0;
bool encryptionEnabled = false;
- bool checksumEnabled = true;
+ checksumMode_t checksumMode = CHECKSUM_ADLER;
bool rawMessages = false;
};
diff --git a/src/protocolgame.cpp b/src/protocolgame.cpp
index 3db0b0b436..884f8950d2 100644
--- a/src/protocolgame.cpp
+++ b/src/protocolgame.cpp
@@ -277,8 +277,8 @@ void ProtocolGame::connect(uint32_t playerId, OperatingSystem_t operatingSystem)
player->clearModalWindows();
player->setOperatingSystem(operatingSystem);
player->isConnecting = false;
-
player->client = getThis();
+
sendAddCreature(player, player->getPosition(), 0, false);
player->lastIP = player->getIP();
player->lastLoginSaved = std::max(time(nullptr), player->lastLoginSaved + 1);
@@ -318,28 +318,43 @@ void ProtocolGame::logout(bool displayEffect, bool forced)
}
}
+ sendSessionEnd(forced ? SESSION_END_FORCECLOSE : SESSION_END_LOGOUT);
disconnect();
g_game.removeCreature(player);
}
+// Login to the game world request
void ProtocolGame::onRecvFirstMessage(NetworkMessage& msg)
{
+ // Server is shutting down
if (g_game.getGameState() == GAME_STATE_SHUTDOWN) {
disconnect();
return;
}
+ // Client type and OS used
OperatingSystem_t operatingSystem = static_cast(msg.get());
- version = msg.get();
- msg.skipBytes(7); // U32 client version, U8 client type, U16 dat revision
+ version = msg.get(); // U16 client version
+ msg.skipBytes(4); // U32 client version
+
+ // String client version
+ if (version >= 1240) {
+ if (msg.getLength() - msg.getBufferPosition() > 132) {
+ msg.getString();
+ }
+ }
+
+ msg.skipBytes(3); // U16 dat revision, U8 preview state
+ // Disconnect if RSA decrypt fails
if (!Protocol::RSA_decrypt(msg)) {
disconnect();
return;
}
+ // Get XTEA key
xtea::key key;
key[0] = msg.get();
key[1] = msg.get();
@@ -348,6 +363,7 @@ void ProtocolGame::onRecvFirstMessage(NetworkMessage& msg)
enableXTEAEncryption();
setXTEAKey(std::move(key));
+ // Enable extended opcode feature for otclient
if (operatingSystem >= CLIENTOS_OTCLIENT_LINUX) {
NetworkMessage opcodeMessage;
opcodeMessage.addByte(0x32);
@@ -356,16 +372,31 @@ void ProtocolGame::onRecvFirstMessage(NetworkMessage& msg)
writeToOutputBuffer(opcodeMessage);
}
- msg.skipBytes(1); // gamemaster flag
+ // Change packet verifying mode for QT clients
+ if (version >= 1111 && operatingSystem >= CLIENTOS_QT_LINUX && operatingSystem <= CLIENTOS_QT_MAC) {
+ setChecksumMode(CHECKSUM_SEQUENCE);
+ }
+
+ // Web login skips the character list request so we need to check the client version again
+ if (version < CLIENT_VERSION_MIN || version > CLIENT_VERSION_MAX) {
+ disconnectClient(fmt::format("Only clients with protocol {:s} allowed!", CLIENT_VERSION_STR));
+ return;
+ }
+
+ msg.skipBytes(1); // Gamemaster flag
std::string sessionKey = msg.getString();
-
- auto sessionArgs = explodeString(sessionKey, "\n", 4);
+ auto sessionArgs = explodeString(sessionKey, "\n", 4); // acc name or email, password, token, timestamp divided by 30
if (sessionArgs.size() != 4) {
disconnect();
return;
}
+ if (operatingSystem == CLIENTOS_QT_LINUX) {
+ msg.getString(); // OS name (?)
+ msg.getString(); // OS version (?)
+ }
+
std::string& accountName = sessionArgs[0];
std::string& password = sessionArgs[1];
std::string& token = sessionArgs[2];
@@ -394,11 +425,6 @@ void ProtocolGame::onRecvFirstMessage(NetworkMessage& msg)
return;
}
- if (version < CLIENT_VERSION_MIN || version > CLIENT_VERSION_MAX) {
- disconnectClient(fmt::format("Only clients with protocol {:s} allowed!", CLIENT_VERSION_STR));
- return;
- }
-
if (g_game.getGameState() == GAME_STATE_STARTUP) {
disconnectClient("Gameworld is starting up. Please wait.");
return;
@@ -503,6 +529,10 @@ void ProtocolGame::parsePacket(NetworkMessage& msg)
case 0x14: g_dispatcher.addTask(createTask(std::bind(&ProtocolGame::logout, getThis(), true, false))); break;
case 0x1D: addGameTask(&Game::playerReceivePingBack, player->getID()); break;
case 0x1E: addGameTask(&Game::playerReceivePing, player->getID()); break;
+ //case 0x2A: break; // bestiary tracker
+ //case 0x2C: break; // team finder (leader)
+ //case 0x2D: break; // team finder (member)
+ //case 0x28: break; // stash withdraw
case 0x32: parseExtendedOpcode(msg); break; //otclient extended opcode
case 0x64: parseAutoWalk(msg); break;
case 0x65: addGameTask(&Game::playerMove, player->getID(), DIRECTION_NORTH); break;
@@ -518,6 +548,7 @@ void ProtocolGame::parsePacket(NetworkMessage& msg)
case 0x70: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_EAST); break;
case 0x71: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_SOUTH); break;
case 0x72: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_WEST); break;
+ //case 0x73: break; // map click(?)
case 0x77: parseEquipObject(msg); break;
case 0x78: parseThrow(msg); break;
case 0x79: parseLookInShop(msg); break;
@@ -532,6 +563,7 @@ void ProtocolGame::parsePacket(NetworkMessage& msg)
case 0x83: parseUseItemEx(msg); break;
case 0x84: parseUseWithCreature(msg); break;
case 0x85: parseRotateItem(msg); break;
+ //case 0x86: break; // podium interaction
case 0x87: parseCloseContainer(msg); break;
case 0x88: parseUpArrowContainer(msg); break;
case 0x89: parseTextWindow(msg); break;
@@ -540,6 +572,10 @@ void ProtocolGame::parsePacket(NetworkMessage& msg)
case 0x8C: parseLookAt(msg); break;
case 0x8D: parseLookInBattleList(msg); break;
case 0x8E: /* join aggression */ break;
+ //case 0x8F: break; // quick loot
+ //case 0x90: break; // loot container
+ //case 0x91: break; // update loot whitelist
+ //case 0x92: break; // request locker items
case 0x96: parseSay(msg); break;
case 0x97: addGameTask(&Game::playerRequestChannels, player->getID()); break;
case 0x98: parseOpenChannel(msg); break;
@@ -558,20 +594,35 @@ void ProtocolGame::parsePacket(NetworkMessage& msg)
case 0xAA: addGameTask(&Game::playerCreatePrivateChannel, player->getID()); break;
case 0xAB: parseChannelInvite(msg); break;
case 0xAC: parseChannelExclude(msg); break;
+ //case 0xB1: break; // request highscores
case 0xBE: addGameTask(&Game::playerCancelAttackAndFollow, player->getID()); break;
+ //case 0xC7: break; // request tournament leaderboard
case 0xC9: /* update tile */ break;
case 0xCA: parseUpdateContainer(msg); break;
case 0xCB: parseBrowseField(msg); break;
case 0xCC: parseSeekInContainer(msg); break;
+ //case 0xCD: break; // request inspect window
case 0xD2: addGameTask(&Game::playerRequestOutfit, player->getID()); break;
case 0xD3: parseSetOutfit(msg); break;
case 0xD4: parseToggleMount(msg); break;
+ //case 0xD5: break; // apply imbuement
+ //case 0xD6: break; // clear imbuement
+ //case 0xD7: break; // close imbuing window
case 0xDC: parseAddVip(msg); break;
case 0xDD: parseRemoveVip(msg); break;
case 0xDE: parseEditVip(msg); break;
+ //case 0xDF: break; // premium shop (?)
+ //case 0xE0: break; // premium shop (?)
+ //case 0xE1: break; // bestiary 1
+ //case 0xE2: break; // bestiary 2
+ //case 0xE3: break; // bestiary 3
+ //case 0xE4: break; // buy charm rune
+ //case 0xE5: break; // request character info (cyclopedia)
case 0xE6: parseBugReport(msg); break;
case 0xE7: /* thank you */ break;
case 0xE8: parseDebugAssert(msg); break;
+ case 0xEE: addGameTask(&Game::playerSay, player->getID(), 0, TALKTYPE_SAY, std::string(), "hi"); break;
+ //case 0xEF: break; // request store coins transfer
case 0xF0: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerShowQuestLog, player->getID()); break;
case 0xF1: parseQuestLine(msg); break;
case 0xF2: parseRuleViolationReport(msg); break;
@@ -582,6 +633,11 @@ void ProtocolGame::parsePacket(NetworkMessage& msg)
case 0xF7: parseMarketCancelOffer(msg); break;
case 0xF8: parseMarketAcceptOffer(msg); break;
case 0xF9: parseModalWindowAnswer(msg); break;
+ //case 0xFA: break; // store window open
+ //case 0xFB: break; // store window click
+ //case 0xFC: break; // store window buy
+ //case 0xFD: break; // store window history 1
+ //case 0xFE: break; // store window history 2
default:
// std::cout << "Player: " << player->getName() << " sent an unknown packet header: 0x" << std::hex << static_cast(recvbyte) << std::dec << "!" << std::endl;
@@ -595,8 +651,6 @@ void ProtocolGame::parsePacket(NetworkMessage& msg)
void ProtocolGame::GetTileDescription(const Tile* tile, NetworkMessage& msg)
{
- msg.add(0x00); //environmental effects
-
int32_t count;
Item* ground = tile->getGround();
if (ground) {
@@ -842,6 +896,8 @@ void ProtocolGame::parseAutoWalk(NetworkMessage& msg)
void ProtocolGame::parseSetOutfit(NetworkMessage& msg)
{
+ uint8_t outfitType = msg.getByte();
+
Outfit_t newOutfit;
newOutfit.lookType = msg.get();
newOutfit.lookHead = msg.getByte();
@@ -849,8 +905,57 @@ void ProtocolGame::parseSetOutfit(NetworkMessage& msg)
newOutfit.lookLegs = msg.getByte();
newOutfit.lookFeet = msg.getByte();
newOutfit.lookAddons = msg.getByte();
- newOutfit.lookMount = msg.get();
- addGameTask(&Game::playerChangeOutfit, player->getID(), newOutfit);
+
+ // Set outfit window
+ if (outfitType == 0) {
+ newOutfit.lookMount = msg.get();
+ if (newOutfit.lookMount != 0) {
+ newOutfit.lookMountHead = msg.getByte();
+ newOutfit.lookMountBody = msg.getByte();
+ newOutfit.lookMountLegs = msg.getByte();
+ newOutfit.lookMountFeet = msg.getByte();
+ } else {
+ msg.skipBytes(4);
+
+ // prevent mount color settings from resetting
+ const Outfit_t& currentOutfit = player->getCurrentOutfit();
+ newOutfit.lookMountHead = currentOutfit.lookMountHead;
+ newOutfit.lookMountBody = currentOutfit.lookMountBody;
+ newOutfit.lookMountLegs = currentOutfit.lookMountLegs;
+ newOutfit.lookMountFeet = currentOutfit.lookMountFeet;
+ }
+
+ msg.get(); // familiar looktype
+ addGameTask(&Game::playerChangeOutfit, player->getID(), newOutfit);
+
+ // Store "try outfit" window
+ } else if (outfitType == 1) {
+ newOutfit.lookMount = 0;
+ // mount colors or store offerId (needs testing)
+ newOutfit.lookMountHead = msg.getByte();
+ newOutfit.lookMountBody = msg.getByte();
+ newOutfit.lookMountLegs = msg.getByte();
+ newOutfit.lookMountFeet = msg.getByte();
+ //player->? (open store?)
+
+ // Podium interaction
+ } else if (outfitType == 2) {
+ Position pos = msg.getPosition();
+ uint16_t spriteId = msg.get();
+ uint8_t stackpos = msg.getByte();
+ newOutfit.lookMount = msg.get();
+ newOutfit.lookMountHead = msg.getByte();
+ newOutfit.lookMountBody = msg.getByte();
+ newOutfit.lookMountLegs = msg.getByte();
+ newOutfit.lookMountFeet = msg.getByte();
+
+ msg.get(); // familiar looktype
+ msg.getByte(); // outfit direction
+ msg.getByte(); // show outfit (bool)
+
+ //apply to podium
+ //player->getID(), newOutfit, pos, stackpos, spriteId, podiumVisible, direction
+ }
}
void ProtocolGame::parseToggleMount(NetworkMessage& msg)
@@ -999,8 +1104,9 @@ void ProtocolGame::parseFollow(NetworkMessage& msg)
void ProtocolGame::parseEquipObject(NetworkMessage& msg)
{
+ // hotkey equip (?)
uint16_t spriteId = msg.get();
- // msg.get();
+ // msg.get(); // bool smartMode (?)
addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerEquipItem, player->getID(), spriteId);
}
@@ -1206,6 +1312,7 @@ void ProtocolGame::parseMarketCreateOffer(NetworkMessage& msg)
uint32_t price = msg.get();
bool anonymous = (msg.getByte() != 0);
addGameTask(&Game::playerCreateMarketOffer, player->getID(), type, spriteId, amount, price, anonymous);
+ sendStoreBalance();
}
void ProtocolGame::parseMarketCancelOffer(NetworkMessage& msg)
@@ -1213,6 +1320,7 @@ void ProtocolGame::parseMarketCancelOffer(NetworkMessage& msg)
uint32_t timestamp = msg.get();
uint16_t counter = msg.get();
addGameTask(&Game::playerCancelMarketOffer, player->getID(), timestamp, counter);
+ sendStoreBalance();
}
void ProtocolGame::parseMarketAcceptOffer(NetworkMessage& msg)
@@ -1294,6 +1402,16 @@ void ProtocolGame::sendWorldLight(LightInfo lightInfo)
writeToOutputBuffer(msg);
}
+void ProtocolGame::sendWorldTime()
+{
+ int16_t time = g_game.getWorldTime();
+ NetworkMessage msg;
+ msg.addByte(0xEF);
+ msg.addByte(time / 60); // hour
+ msg.addByte(time % 60); // min
+ writeToOutputBuffer(msg);
+}
+
void ProtocolGame::sendCreatureWalkthrough(const Creature* creature, bool walkthrough)
{
if (!canSee(creature)) {
@@ -1337,24 +1455,6 @@ void ProtocolGame::sendCreatureSkull(const Creature* creature)
writeToOutputBuffer(msg);
}
-void ProtocolGame::sendCreatureType(uint32_t creatureId, uint8_t creatureType)
-{
- NetworkMessage msg;
- msg.addByte(0x95);
- msg.add(creatureId);
- msg.addByte(creatureType);
- writeToOutputBuffer(msg);
-}
-
-void ProtocolGame::sendCreatureHelpers(uint32_t creatureId, uint16_t helpers)
-{
- NetworkMessage msg;
- msg.addByte(0x94);
- msg.add(creatureId);
- msg.add(helpers);
- writeToOutputBuffer(msg);
-}
-
void ProtocolGame::sendCreatureSquare(const Creature* creature, SquareColor_t color)
{
if (!canSee(creature)) {
@@ -1381,6 +1481,7 @@ void ProtocolGame::sendAddMarker(const Position& pos, uint8_t markType, const st
{
NetworkMessage msg;
msg.addByte(0xDD);
+ msg.addByte(0x00); // unknown
msg.addPosition(pos);
msg.addByte(markType);
msg.addString(desc);
@@ -1393,6 +1494,7 @@ void ProtocolGame::sendReLoginWindow(uint8_t unfairFightReduction)
msg.addByte(0x28);
msg.addByte(0x00);
msg.addByte(unfairFightReduction);
+ msg.addByte(0x00); // can use death redemption (bool)
writeToOutputBuffer(msg);
}
@@ -1403,6 +1505,33 @@ void ProtocolGame::sendStats()
writeToOutputBuffer(msg);
}
+void ProtocolGame::sendClientFeatures()
+{
+ NetworkMessage msg;
+ msg.addByte(0x17);
+
+ msg.add(player->getID());
+ msg.add(50); // beat duration
+
+ msg.addDouble(Creature::speedA, 3);
+ msg.addDouble(Creature::speedB, 3);
+ msg.addDouble(Creature::speedC, 3);
+
+ // can report bugs?
+ msg.addByte(player->getAccountType() >= ACCOUNT_TYPE_TUTOR ? 0x01 : 0x00);
+
+ msg.addByte(0x00); // can change pvp framing option
+ msg.addByte(0x00); // expert mode button enabled
+
+ msg.add(0x00); // store images url (string or u16 0x00)
+ msg.add(25); // premium coin package size
+
+ msg.addByte(0x00); // exiva button enabled (bool)
+ msg.addByte(0x00); // Tournament button (bool)
+
+ writeToOutputBuffer(msg);
+}
+
void ProtocolGame::sendBasicData()
{
NetworkMessage msg;
@@ -1415,10 +1544,15 @@ void ProtocolGame::sendBasicData()
msg.add(0);
}
msg.addByte(player->getVocation()->getClientId());
- msg.add(0xFF); // number of known spells
+ msg.addByte(0x00); // is prey system enabled (bool)
+
+ // unlock spells on action bar
+ msg.add(0xFF);
for (uint8_t spellId = 0x00; spellId < 0xFF; spellId++) {
msg.addByte(spellId);
}
+
+ msg.addByte(0x00); // is magic shield active (bool)
writeToOutputBuffer(msg);
}
@@ -1536,11 +1670,11 @@ void ProtocolGame::sendChannelMessage(const std::string& author, const std::stri
writeToOutputBuffer(msg);
}
-void ProtocolGame::sendIcons(uint16_t icons)
+void ProtocolGame::sendIcons(uint32_t icons)
{
NetworkMessage msg;
msg.addByte(0xA2);
- msg.add(icons);
+ msg.add(icons);
writeToOutputBuffer(msg);
}
@@ -1560,9 +1694,8 @@ void ProtocolGame::sendContainer(uint8_t cid, const Container* container, bool h
}
msg.addByte(container->capacity());
-
msg.addByte(hasParent ? 0x01 : 0x00);
-
+ msg.addByte(0x00); // show search icon (boolean)
msg.addByte(container->isUnlocked() ? 0x01 : 0x00); // Drag and drop
msg.addByte(container->hasPagination() ? 0x01 : 0x00); // Pagination
@@ -1582,12 +1715,39 @@ void ProtocolGame::sendContainer(uint8_t cid, const Container* container, bool h
writeToOutputBuffer(msg);
}
+void ProtocolGame::sendEmptyContainer(uint8_t cid)
+{
+ NetworkMessage msg;
+ msg.addByte(0x6E);
+
+ msg.addByte(cid);
+
+ msg.addItem(ITEM_BAG, 1);
+ msg.addString("Placeholder");
+
+ msg.addByte(8);
+ msg.addByte(0x00);
+ msg.addByte(0x00);
+ msg.addByte(0x01);
+ msg.addByte(0x00);
+ msg.add(0);
+ msg.add(0);
+ msg.addByte(0x00);
+ writeToOutputBuffer(msg);
+}
+
void ProtocolGame::sendShop(Npc* npc, const ShopInfoList& itemList)
{
NetworkMessage msg;
msg.addByte(0x7A);
msg.addString(npc->getName());
+ // currency displayed in trade window (currently only gold supported)
+ // if item other than gold coin is sent, the shop window takes information
+ // about currency amount from player items packet (the one that updates action bars)
+ msg.add(Item::items[ITEM_GOLD_COIN].clientId);
+ msg.addString(""); // doesn't show anywhere, could be used in otclient for currency name
+
uint16_t itemsToSend = std::min(itemList.size(), std::numeric_limits::max());
msg.add(itemsToSend);
@@ -1608,9 +1768,14 @@ void ProtocolGame::sendCloseShop()
void ProtocolGame::sendSaleItemList(const std::list& shop)
{
+ uint64_t playerBank = player->getBankBalance();
+ uint64_t playerMoney = player->getMoney();
+ sendResourceBalance(RESOURCE_BANK_BALANCE, playerBank);
+ sendResourceBalance(RESOURCE_GOLD_EQUIPPED, playerMoney);
+
NetworkMessage msg;
msg.addByte(0x7B);
- msg.add(player->getMoney() + player->getBankBalance());
+ msg.add(playerBank + playerMoney); // deprecated and ignored by QT client. OTClient still uses it.
std::map saleMap;
@@ -1687,12 +1852,33 @@ void ProtocolGame::sendSaleItemList(const std::list& shop)
writeToOutputBuffer(msg);
}
+void ProtocolGame::sendResourceBalance(const ResourceTypes_t resourceType, uint64_t amount)
+{
+ NetworkMessage msg;
+ msg.addByte(0xEE);
+ msg.addByte(resourceType);
+ msg.add(amount);
+ writeToOutputBuffer(msg);
+}
+
+void ProtocolGame::sendStoreBalance()
+{
+ NetworkMessage msg;
+ msg.addByte(0xDF);
+ msg.addByte(0x01);
+
+ // placeholder packet / to do
+ msg.add(0); // total store coins (transferable + non-t)
+ msg.add(0); // transferable store coins
+ msg.add(0); // reserved auction coins
+ msg.add(0); // tournament coins
+ writeToOutputBuffer(msg);
+}
+
void ProtocolGame::sendMarketEnter(uint32_t depotId)
{
NetworkMessage msg;
msg.addByte(0xF6);
-
- msg.add(player->getBankBalance());
msg.addByte(std::min(IOMarket::getPlayerOfferCount(player->getGUID()), std::numeric_limits::max()));
DepotChest* depotChest = player->getDepotChest(depotId, false);
@@ -1745,6 +1931,10 @@ void ProtocolGame::sendMarketEnter(uint32_t depotId)
}
writeToOutputBuffer(msg);
+
+ sendResourceBalance(RESOURCE_BANK_BALANCE, player->getBankBalance());
+ sendResourceBalance(RESOURCE_GOLD_EQUIPPED, player->getMoney());
+ sendStoreBalance();
}
void ProtocolGame::sendMarketLeave()
@@ -1779,6 +1969,7 @@ void ProtocolGame::sendMarketBrowseItem(uint16_t itemId, const MarketOfferList&
msg.addString(offer.playerName);
}
+ sendStoreBalance();
writeToOutputBuffer(msg);
}
@@ -2074,6 +2265,13 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId)
msg.add(0x00);
}
+ //string or u16 0x00
+ msg.add(0x00); // magic shield capacity
+ msg.add(0x00); // cleave
+ msg.add(0x00); // damage reflection
+ msg.add(0x00); // perfect shot
+ msg.add(0x00); // imbuing slots
+
MarketStatistics* statistics = IOMarket::getInstance().getPurchaseStatistics(itemId);
if (statistics) {
msg.addByte(0x01);
@@ -2219,6 +2417,7 @@ void ProtocolGame::sendCreatureSay(const Creature* creature, SpeakClasses type,
msg.add(++statementId);
msg.addString(creature->getName());
+ msg.addByte(0x00); // "(Traded)" suffix after player name
//Add level only for players
if (const Player* speaker = creature->getPlayer()) {
@@ -2247,8 +2446,11 @@ void ProtocolGame::sendToChannel(const Creature* creature, SpeakClasses type, co
msg.add(++statementId);
if (!creature) {
msg.add(0x00);
+ msg.addByte(0x00); // "(Traded)" suffix after player name
} else {
msg.addString(creature->getName());
+ msg.addByte(0x00); // "(Traded)" suffix after player name
+
//Add level only for players
if (const Player* speaker = creature->getPlayer()) {
msg.add(speaker->getLevel());
@@ -2271,9 +2473,11 @@ void ProtocolGame::sendPrivateMessage(const Player* speaker, SpeakClasses type,
msg.add(++statementId);
if (speaker) {
msg.addString(speaker->getName());
+ msg.addByte(0x00); // "(Traded)" suffix after player name
msg.add(speaker->getLevel());
} else {
msg.add(0x00);
+ msg.addByte(0x00); // "(Traded)" suffix after player name
}
msg.addByte(type);
msg.addString(text);
@@ -2330,10 +2534,13 @@ void ProtocolGame::sendPingBack()
void ProtocolGame::sendDistanceShoot(const Position& from, const Position& to, uint8_t type)
{
NetworkMessage msg;
- msg.addByte(0x85);
+ msg.addByte(0x83);
msg.addPosition(from);
- msg.addPosition(to);
+ msg.addByte(MAGIC_EFFECTS_CREATE_DISTANCEEFFECT);
msg.addByte(type);
+ msg.addByte(static_cast(static_cast(static_cast(to.x) - static_cast(from.x))));
+ msg.addByte(static_cast(static_cast(static_cast(to.y) - static_cast(from.y))));
+ msg.addByte(MAGIC_EFFECTS_END_LOOP);
writeToOutputBuffer(msg);
}
@@ -2346,7 +2553,9 @@ void ProtocolGame::sendMagicEffect(const Position& pos, uint8_t type)
NetworkMessage msg;
msg.addByte(0x83);
msg.addPosition(pos);
+ msg.addByte(MAGIC_EFFECTS_CREATE_EFFECT);
msg.addByte(type);
+ msg.addByte(MAGIC_EFFECTS_END_LOOP);
writeToOutputBuffer(msg);
}
@@ -2545,58 +2754,46 @@ void ProtocolGame::sendAddCreature(const Creature* creature, const Position& pos
return;
}
- NetworkMessage msg;
- msg.addByte(0x17);
-
- msg.add(player->getID());
- msg.add(0x32); // beat duration (50)
-
- msg.addDouble(Creature::speedA, 3);
- msg.addDouble(Creature::speedB, 3);
- msg.addDouble(Creature::speedC, 3);
-
- // can report bugs?
- if (player->getAccountType() >= ACCOUNT_TYPE_TUTOR) {
- msg.addByte(0x01);
- } else {
- msg.addByte(0x00);
- }
-
- msg.addByte(0x00); // can change pvp framing option
- msg.addByte(0x00); // expert mode button enabled
-
- msg.add(0x00); // URL (string) to ingame store images
- msg.add(25); // premium coin package size
+ // send player stats
+ sendStats(); // hp, cap, level, xp rate, etc.
+ sendSkills(); // skills and special skills
+ player->sendIcons(); // active conditions
- writeToOutputBuffer(msg);
+ // send client info
+ sendClientFeatures(); // player speed, bug reports, store url, pvp mode, etc
+ sendBasicData(); // premium account, vocation, known spells, prey system status, magic shield status
+ sendItems(); // send carried items for action bars
+ // enter world and send game screen
sendPendingStateEntered();
sendEnterWorld();
sendMapDescription(pos);
+ // send login effect
if (isLogin) {
sendMagicEffect(pos, CONST_ME_TELEPORT);
}
+ // send equipment
for (int i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) {
sendInventoryItem(static_cast(i), player->getInventoryItem(static_cast(i)));
}
+ // send store inbox
sendInventoryItem(CONST_SLOT_STORE_INBOX, player->getStoreInbox()->getItem());
- sendStats();
- sendSkills();
-
- //gameworld light-settings
+ // gameworld time of the day
sendWorldLight(g_game.getWorldLightInfo());
+ sendWorldTime();
- //player light level
+ // player light level
sendCreatureLight(creature);
+ // player vip list
sendVIPEntries();
- sendBasicData();
- player->sendIcons();
+ // opened containers
+ player->openSavedContainers();
}
void ProtocolGame::sendMoveCreature(const Creature* creature, const Position& newPos, int32_t newStackPos, const Position& oldPos, int32_t oldStackPos, bool teleport)
@@ -2682,23 +2879,27 @@ void ProtocolGame::sendInventoryItem(slots_t slot, const Item* item)
writeToOutputBuffer(msg);
}
+// to do: make it lightweight, update each time player gets/loses an item
void ProtocolGame::sendItems()
{
NetworkMessage msg;
msg.addByte(0xF5);
- const std::vector& inventory = Item::items.getInventory();
+ // find all items carried by character (itemId, amount)
+ std::map inventory;
+ player->getAllItemTypeCount(inventory);
+
msg.add(inventory.size() + 11);
for (uint16_t i = 1; i <= 11; i++) {
- msg.add(i);
- msg.addByte(0); //always 0
+ msg.add(i); // slotId
+ msg.addByte(0); // always 0
msg.add(1); // always 1
}
- for (auto clientId : inventory) {
- msg.add(clientId);
- msg.addByte(0); //always 0
- msg.add(1);
+ for (const auto& item : inventory) {
+ msg.add(Item::items[item.first].clientId); // item clientId
+ msg.addByte(0); // always 0
+ msg.add(item.second); // count
}
writeToOutputBuffer(msg);
@@ -2761,6 +2962,8 @@ void ProtocolGame::sendTextWindow(uint32_t windowTextId, Item* item, uint16_t ma
msg.add(0x00);
}
+ msg.addByte(0x00); // "(traded)" suffix after player name (bool)
+
time_t writtenDate = item->getDate();
if (writtenDate != 0) {
msg.addString(formatDateShort(writtenDate));
@@ -2779,8 +2982,9 @@ void ProtocolGame::sendTextWindow(uint32_t windowTextId, uint32_t itemId, const
msg.addItem(itemId, 1);
msg.add(text.size());
msg.addString(text);
- msg.add(0x00);
- msg.add(0x00);
+ msg.add(0x00); // writer name
+ msg.addByte(0x00); // "(traded)" byte
+ msg.add(0x00); // date
writeToOutputBuffer(msg);
}
@@ -2805,6 +3009,8 @@ void ProtocolGame::sendOutfitWindow()
msg.addByte(0xC8);
Outfit_t currentOutfit = player->getDefaultOutfit();
+ bool mounted = currentOutfit.lookMount != 0;
+
if (currentOutfit.lookType == 0) {
Outfit_t newOutfit;
newOutfit.lookType = outfits.front().lookType;
@@ -2818,6 +3024,16 @@ void ProtocolGame::sendOutfitWindow()
AddOutfit(msg, currentOutfit);
+ // mount color bytes are required here regardless of having one
+ if (currentOutfit.lookMount == 0){
+ msg.addByte(currentOutfit.lookMountHead);
+ msg.addByte(currentOutfit.lookMountBody);
+ msg.addByte(currentOutfit.lookMountLegs);
+ msg.addByte(currentOutfit.lookMountFeet);
+ }
+
+ msg.add(0); // current familiar looktype
+
std::vector protocolOutfits;
if (player->isAccessPlayer()) {
static const std::string gamemasterOutfitName = "Gamemaster";
@@ -2832,16 +3048,14 @@ void ProtocolGame::sendOutfitWindow()
}
protocolOutfits.emplace_back(outfit.name, outfit.lookType, addons);
- if (protocolOutfits.size() == std::numeric_limits::max()) { // Game client currently doesn't allow more than 255 outfits
- break;
- }
}
- msg.addByte(protocolOutfits.size());
+ msg.add(protocolOutfits.size());
for (const ProtocolOutfit& outfit : protocolOutfits) {
msg.add(outfit.lookType);
msg.addString(outfit.name);
msg.addByte(outfit.addons);
+ msg.addByte(0x00); // mode: 0x00 - available, 0x01 store (requires U32 store offerId), 0x02 golden outfit tooltip (hardcoded)
}
std::vector mounts;
@@ -2851,12 +3065,21 @@ void ProtocolGame::sendOutfitWindow()
}
}
- msg.addByte(mounts.size());
+ msg.add(mounts.size());
for (const Mount* mount : mounts) {
msg.add(mount->clientId);
msg.addString(mount->name);
+ msg.addByte(0x00); // mode: 0x00 - available, 0x01 store (requires U32 store offerId)
}
+ msg.add(0x00); // familiars.size()
+ // size > 0
+ // U16 looktype
+ // String name
+ // 0x00 // mode: 0x00 - available, 0x01 store (requires U32 store offerId)
+
+ msg.addByte(0x00); //Try outfit mode (?)
+ msg.addByte(mounted ? 0x01 : 0x00);
writeToOutputBuffer(msg);
}
@@ -2879,6 +3102,7 @@ void ProtocolGame::sendVIP(uint32_t guid, const std::string& name, const std::st
msg.add(std::min(10, icon));
msg.addByte(notify ? 0x01 : 0x00);
msg.addByte(status);
+ msg.addByte(0x00); // vipGroups (placeholder)
writeToOutputBuffer(msg);
}
@@ -2945,12 +3169,32 @@ void ProtocolGame::sendModalWindow(const ModalWindow& modalWindow)
writeToOutputBuffer(msg);
}
+void ProtocolGame::sendSessionEnd(SessionEndTypes_t reason)
+{
+ auto output = OutputMessagePool::getOutputMessage();
+ output->addByte(0x18);
+ output->addByte(reason);
+ send(output);
+}
+
////////////// Add common messages
void ProtocolGame::AddCreature(NetworkMessage& msg, const Creature* creature, bool known, uint32_t remove)
{
CreatureType_t creatureType = creature->getType();
-
const Player* otherPlayer = creature->getPlayer();
+ const Player* masterPlayer = nullptr;
+ uint32_t masterId = 0;
+
+ if (creatureType == CREATURETYPE_MONSTER) {
+ const Creature* master = creature->getMaster();
+ if (master) {
+ masterPlayer = master->getPlayer();
+ if (masterPlayer) {
+ masterId = master->getID();
+ creatureType = CREATURETYPE_SUMMON_OWN;
+ }
+ }
+ }
if (known) {
msg.add(0x62);
@@ -2959,8 +3203,13 @@ void ProtocolGame::AddCreature(NetworkMessage& msg, const Creature* creature, bo
msg.add(0x61);
msg.add(remove);
msg.add(creature->getID());
- msg.addByte(creatureType);
- msg.addString(creature->getName());
+ msg.addByte(creature->isHealthHidden() ? CREATURETYPE_HIDDEN : creatureType);
+
+ if (creatureType == CREATURETYPE_SUMMON_OWN) {
+ msg.add(masterId);
+ }
+
+ msg.addString(creature->isHealthHidden() ? "" : creature->getName());
}
if (creature->isHealthHidden()) {
@@ -2972,7 +3221,8 @@ void ProtocolGame::AddCreature(NetworkMessage& msg, const Creature* creature, bo
msg.addByte(creature->getDirection());
if (!creature->isInGhostMode() && !creature->isInvisible()) {
- AddOutfit(msg, creature->getCurrentOutfit());
+ const Outfit_t& outfit = creature->getCurrentOutfit();
+ AddOutfit(msg, outfit);
} else {
static Outfit_t outfit;
AddOutfit(msg, outfit);
@@ -2984,6 +3234,15 @@ void ProtocolGame::AddCreature(NetworkMessage& msg, const Creature* creature, bo
msg.add(creature->getStepSpeed() / 2);
+ msg.addByte(0x00); //creature debuffs, to do
+ /*
+ if (icon != CREATUREICON_NONE) {
+ msg.addByte(icon);
+ msg.addByte(1);
+ msg.add(0);
+ }
+ */
+
msg.addByte(player->getSkullClient(creature));
msg.addByte(player->getPartyShield(otherPlayer));
@@ -2991,29 +3250,25 @@ void ProtocolGame::AddCreature(NetworkMessage& msg, const Creature* creature, bo
msg.addByte(player->getGuildEmblem(otherPlayer));
}
- if (creatureType == CREATURETYPE_MONSTER) {
- const Creature* master = creature->getMaster();
- if (master) {
- const Player* masterPlayer = master->getPlayer();
- if (masterPlayer) {
- if (masterPlayer == player) {
- creatureType = CREATURETYPE_SUMMON_OWN;
- } else {
- creatureType = CREATURETYPE_SUMMON_OTHERS;
- }
- }
+ // Creature type and summon emblem
+ msg.addByte(creature->isHealthHidden() ? CREATURETYPE_HIDDEN : creatureType);
+ if (creatureType == CREATURETYPE_SUMMON_OWN) {
+ msg.add(masterId);
+ }
+
+ // Player vocation info
+ if (creatureType == CREATURETYPE_PLAYER) {
+ const Player* otherCreature = creature->getPlayer();
+ if (otherCreature) {
+ msg.addByte(otherCreature->getVocation()->getClientId());
+ } else {
+ msg.addByte(0x00);
}
}
- msg.addByte(creatureType); // Type (for summons)
msg.addByte(creature->getSpeechBubble());
msg.addByte(0xFF); // MARK_UNMARKED
-
- if (otherPlayer) {
- msg.add(otherPlayer->getHelpers());
- } else {
- msg.add(0x00);
- }
+ msg.addByte(0x00); // inspection type (bool?)
msg.addByte(player->canWalkthroughEx(creature) ? 0x00 : 0x01);
}
@@ -3026,15 +3281,12 @@ void ProtocolGame::AddPlayerStats(NetworkMessage& msg)
msg.add(std::min(player->getMaxHealth(), std::numeric_limits::max()));
msg.add(player->getFreeCapacity());
- msg.add(player->getCapacity());
-
msg.add(player->getExperience());
msg.add(player->getLevel());
msg.addByte(player->getLevelPercent());
msg.add(100); // base xp gain rate
- msg.add(0); // xp voucher
msg.add(0); // low level bonus
msg.add(0); // xp boost
msg.add(100); // stamina multiplier (100 = x1.0)
@@ -3042,14 +3294,8 @@ void ProtocolGame::AddPlayerStats(NetworkMessage& msg)
msg.add(std::min(player->getMana(), std::numeric_limits::max()));
msg.add(std::min(player->getMaxMana(), std::numeric_limits::max()));
- msg.addByte(std::min(player->getMagicLevel(), std::numeric_limits::max()));
- msg.addByte(std::min(player->getBaseMagicLevel(), std::numeric_limits::max()));
- msg.addByte(player->getMagicLevelPercent());
-
msg.addByte(player->getSoul());
-
msg.add(player->getStaminaMinutes());
-
msg.add(player->getBaseSpeed() / 2);
Condition* condition = player->getCondition(CONDITION_REGENERATION);
@@ -3058,29 +3304,41 @@ void ProtocolGame::AddPlayerStats(NetworkMessage& msg)
msg.add(player->getOfflineTrainingTime() / 60 / 1000);
msg.add(0); // xp boost time (seconds)
- msg.addByte(0); // enables exp boost in the store
+ msg.addByte(0x00); // enables exp boost in the store
+
+ msg.add(0); // remaining mana shield
+ msg.add(0); // total mana shield
}
void ProtocolGame::AddPlayerSkills(NetworkMessage& msg)
{
msg.addByte(0xA1);
+ msg.add(player->getMagicLevel());
+ msg.add(player->getBaseMagicLevel());
+ msg.add(player->getBaseMagicLevel()); // base + loyalty bonus(?)
+ msg.add(player->getMagicLevelPercent() * 100);
for (uint8_t i = SKILL_FIRST; i <= SKILL_LAST; ++i) {
msg.add(std::min(player->getSkillLevel(i), std::numeric_limits::max()));
msg.add(player->getBaseSkill(i));
- msg.addByte(player->getSkillPercent(i));
+ msg.add(player->getBaseSkill(i)); // base + loyalty bonus(?)
+ msg.add(player->getSkillPercent(i) * 100);
}
for (uint8_t i = SPECIALSKILL_FIRST; i <= SPECIALSKILL_LAST; ++i) {
- msg.add(std::min(100, player->varSpecialSkills[i]));
- msg.add(0);
+ msg.add(std::min(100, player->varSpecialSkills[i])); // base + bonus special skill
+ msg.add(0); // base special skill
}
+
+ // to do: bonus cap
+ msg.add(player->getCapacity()); // base + bonus capacity
+ msg.add(player->getCapacity()); // base capacity
}
void ProtocolGame::AddOutfit(NetworkMessage& msg, const Outfit_t& outfit)
{
+ // outfit
msg.add(outfit.lookType);
-
if (outfit.lookType != 0) {
msg.addByte(outfit.lookHead);
msg.addByte(outfit.lookBody);
@@ -3091,7 +3349,14 @@ void ProtocolGame::AddOutfit(NetworkMessage& msg, const Outfit_t& outfit)
msg.addItemId(outfit.lookTypeEx);
}
+ // mount
msg.add(outfit.lookMount);
+ if (outfit.lookMount != 0) {
+ msg.addByte(outfit.lookMountHead);
+ msg.addByte(outfit.lookMountBody);
+ msg.addByte(outfit.lookMountLegs);
+ msg.addByte(outfit.lookMountFeet);
+ }
}
void ProtocolGame::AddWorldLight(NetworkMessage& msg, LightInfo lightInfo)
@@ -3233,8 +3498,8 @@ void ProtocolGame::AddShopItem(NetworkMessage& msg, const ShopInfo& item)
msg.addString(item.realName);
msg.add(it.weight);
- msg.add(item.buyPrice);
- msg.add(item.sellPrice);
+ msg.add(item.buyPrice == std::numeric_limits::max() ? 0 : item.buyPrice);
+ msg.add(item.sellPrice == std::numeric_limits::max() ? 0 : item.sellPrice);
}
void ProtocolGame::parseExtendedOpcode(NetworkMessage& msg)
diff --git a/src/protocolgame.h b/src/protocolgame.h
index 7d9055e973..32e8dcdb58 100644
--- a/src/protocolgame.h
+++ b/src/protocolgame.h
@@ -25,6 +25,13 @@
#include "creature.h"
#include "tasks.h"
+enum SessionEndTypes_t : uint8_t {
+ SESSION_END_LOGOUT = 0,
+ SESSION_END_UNKNOWN = 1, // unknown, no difference from logout
+ SESSION_END_FORCECLOSE = 2,
+ SESSION_END_UNKNOWN2 = 3, // unknown, no difference from logout
+};
+
class NetworkMessage;
class Player;
class Game;
@@ -174,7 +181,7 @@ class ProtocolGame final : public Protocol
void sendOpenPrivateChannel(const std::string& receiver);
void sendToChannel(const Creature* creature, SpeakClasses type, const std::string& text, uint16_t channelId);
void sendPrivateMessage(const Player* speaker, SpeakClasses type, const std::string& text);
- void sendIcons(uint16_t icons);
+ void sendIcons(uint32_t icons);
void sendFYIBox(const std::string& message);
void sendDistanceShoot(const Position& from, const Position& to, uint8_t type);
@@ -194,6 +201,7 @@ class ProtocolGame final : public Protocol
void sendCancelTarget();
void sendCreatureOutfit(const Creature* creature, const Outfit_t& outfit);
void sendStats();
+ void sendClientFeatures();
void sendBasicData();
void sendTextMessage(const TextMessage& message);
void sendReLoginWindow(uint8_t unfairFightReduction);
@@ -204,12 +212,12 @@ class ProtocolGame final : public Protocol
void sendCreatureWalkthrough(const Creature* creature, bool walkthrough);
void sendCreatureShield(const Creature* creature);
void sendCreatureSkull(const Creature* creature);
- void sendCreatureType(uint32_t creatureId, uint8_t creatureType);
- void sendCreatureHelpers(uint32_t creatureId, uint16_t helpers);
void sendShop(Npc* npc, const ShopInfoList& itemList);
void sendCloseShop();
void sendSaleItemList(const std::list& shop);
+ void sendResourceBalance(const ResourceTypes_t resourceType, uint64_t amount);
+ void sendStoreBalance();
void sendMarketEnter(uint32_t depotId);
void sendMarketLeave();
void sendMarketBrowseItem(uint16_t itemId, const MarketOfferList& buyOffers, const MarketOfferList& sellOffers);
@@ -237,6 +245,7 @@ class ProtocolGame final : public Protocol
void sendCreatureLight(const Creature* creature);
void sendWorldLight(LightInfo lightInfo);
+ void sendWorldTime();
void sendCreatureSquare(const Creature* creature, SquareColor_t color);
@@ -263,6 +272,7 @@ class ProtocolGame final : public Protocol
void sendRemoveContainerItem(uint8_t cid, uint16_t slot, const Item* lastItem);
void sendContainer(uint8_t cid, const Container* container, bool hasParent, uint16_t firstIndex);
+ void sendEmptyContainer(uint8_t cid);
void sendCloseContainer(uint8_t cid);
//inventory
@@ -272,6 +282,9 @@ class ProtocolGame final : public Protocol
//messages
void sendModalWindow(const ModalWindow& modalWindow);
+ //session end
+ void sendSessionEnd(SessionEndTypes_t reason);
+
//Help functions
// translate a tile to client-readable format
diff --git a/src/protocollogin.cpp b/src/protocollogin.cpp
index 309dce5c89..bb0e099ae4 100644
--- a/src/protocollogin.cpp
+++ b/src/protocollogin.cpp
@@ -129,6 +129,7 @@ void ProtocolLogin::getCharacterList(const std::string& accountName, const std::
disconnect();
}
+// Character list request
void ProtocolLogin::onRecvFirstMessage(NetworkMessage& msg)
{
if (g_game.getGameState() == GAME_STATE_SHUTDOWN) {
@@ -139,6 +140,15 @@ void ProtocolLogin::onRecvFirstMessage(NetworkMessage& msg)
msg.skipBytes(2); // client OS
uint16_t version = msg.get();
+ if (version <= 822) {
+ setChecksumMode(CHECKSUM_DISABLED);
+ }
+
+ if (version <= 760) {
+ disconnectClient(fmt::format("Only clients with protocol {:s} allowed!", CLIENT_VERSION_STR), version);
+ return;
+ }
+
if (version >= 971) {
msg.skipBytes(17);
} else {
@@ -151,11 +161,6 @@ void ProtocolLogin::onRecvFirstMessage(NetworkMessage& msg)
* 1 byte: 0
*/
- if (version <= 760) {
- disconnectClient(fmt::format("Only clients with protocol {:s} allowed!", CLIENT_VERSION_STR), version);
- return;
- }
-
if (!Protocol::RSA_decrypt(msg)) {
disconnect();
return;
diff --git a/src/protocolold.cpp b/src/protocolold.cpp
index 49900b2d7d..172922a974 100644
--- a/src/protocolold.cpp
+++ b/src/protocolold.cpp
@@ -68,7 +68,7 @@ void ProtocolOld::onRecvFirstMessage(NetworkMessage& msg)
setXTEAKey(std::move(key));
if (version <= 822) {
- disableChecksum();
+ setChecksumMode(CHECKSUM_DISABLED);
}
disconnectClient(fmt::format("Only clients with protocol {:s} allowed!", CLIENT_VERSION_STR));
diff --git a/src/raids.cpp b/src/raids.cpp
index ab308841aa..15ed944be4 100644
--- a/src/raids.cpp
+++ b/src/raids.cpp
@@ -324,10 +324,8 @@ bool AnnounceEvent::configureRaidEvent(const pugi::xml_node& eventNode)
messageType = MESSAGE_INFO_DESCR;
} else if (tmpStrValue == "smallstatus") {
messageType = MESSAGE_STATUS_SMALL;
- } else if (tmpStrValue == "blueconsole") {
- messageType = MESSAGE_STATUS_CONSOLE_BLUE;
- } else if (tmpStrValue == "redconsole") {
- messageType = MESSAGE_STATUS_CONSOLE_RED;
+ } else if (tmpStrValue == "blueconsole" || tmpStrValue == "redconsole") {
+ std::cout << "[Notice] Raid: Deprecated type tag for announce event. Using default: " << static_cast(messageType) << std::endl;
} else {
std::cout << "[Notice] Raid: Unknown type tag missing for announce event. Using default: " << static_cast(messageType) << std::endl;
}
diff --git a/src/server.cpp b/src/server.cpp
index d0ed14f769..58ca3c5672 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -132,17 +132,14 @@ void ServicePort::onAccept(Connection_ptr connection, const boost::system::error
}
}
-Protocol_ptr ServicePort::make_protocol(bool checksummed, NetworkMessage& msg, const Connection_ptr& connection) const
+Protocol_ptr ServicePort::make_protocol(NetworkMessage& msg, const Connection_ptr& connection) const
{
uint8_t protocolID = msg.getByte();
for (auto& service : services) {
if (protocolID != service->get_protocol_identifier()) {
continue;
}
-
- if ((checksummed && service->is_checksummed()) || !service->is_checksummed()) {
- return service->make_protocol(connection);
- }
+ return service->make_protocol(connection);
}
return nullptr;
}
diff --git a/src/server.h b/src/server.h
index 60274bd263..4892f7419a 100644
--- a/src/server.h
+++ b/src/server.h
@@ -76,7 +76,7 @@ class ServicePort : public std::enable_shared_from_this
std::string get_protocol_names() const;
bool add_service(const Service_ptr& new_svc);
- Protocol_ptr make_protocol(bool checksummed, NetworkMessage& msg, const Connection_ptr& connection) const;
+ Protocol_ptr make_protocol(NetworkMessage& msg, const Connection_ptr& connection) const;
void onStopServer();
void onAccept(Connection_ptr connection, const boost::system::error_code& error);
diff --git a/src/tools.cpp b/src/tools.cpp
index 4e8bc70fa4..f99939dac0 100644
--- a/src/tools.cpp
+++ b/src/tools.cpp
@@ -464,7 +464,7 @@ Direction getDirectionTo(const Position& from, const Position& to)
if (from == to) {
return DIRECTION_NONE;
}
-
+
Direction dir;
int32_t x_offset = Position::getOffsetX(from, to);
@@ -590,6 +590,54 @@ MagicEffectNames magicEffectNames = {
{"yellowsmoke", CONST_ME_YELLOWSMOKE},
{"greensmoke", CONST_ME_GREENSMOKE},
{"purplesmoke", CONST_ME_PURPLESMOKE},
+ {"earlythunder", CONST_ME_EARLY_THUNDER},
+ {"bonecapsule", CONST_ME_RAGIAZ_BONECAPSULE},
+ {"criticaldamage", CONST_ME_CRITICAL_DAMAGE},
+ {"plungingfish", CONST_ME_PLUNGING_FISH},
+ {"bluechain", CONST_ME_BLUECHAIN},
+ {"orangechain", CONST_ME_ORANGECHAIN},
+ {"greenchain", CONST_ME_GREENCHAIN},
+ {"purplechain", CONST_ME_PURPLECHAIN},
+ {"greychain", CONST_ME_GREYCHAIN},
+ {"yellowchain", CONST_ME_YELLOWCHAIN},
+ {"yellowsparkles", CONST_ME_YELLOWSPARKLES},
+ {"faeexplosion", CONST_ME_FAEEXPLOSION},
+ {"faecoming", CONST_ME_FAECOMING},
+ {"faegoing", CONST_ME_FAEGOING},
+ {"bigcloudssinglespace", CONST_ME_BIGCLOUDSSINGLESPACE},
+ {"stonessinglespace", CONST_ME_STONESSINGLESPACE},
+ {"blueghost", CONST_ME_BLUEGHOST},
+ {"pointofinterest", CONST_ME_POINTOFINTEREST},
+ {"mapeffect", CONST_ME_MAPEFFECT},
+ {"pinkspark", CONST_ME_PINKSPARK},
+ {"greenfirework", CONST_ME_FIREWORK_GREEN},
+ {"orangefirework", CONST_ME_FIREWORK_ORANGE},
+ {"purplefirework", CONST_ME_FIREWORK_PURPLE},
+ {"turquoisefirework", CONST_ME_FIREWORK_TURQUOISE},
+ {"thecube", CONST_ME_THECUBE},
+ {"drawink", CONST_ME_DRAWINK},
+ {"prismaticsparkles", CONST_ME_PRISMATICSPARKLES},
+ {"thaian", CONST_ME_THAIAN},
+ {"thaianghost", CONST_ME_THAIANGHOST},
+ {"ghostsmoke", CONST_ME_GHOSTSMOKE},
+ {"floatingblock", CONST_ME_FLOATINGBLOCK},
+ {"block", CONST_ME_BLOCK},
+ {"rooting", CONST_ME_ROOTING},
+ {"sunpriest", CONST_ME_SUNPRIEST},
+ {"werelion", CONST_ME_WERELION},
+ {"ghostlyscratch", CONST_ME_GHOSTLYSCRATCH},
+ {"ghostlybite", CONST_ME_GHOSTLYBITE},
+ {"bigscratching", CONST_ME_BIGSCRATCHING},
+ {"slash", CONST_ME_SLASH},
+ {"bite", CONST_ME_BITE},
+ {"chivalriouschallenge", CONST_ME_CHIVALRIOUSCHALLENGE},
+ {"divinedazzle", CONST_ME_DIVINEDAZZLE},
+ {"electricalspark", CONST_ME_ELECTRICALSPARK},
+ {"purpleteleport", CONST_ME_PURPLETELEPORT},
+ {"redteleport", CONST_ME_REDTELEPORT},
+ {"orangeteleport", CONST_ME_ORANGETELEPORT},
+ {"greyteleport", CONST_ME_GREYTELEPORT},
+ {"lightblueteleport", CONST_ME_LIGHTBLUETELEPORT},
};
ShootTypeNames shootTypeNames = {
@@ -643,6 +691,10 @@ ShootTypeNames shootTypeNames = {
{"envenomedarrow", CONST_ANI_ENVENOMEDARROW},
{"gloothspear", CONST_ANI_GLOOTHSPEAR},
{"simplearrow", CONST_ANI_SIMPLEARROW},
+ {"leafstar", CONST_ANI_LEAFSTAR},
+ {"diamondarrow", CONST_ANI_DIAMONDARROW},
+ {"spectralbolt", CONST_ANI_SPECTRALBOLT},
+ {"royalstar", CONST_ANI_ROYALSTAR},
};
CombatTypeNames combatTypeNames = {
@@ -684,6 +736,10 @@ AmmoTypeNames ammoTypeNames = {
{"flammingarrow", AMMO_ARROW},
{"shiverarrow", AMMO_ARROW},
{"eartharrow", AMMO_ARROW},
+ {"leafstar", AMMO_THROWINGSTAR},
+ {"diamondarrow", AMMO_ARROW},
+ {"spectralbolt", AMMO_BOLT},
+ {"royalstar", AMMO_THROWINGSTAR},
};
WeaponActionNames weaponActionNames = {