Compare commits
3 Commits
solo-lfg
...
6f075133f7
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f075133f7 | |||
| d9f0d414fc | |||
| 9b0e732fb8 |
+169
@@ -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"
|
||||
-1715993
File diff suppressed because it is too large
Load Diff
@@ -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)"
|
||||
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user