3 Commits

7 changed files with 307 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
+22
View File
@@ -189,6 +189,28 @@ import_store_sql() {
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
# contains UPDATE and DELETE statements that would overwrite values
# maintained in the live store database. The canonical services table is
# created by the initial bootstrap, so its presence also protects
# installations created before this guard was introduced.
if database_exists "store" && table_exists "store" "store_services"; then
log "store database already initialized, skipping one-time Store.sql bootstrap"
return 0
fi
temp_sql="$(mktemp)"
{
+52 -5
View File
@@ -64,7 +64,8 @@ local KEYS = {
rewardCount_7 = 27,
rewardCount_8 = 28,
new = 29,
enabled = 30
enabled = 30,
previewDisplayId = 31
},
}
@@ -86,7 +87,14 @@ end
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"].links = links
SHOP_UI["Data"].nav = nav
@@ -112,6 +120,7 @@ SHOP_UI = {
currentPage = 1,
maxPages = 1,
accountRank = 0,
storeEnabled = false,
["playerCurrencies"] = {}
},
["Data"] = {
@@ -145,6 +154,12 @@ function SHOP_UI.MainFrame_Create()
shopFrame.Title:SetShadowOffset(1, -1)
shopFrame.Title:SetPoint("TOP", shopFrame, "TOP", 0, -3)
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
SHOP_UI.NavButtons_Create(shopFrame)
@@ -188,6 +203,7 @@ function SHOP_UI.MainFrame_Create()
shopFrame:SetScript(
"OnShow",
function()
AIO.Handle("STORE_SERVER", "FrameData")
AIO.Handle("STORE_SERVER", "UpdateCurrencies")
PlaySound("AuctionWindowOpen", "Master")
end
@@ -206,6 +222,32 @@ function SHOP_UI.MainFrame_Create()
tinsert(UISpecialFrames, shopFrame:GetName())
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
-- create navigation button placeholders
@@ -503,7 +545,7 @@ function SHOP_UI.ServiceBoxes_Create(parent)
if(self.Type == 1 and service.Flags == 1) then
SHOP_UI.ModelFrame_ShowPlayer(self.Rewards)
elseif((self.Type == 3 or self.Type == 4) and self.DisplayId > 0) then -- Handler for creatures with models
SHOP_UI.ModelFrame_ShowCreature(self.DisplayId)
SHOP_UI.ModelFrame_ShowCreature(self.DisplayId, self.PreviewDisplayId)
end
-- Parchment page sound
PlaySound(836)
@@ -614,6 +656,7 @@ function SHOP_UI.ServiceBoxes_Update()
service.Currency = serviceData[KEYS.service.currency]
service.TooltipHyperlink = serviceData[KEYS.service.hyperlink]
service.DisplayId = serviceData[KEYS.service.displayId]
service.PreviewDisplayId = serviceData[KEYS.service.previewDisplayId] or 0
service.Discount = serviceData[KEYS.service.discount]
service.Flags = serviceData[KEYS.service.flags]
service.New = serviceData[KEYS.service.new]
@@ -1150,7 +1193,7 @@ function SHOP_UI.ModelFrame_ShowPlayer(displayId)
PlaySound("INTERFACESOUND_GAMESCROLLBUTTON", "Master")
end
function SHOP_UI.ModelFrame_ShowCreature(displayId)
function SHOP_UI.ModelFrame_ShowCreature(creatureEntry, previewDisplayId)
-- hacky ass model frame handling
-- hide model frames
if(SHOP_UI["MODEL_FRAME"].playerModel:IsShown()) then
@@ -1161,7 +1204,11 @@ function SHOP_UI.ModelFrame_ShowCreature(displayId)
SHOP_UI["MODEL_FRAME"]:Hide()
-- set the correct unit and show frame
SHOP_UI["MODEL_FRAME"].creatureModel:SetCreature(displayId)
if(previewDisplayId and previewDisplayId > 0) then
SHOP_UI["MODEL_FRAME"].creatureModel:SetCreature(creatureEntry, previewDisplayId)
else
SHOP_UI["MODEL_FRAME"].creatureModel:SetCreature(creatureEntry)
end
SHOP_UI["MODEL_FRAME"].creatureModel:Show()
SHOP_UI["MODEL_FRAME"]:Show()
+25 -1
View File
@@ -59,7 +59,8 @@ local KEYS = {
rewardCount_7 = 27,
rewardCount_8 = 28,
new = 29,
enabled = 30
enabled = 30,
previewDisplayId = 31
},
}
@@ -139,6 +140,8 @@ function ServiceData.Load()
Query:GetUInt32(KEYS.service.rewardCount_7),
Query:GetUInt32(KEYS.service.rewardCount_8),
Query:GetUInt32(KEYS.service.new),
Query:GetUInt32(KEYS.service.enabled),
0,
}
end
until not Query:NextRow()
@@ -224,6 +227,27 @@ function CreatureDisplays.Load()
until not ModelQuery:NextRow()
end
end
-- Keep the creature entry for the stock preview path, but also send its
-- primary CreatureDisplayInfo ID. WarcraftXL can apply that display
-- directly, avoiding stale or missing client creature-cache records.
for _, service in pairs(ServiceData.Cache) do
local serviceType = service[KEYS.service.serviceType]
local entry = service[KEYS.service.displayOrEntry]
if ((serviceType == 3 or serviceType == 4) and entry > 0) then
local creature = CreatureDisplays.Cache[entry]
local displayId = 0
if (creature) then
for modelIndex = 11, 14 do
if (creature[modelIndex] and creature[modelIndex] > 0) then
displayId = creature[modelIndex]
break
end
end
end
service[KEYS.service.previewDisplayId] = displayId
end
end
end
function LinkData.Load()
+28 -2
View File
@@ -15,7 +15,8 @@ local CONFIG = {
tooHighLevel = "Уровень персонажа слишком высок",
mailBody = "Благодарим за покупку в Лавке Лунной Жрицы!",
-- 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 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)
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
function StoreHandler.UpdateCurrencies(player)
if not IsStoreEnabled() then
return
end
local tmp = {}
for currencyId, currency in pairs(GetCurrencyData()) do
local val = 0
@@ -87,6 +108,11 @@ function StoreHandler.UpdateCurrencies(player)
end
function StoreHandler.Purchase(player, serviceId)
if not IsStoreEnabled() then
player:SendAreaTriggerMessage(CONFIG.strings.storeUnavailable)
return
end
local services = GetServiceData()
-- 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' */;
/*!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
CREATE TABLE IF NOT EXISTS `store_categories` (
`id` int unsigned NOT NULL AUTO_INCREMENT,