From a43dd44ab9cc4cb177f608139e8b4c8c8c4f1fda Mon Sep 17 00:00:00 2001 From: Skjalf <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 18 Jul 2026 21:17:09 -0300 Subject: [PATCH] feat(smartstone): gift character services via tokens Add optional gifting for the name, faction, race and customization character services using the mod-chromiecraft-smartstone token system. When a buyer enters a recipient character name, the purchase grants a token to that account (.smartstone token grant) instead of applying the change directly, and the recipient redeems it in-game. Leaving the field blank keeps the existing self-service (instant apply) behaviour. Gated behind a new "Smartstone Token Gifting" toggle (acore_smartstone_enabled) on the admin Tools page, disabled by default, so nothing changes unless the module is in use. Token types map 1:1 to the services (rename, faction, race, customize); char-restore-delete has no token equivalent and is excluded. The resolved recipient account login is stored under a hidden _acore_gift_account order-item meta key so WooCommerce keeps it out of customer order details and emails; only the character name is shown. The same fix is applied to the existing account-wide smartstone gifting path, which shared the key. Co-Authored-By: Claude Opus 4.8 --- .../src/Components/AdminPanel/Pages/Tools.php | 12 ++ .../src/Hooks/WooCommerce/CartValidation.php | 10 ++ .../src/Hooks/WooCommerce/CharChange.php | 121 +++++++++++++++++- .../src/Hooks/WooCommerce/Smartstone.php | 11 +- src/acore-wp-plugin/src/Manager/Opts.php | 1 + .../src/Manager/Soap/SmartstoneService.php | 15 +++ 6 files changed, 164 insertions(+), 6 deletions(-) diff --git a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php index 86058a20..7fe8a16f 100644 --- a/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php +++ b/src/acore-wp-plugin/src/Components/AdminPanel/Pages/Tools.php @@ -61,6 +61,18 @@ + + + +

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