скрытие магазина и механизм удаления гильдий

This commit is contained in:
2026-07-28 14:46:19 +04:00
parent d9f0d414fc
commit 6f075133f7
6 changed files with 267 additions and 1716001 deletions
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${ENV_FILE:-$ROOT_DIR/.env}"
GUILD_NAME=""
ASSUME_YES=0
if [[ -f "$ENV_FILE" ]]; then
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
fi
usage() {
cat <<EOF
Usage: $(basename "$0") <guild name> [--yes]
Permanently deletes a guild and its related character-database records.
The worldserver must be stopped before running this script.
Options:
--yes Skip the interactive confirmation
-h, --help Show this help
Examples:
docker compose stop ac-worldserver
./delete-guild.sh "Название гильдии"
./delete-guild.sh "Название гильдии" --yes
docker compose start ac-worldserver
EOF
}
log() {
printf '[delete-guild] %s\n' "$*"
}
while (($#)); do
case "$1" in
--yes)
ASSUME_YES=1
shift
;;
-h|--help)
usage
exit 0
;;
-*)
printf 'Unknown option: %s\n\n' "$1" >&2
usage >&2
exit 1
;;
*)
if [[ -n "$GUILD_NAME" ]]; then
printf 'Only one guild name may be specified\n\n' >&2
usage >&2
exit 1
fi
GUILD_NAME="$1"
shift
;;
esac
done
if [[ -z "$GUILD_NAME" ]]; then
usage >&2
exit 1
fi
if [[ "$GUILD_NAME" == *$'\n'* || "$GUILD_NAME" == *$'\r'* || "$GUILD_NAME" == *$'\t'* ]]; then
printf 'Guild name must not contain control characters\n' >&2
exit 1
fi
cd "$ROOT_DIR"
RUNNING_SERVICES="$(docker compose ps --status running --services)"
if grep -qx 'ac-worldserver' <<<"$RUNNING_SERVICES"; then
printf 'ac-worldserver is running. Stop it first:\n docker compose stop ac-worldserver\n' >&2
exit 1
fi
if ! grep -qx 'ac-database' <<<"$RUNNING_SERVICES"; then
printf 'ac-database is not running. Start it first:\n docker compose up -d ac-database\n' >&2
exit 1
fi
export ACORE_DELETE_GUILD_NAME="$GUILD_NAME"
SQL_GUILD_NAME="$(python3 <<'PY'
import os
value = os.environ["ACORE_DELETE_GUILD_NAME"]
print("'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'")
PY
)"
GUILD_ROW="$(
docker compose exec -T ac-database bash -lc \
'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --batch --skip-column-names acore_characters' \
<<<"SELECT guildid, name, leaderguid FROM guild WHERE name = ${SQL_GUILD_NAME} LIMIT 1;"
)"
if [[ -z "$GUILD_ROW" ]]; then
printf 'Guild not found: %s\n' "$GUILD_NAME" >&2
exit 1
fi
IFS=$'\t' read -r GUILD_ID FOUND_NAME LEADER_GUID <<<"$GUILD_ROW"
log "found guild id=${GUILD_ID}, name=${FOUND_NAME}, leader=${LEADER_GUID}"
if ((ASSUME_YES == 0)); then
printf 'This permanently deletes the guild and all items in its bank.\n'
printf 'Type the guild name to confirm: '
read -r CONFIRMATION
if [[ "$CONFIRMATION" != "$FOUND_NAME" ]]; then
printf 'Confirmation did not match; nothing was deleted.\n' >&2
exit 1
fi
fi
SQL=$(cat <<EOF
START TRANSACTION;
SET @guild_id := ${GUILD_ID};
CREATE TEMPORARY TABLE delete_guild_members (
guid INT UNSIGNED NOT NULL PRIMARY KEY
) ENGINE=MEMORY
SELECT guid FROM guild_member WHERE guildid = @guild_id;
CREATE TEMPORARY TABLE delete_guild_bank_items (
item_guid INT UNSIGNED NOT NULL PRIMARY KEY
) ENGINE=MEMORY
SELECT item_guid FROM guild_bank_item WHERE guildid = @guild_id;
DELETE item_instance
FROM item_instance
INNER JOIN delete_guild_bank_items
ON delete_guild_bank_items.item_guid = item_instance.guid;
DELETE guild_member_withdraw
FROM guild_member_withdraw
INNER JOIN delete_guild_members
ON delete_guild_members.guid = guild_member_withdraw.guid;
DELETE FROM guild_bank_eventlog WHERE guildid = @guild_id;
DELETE FROM guild_eventlog WHERE guildid = @guild_id;
DELETE FROM guild_bank_right WHERE guildid = @guild_id;
DELETE FROM guild_bank_item WHERE guildid = @guild_id;
DELETE FROM guild_bank_tab WHERE guildid = @guild_id;
DELETE FROM guild_rank WHERE guildid = @guild_id;
DELETE FROM guild_member WHERE guildid = @guild_id;
DELETE FROM guild WHERE guildid = @guild_id;
SET @deleted := ROW_COUNT();
COMMIT;
SELECT @guild_id AS guildid, @deleted AS deleted;
EOF
)
log "deleting guild ${FOUND_NAME} (${GUILD_ID})"
docker compose exec -T ac-database bash -lc \
'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --show-warnings acore_characters' <<<"$SQL"
log "guild deleted"
File diff suppressed because it is too large Load Diff
+17 -5
View File
@@ -184,6 +184,23 @@ import_store_sql() {
return 0 return 0
fi fi
if ! database_exists "store"; then
log "creating database store"
mysql_exec "" "CREATE DATABASE IF NOT EXISTS \`store\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
fi
# Unlike the rest of Store.sql, this is a repeatable migration. It ensures
# existing store databases receive the global switch without overwriting a
# value later changed by an administrator.
mysql_exec "store" "
CREATE TABLE IF NOT EXISTS \`store_config\` (
\`id\` TINYINT UNSIGNED NOT NULL,
\`enabled\` TINYINT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT IGNORE INTO \`store_config\` (\`id\`, \`enabled\`) VALUES (1, 0);
"
# Store.sql is a one-time bootstrap, not a repeatable migration. It # Store.sql is a one-time bootstrap, not a repeatable migration. It
# contains UPDATE and DELETE statements that would overwrite values # contains UPDATE and DELETE statements that would overwrite values
# maintained in the live store database. The canonical services table is # maintained in the live store database. The canonical services table is
@@ -194,11 +211,6 @@ import_store_sql() {
return 0 return 0
fi fi
if ! database_exists "store"; then
log "creating database store"
mysql_exec "" "CREATE DATABASE IF NOT EXISTS \`store\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
fi
temp_sql="$(mktemp)" temp_sql="$(mktemp)"
{ {
+42 -1
View File
@@ -87,7 +87,14 @@ end
local StoreHandler = AIO.AddHandlers("STORE_CLIENT", {}) local StoreHandler = AIO.AddHandlers("STORE_CLIENT", {})
function StoreHandler.FrameData(player, services, links, nav, currencies, rank) function StoreHandler.FrameData(player, storeEnabled, services, links, nav, currencies, rank)
SHOP_UI["Vars"].storeEnabled = storeEnabled == 1
SHOP_UI.StoreAvailability_Update()
if not SHOP_UI["Vars"].storeEnabled then
return
end
SHOP_UI["Data"].services = services SHOP_UI["Data"].services = services
SHOP_UI["Data"].links = links SHOP_UI["Data"].links = links
SHOP_UI["Data"].nav = nav SHOP_UI["Data"].nav = nav
@@ -113,6 +120,7 @@ SHOP_UI = {
currentPage = 1, currentPage = 1,
maxPages = 1, maxPages = 1,
accountRank = 0, accountRank = 0,
storeEnabled = false,
["playerCurrencies"] = {} ["playerCurrencies"] = {}
}, },
["Data"] = { ["Data"] = {
@@ -146,6 +154,12 @@ function SHOP_UI.MainFrame_Create()
shopFrame.Title:SetShadowOffset(1, -1) shopFrame.Title:SetShadowOffset(1, -1)
shopFrame.Title:SetPoint("TOP", shopFrame, "TOP", 0, -3) shopFrame.Title:SetPoint("TOP", shopFrame, "TOP", 0, -3)
shopFrame.Title:SetText("|cffedd100Лавка Лунной Жрицы|r") shopFrame.Title:SetText("|cffedd100Лавка Лунной Жрицы|r")
shopFrame.UnavailableText = shopFrame:CreateFontString()
shopFrame.UnavailableText:SetFont("Fonts\\FRIZQT__.TTF", 24, "OUTLINE")
shopFrame.UnavailableText:SetShadowOffset(1, -1)
shopFrame.UnavailableText:SetPoint("CENTER", shopFrame, "CENTER", 85, 0)
shopFrame.UnavailableText:SetText("|cffedd100Извините, магазин ещё не работает|r")
-- create navigation button placeholders, pass parent as arg -- create navigation button placeholders, pass parent as arg
SHOP_UI.NavButtons_Create(shopFrame) SHOP_UI.NavButtons_Create(shopFrame)
@@ -189,6 +203,7 @@ function SHOP_UI.MainFrame_Create()
shopFrame:SetScript( shopFrame:SetScript(
"OnShow", "OnShow",
function() function()
AIO.Handle("STORE_SERVER", "FrameData")
AIO.Handle("STORE_SERVER", "UpdateCurrencies") AIO.Handle("STORE_SERVER", "UpdateCurrencies")
PlaySound("AuctionWindowOpen", "Master") PlaySound("AuctionWindowOpen", "Master")
end end
@@ -207,6 +222,32 @@ function SHOP_UI.MainFrame_Create()
tinsert(UISpecialFrames, shopFrame:GetName()) tinsert(UISpecialFrames, shopFrame:GetName())
SHOP_UI["FRAME"] = shopFrame SHOP_UI["FRAME"] = shopFrame
SHOP_UI.StoreAvailability_Update()
end
function SHOP_UI.StoreAvailability_Update()
local enabled = SHOP_UI["Vars"].storeEnabled
if SHOP_UI["FRAME"] and SHOP_UI["FRAME"].UnavailableText then
if enabled then
SHOP_UI["FRAME"].UnavailableText:Hide()
else
SHOP_UI["FRAME"].UnavailableText:Show()
end
end
for _, collectionName in pairs({"NAV_BUTTONS", "SERVICE_BUTTONS", "PAGING_ELEMENTS", "CURRENCY_BUTTONS"}) do
local collection = SHOP_UI[collectionName]
if collection and not enabled then
for _, element in pairs(collection) do
element:Hide()
end
end
end
if SHOP_UI["MODEL_FRAME"] and not enabled then
SHOP_UI["MODEL_FRAME"]:Hide()
end
end end
-- create navigation button placeholders -- create navigation button placeholders
+28 -2
View File
@@ -15,7 +15,8 @@ local CONFIG = {
tooHighLevel = "Уровень персонажа слишком высок", tooHighLevel = "Уровень персонажа слишком высок",
mailBody = "Благодарим за покупку в Лавке Лунной Жрицы!", mailBody = "Благодарим за покупку в Лавке Лунной Жрицы!",
-- The service name is prefixed to this message -- The service name is prefixed to this message
successfulPurchase = "Покупка успешно совершена!" successfulPurchase = "Покупка успешно совершена!",
storeUnavailable = "Магазин пока не работает"
} }
} }
@@ -60,11 +61,31 @@ local KEYS = GetDataStructKeys();
local StoreHandler = AIO.AddHandlers("STORE_SERVER", {}) local StoreHandler = AIO.AddHandlers("STORE_SERVER", {})
local function IsStoreEnabled()
local query = WorldDBQuery("SELECT `enabled` FROM `store`.`store_config` WHERE `id` = 1 LIMIT 1;")
return query and query:GetUInt32(0) == 1
end
function StoreHandler.FrameData(player) function StoreHandler.FrameData(player)
AIO.Handle(player, "STORE_CLIENT", "FrameData", GetServiceData(), GetLinkData(), GetNavData(), GetCurrencyData(), player:GetGMRank()) local enabled = IsStoreEnabled()
AIO.Handle(
player,
"STORE_CLIENT",
"FrameData",
enabled and 1 or 0,
enabled and GetServiceData() or {},
enabled and GetLinkData() or {},
enabled and GetNavData() or {},
enabled and GetCurrencyData() or {},
player:GetGMRank()
)
end end
function StoreHandler.UpdateCurrencies(player) function StoreHandler.UpdateCurrencies(player)
if not IsStoreEnabled() then
return
end
local tmp = {} local tmp = {}
for currencyId, currency in pairs(GetCurrencyData()) do for currencyId, currency in pairs(GetCurrencyData()) do
local val = 0 local val = 0
@@ -87,6 +108,11 @@ function StoreHandler.UpdateCurrencies(player)
end end
function StoreHandler.Purchase(player, serviceId) function StoreHandler.Purchase(player, serviceId)
if not IsStoreEnabled() then
player:SendAreaTriggerMessage(CONFIG.strings.storeUnavailable)
return
end
local services = GetServiceData() local services = GetServiceData()
-- See if the requested service exists -- See if the requested service exists
+11
View File
@@ -14,6 +14,17 @@
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-- Global store switch. Keep the initial value disabled until the storefront is
-- ready. Existing installations preserve their current value on re-import.
CREATE TABLE IF NOT EXISTS `store_config` (
`id` tinyint unsigned NOT NULL,
`enabled` tinyint unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `store_config` (`id`, `enabled`) VALUES (1, 0)
ON DUPLICATE KEY UPDATE `id` = VALUES(`id`);
-- Dumping structure for table store.store_categories -- Dumping structure for table store.store_categories
CREATE TABLE IF NOT EXISTS `store_categories` ( CREATE TABLE IF NOT EXISTS `store_categories` (
`id` int unsigned NOT NULL AUTO_INCREMENT, `id` int unsigned NOT NULL AUTO_INCREMENT,