Requires the mod-chromiecraft-smartstone module. Lets buyers gift name/faction/race/customize services as tokens.
+
+
+
+
+
diff --git a/src/acore-wp-plugin/src/Hooks/WooCommerce/CartValidation.php b/src/acore-wp-plugin/src/Hooks/WooCommerce/CartValidation.php
index 0f4dac2a..f55ddba5 100644
--- a/src/acore-wp-plugin/src/Hooks/WooCommerce/CartValidation.php
+++ b/src/acore-wp-plugin/src/Hooks/WooCommerce/CartValidation.php
@@ -63,6 +63,16 @@ public static function add_to_cart_validation($flaq, $product_id, $quantity, $va
return false;
}
+ // Gift via smartstone token: for giftable char-change services, a
+ // recipient name replaces the self character selection. Validate the
+ // recipient and skip the remaining self-service field checks.
+ if (CharChange::isGiftEligible($sku)) {
+ $destName = isset($_REQUEST['acore_char_dest']) ? trim((string) $_REQUEST['acore_char_dest']) : '';
+ if ($destName !== '') {
+ return CharChange::validateGiftRecipient($destName);
+ }
+ }
+
if (in_array('acore_char_sel', self::$skuList[$activeSku])) {
$guid = intval($_REQUEST['acore_char_sel'] ?? 0);
diff --git a/src/acore-wp-plugin/src/Hooks/WooCommerce/CharChange.php b/src/acore-wp-plugin/src/Hooks/WooCommerce/CharChange.php
index 859eec4f..8d34c659 100644
--- a/src/acore-wp-plugin/src/Hooks/WooCommerce/CharChange.php
+++ b/src/acore-wp-plugin/src/Hooks/WooCommerce/CharChange.php
@@ -3,6 +3,7 @@
namespace ACore\Hooks\WooCommerce;
use ACore\Manager\ACoreServices;
+use ACore\Manager\Opts;
class CharChange extends \ACore\Lib\WpClass {
@@ -15,6 +16,62 @@ class CharChange extends \ACore\Lib\WpClass {
"char-restore-delete"
);
+ /**
+ * SKUs that can be gifted through the smartstone token system, mapped to
+ * the mod's TokenType enum (src/Smartstone.h):
+ * 1 = Rename, 2 = Faction change, 3 = Race change, 4 = Customize.
+ * char-restore-delete has no token equivalent and is intentionally absent.
+ */
+ private static $tokenTypes = array(
+ "char-change-name" => 1,
+ "char-change-faction" => 2,
+ "char-change-race" => 3,
+ "char-change-customize" => 4,
+ );
+
+ private static function giftingEnabled(): bool {
+ return Opts::I()->acore_smartstone_enabled == '1';
+ }
+
+ /** A concrete service SKU the buyer may gift (smartstone must be enabled). */
+ public static function isGiftEligible($sku): bool {
+ return self::giftingEnabled() && isset(self::$tokenTypes[$sku]);
+ }
+
+ /**
+ * Whether to offer the gift field on the product page. Uses the display SKU,
+ * which for the variable "char-change" product is the parent (variations all
+ * map to giftable services), so the base SKU is accepted here too.
+ */
+ private static function isGiftableDisplaySku($sku): bool {
+ return self::giftingEnabled()
+ && ($sku === 'char-change' || isset(self::$tokenTypes[$sku]));
+ }
+
+ /**
+ * Validate a gift recipient (character name -> account). Adds a WooCommerce
+ * notice and returns false on any problem. Mirrors the smartstone
+ * account-wide gifting checks.
+ */
+ public static function validateGiftRecipient($destName): bool {
+ $ACoreSrv = ACoreServices::I();
+
+ $char = $ACoreSrv->getCharactersRepo()->findOneByName($destName);
+ if (!$char) {
+ \wc_add_notice(__('Recipient character not found.', 'acore-wp-plugin'), 'error');
+ return false;
+ }
+ if ($ACoreSrv->getCharactersBannedRepo()->isActiveByGuid($char->getGuid())) {
+ \wc_add_notice(__('Recipient character is banned.', 'acore-wp-plugin'), 'error');
+ return false;
+ }
+ if ($ACoreSrv->getAccountBannedRepo()->isActiveById($char->getAccount())) {
+ \wc_add_notice(__('Recipient account is banned.', 'acore-wp-plugin'), 'error');
+ return false;
+ }
+ return true;
+ }
+
public static function init() {
add_action('woocommerce_after_add_to_cart_quantity', self::sprefix() . 'before_add_to_cart_button');
add_filter('woocommerce_add_cart_item_data', self::sprefix() . 'add_cart_item_data', 20, 3);
@@ -36,6 +93,12 @@ public static function before_add_to_cart_button() {
if ($current_user) {
FieldElements::charList($current_user->user_login, self::isDeletedSKU($product->get_sku()));
}
+
+ // Optional gifting via smartstone token: leaving the field blank keeps
+ // the normal self-service (instant apply to the selected character).
+ if (self::isGiftableDisplaySku($product->get_sku())) {
+ FieldElements::destCharacter(__('Gift to a character (optional, leave blank to apply to your own selected character):', 'acore-wp-plugin'));
+ }
}
// 3) SAVE INTO ITEM DATA
@@ -43,7 +106,30 @@ public static function before_add_to_cart_button() {
// ( each cart item has their own data )
public static function add_cart_item_data($cart_item_data, $product_id, $variation_id) {
$product = $variation_id ? \wc_get_product($variation_id) : \wc_get_product($product_id);
- if (!in_array($product->get_sku(), self::$skuList)) {
+ $sku = $product->get_sku();
+ if (!in_array($sku, self::$skuList)) {
+ return $cart_item_data;
+ }
+
+ // Gift mode: a recipient name was supplied for a giftable service. Resolve
+ // it to an account now so payment_complete can grant the token there.
+ // add_to_cart_validation has already vetted existence/ban state.
+ $destName = self::isGiftEligible($sku) && isset($_REQUEST['acore_char_dest'])
+ ? trim((string) $_REQUEST['acore_char_dest'])
+ : '';
+ if ($destName !== '') {
+ $cart_item_data['acore_item_sku'] = $sku;
+ $cart_item_data['unique_key'] = md5(microtime() . rand());
+
+ $ACoreSrv = ACoreServices::I();
+ $char = $ACoreSrv->getCharactersRepo()->findOneByName($destName);
+ if ($char) {
+ $account = $ACoreSrv->getAccountRepo()->findOneById($char->getAccount());
+ if ($account) {
+ $cart_item_data['acore_gift_account'] = $account->getUsername();
+ $cart_item_data['acore_gift_charname'] = $char->getName();
+ }
+ }
return $cart_item_data;
}
@@ -71,6 +157,14 @@ public static function get_item_data($cart_data, $cart_item = null) {
return $custom_items;
}
+ if (isset($cart_item['acore_gift_account'])) {
+ $giftCharname = isset($cart_item['acore_gift_charname'])
+ ? $cart_item['acore_gift_charname']
+ : '(recipient)';
+ $custom_items[] = array("name" => 'Gift for', "value" => $giftCharname);
+ return $custom_items;
+ }
+
if (isset($cart_item['acore_char_sel'])) {
$ACoreSrv = ACoreServices::I();
$charRepo = $ACoreSrv->getCharactersRepo();
@@ -95,6 +189,19 @@ public static function add_order_item_meta($item_id, $values, $cart_item_key) {
return;
}
+ // Gift: persist the resolved recipient so payment_complete can grant the
+ // token. The account login is stored under an underscore-prefixed key so
+ // WooCommerce keeps it out of customer-facing order details and emails;
+ // only the character name is shown to the buyer. Charname is informational.
+ if (isset($values['acore_gift_account'])) {
+ wc_add_order_item_meta($item_id, "acore_item_sku", $values['acore_item_sku']);
+ wc_add_order_item_meta($item_id, "_acore_gift_account", $values['acore_gift_account']);
+ if (isset($values['acore_gift_charname'])) {
+ wc_add_order_item_meta($item_id, "acore_gift_charname", $values['acore_gift_charname']);
+ }
+ return;
+ }
+
if (isset($values['acore_char_sel']) && isset($values["acore_item_sku"])) {
wc_add_order_item_meta($item_id, "acore_char_sel", $values['acore_char_sel']);
wc_add_order_item_meta($item_id, "acore_item_sku", $values['acore_item_sku']);
@@ -135,6 +242,18 @@ public static function payment_complete($order_id) {
foreach ($items as $item) {
if (isset($item["acore_item_sku"])) {
+ // Gift: grant a token to the recipient's account instead of
+ // applying the change directly. The recipient redeems it in-game.
+ if (!empty($item["_acore_gift_account"])
+ && isset(self::$tokenTypes[$item["acore_item_sku"]])) {
+ $tokenType = self::$tokenTypes[$item["acore_item_sku"]];
+ $res = $WoWSrv->getSmartstoneSoap()->grantToken($item["_acore_gift_account"], $tokenType);
+ if ($res instanceof \Exception) {
+ throw new \Exception("There was an error granting a token to " . $item["_acore_gift_account"] . " - " . $res->getMessage());
+ }
+ continue;
+ }
+
switch ($item["acore_item_sku"]) {
case "char-change-name":
$charName = $WoWSrv->getCharName($item["acore_char_sel"]);
diff --git a/src/acore-wp-plugin/src/Hooks/WooCommerce/Smartstone.php b/src/acore-wp-plugin/src/Hooks/WooCommerce/Smartstone.php
index 7f2ef279..daefee5a 100644
--- a/src/acore-wp-plugin/src/Hooks/WooCommerce/Smartstone.php
+++ b/src/acore-wp-plugin/src/Hooks/WooCommerce/Smartstone.php
@@ -279,10 +279,11 @@ public static function add_order_item_meta($item_id, $values, $cart_item_key) {
}
// Gifting: persist the resolved recipient so payment_complete can route
- // the SOAP unlock to their account. Charname is informational (for admin
- // order view); only acore_gift_account is functional.
+ // the SOAP unlock to their account. The account login is stored under an
+ // underscore-prefixed key so WooCommerce keeps it out of customer-facing
+ // order details and emails; only the character name is shown to the buyer.
if (self::isAccountWide($smartstone_category) && isset($values['acore_gift_account'])) {
- \wc_add_order_item_meta($item_id, "acore_gift_account", $values['acore_gift_account']);
+ \wc_add_order_item_meta($item_id, "_acore_gift_account", $values['acore_gift_account']);
if (isset($values['acore_gift_charname'])) {
\wc_add_order_item_meta($item_id, "acore_gift_charname", $values['acore_gift_charname']);
}
@@ -340,8 +341,8 @@ public static function payment_complete($order_id) {
if (self::isAccountWide($smartstone_category)) {
// Gifted items route to the recipient's account; otherwise unlock
// for the buyer. Resolution + ban-check happened at add-to-cart time.
- $target = !empty($item['acore_gift_account'])
- ? $item['acore_gift_account']
+ $target = !empty($item['_acore_gift_account'])
+ ? $item['_acore_gift_account']
: $accountName;
if (!$target) {
throw new \Exception("Smartstone account-wide unlock requires a buyer with a linked AC account; order $order_id has none.");
diff --git a/src/acore-wp-plugin/src/Manager/Opts.php b/src/acore-wp-plugin/src/Manager/Opts.php
index d9488021..e853f13c 100644
--- a/src/acore-wp-plugin/src/Manager/Opts.php
+++ b/src/acore-wp-plugin/src/Manager/Opts.php
@@ -40,6 +40,7 @@ class Opts {
public $acore_resurrection_scroll="";
public $acore_resurrection_scroll_days_inactive="180";
public $acore_item_restoration="";
+ public $acore_smartstone_enabled="";
public $acore_name_unlock_thresholds = [
[5, 30], // level < 5 -> 30 days
[30, 90], // level < 30 -> 90 days
diff --git a/src/acore-wp-plugin/src/Manager/Soap/SmartstoneService.php b/src/acore-wp-plugin/src/Manager/Soap/SmartstoneService.php
index 79b89e0a..1bc2f8bc 100644
--- a/src/acore-wp-plugin/src/Manager/Soap/SmartstoneService.php
+++ b/src/acore-wp-plugin/src/Manager/Soap/SmartstoneService.php
@@ -27,4 +27,19 @@ public function addAccountVanity($accountName, $category, $vanityID) {
return $this->executeCommand(".smartstone unlock account $accountName $category $vanityID true");
}
+ /**
+ * Grant a service token to an account. The recipient redeems it in-game
+ * (`.smartstone token claim`) on the character of their choice.
+ *
+ * $tokenType values come from the mod's TokenType enum (src/Smartstone.h):
+ * 1 = Rename, 2 = Faction change, 3 = Race change, 4 = Customize.
+ *
+ * $accountName is a WordPress user_login (sanitize_user restricts it to
+ * alphanumerics, dot, hyphen, underscore, at - no spaces).
+ */
+ public function grantToken($accountName, $tokenType) {
+ $tokenType = (int) $tokenType;
+ return $this->executeCommand(".smartstone token grant $accountName $tokenType");
+ }
+
}
\ No newline at end of file