PVP Matches Design
thats the draft for example db designs for pvp matchesunknown
sql
10 months ago
31 kB
18
Indexable
/* ============================================================================
PVP WAGERED ARENA — FULL DB SCHEMA (SQL Server)
Author: Navixo Ops
============================================================================ */
SET NOCOUNT ON;
GO
/* ==============================
0) SAFETY (idempotency)
============================== */
IF OBJECT_ID('dbo.v_pvp_browse') IS NOT NULL DROP VIEW dbo.v_pvp_browse;
IF OBJECT_ID('dbo.v_pvp_pending_requests') IS NOT NULL DROP VIEW dbo.v_pvp_pending_requests;
IF OBJECT_ID('dbo.v_pvp_live_score') IS NOT NULL DROP VIEW dbo.v_pvp_live_score;
GO
-- Drop procedures if exist (minimal list)
DECLARE @procs TABLE(name sysname);
INSERT INTO @procs(name) VALUES
('usp_pvp_create_offer'),('usp_pvp_cancel_offer'),('usp_pvp_join_request'),
('usp_pvp_approve_request'),('usp_pvp_allocate_world'),('usp_pvp_start_match'),
('usp_pvp_record_kill'),('usp_pvp_finish_match'),('usp_pvp_world_free'),
('usp_pvp_add_spectator'),('usp_pvp_remove_spectator');
DECLARE @n sysname;
WHILE EXISTS(SELECT 1 FROM @procs)
BEGIN
SELECT TOP(1) @n=name FROM @procs;
IF OBJECT_ID('dbo.'+@n) IS NOT NULL EXEC('DROP PROCEDURE dbo.'+@n);
DELETE FROM @procs WHERE name=@n;
END
GO
/* ==============================
1) LOOKUPS
============================== */
IF OBJECT_ID('dbo.currency_lu') IS NOT NULL DROP TABLE dbo.currency_lu;
CREATE TABLE dbo.currency_lu (
currency_id TINYINT NOT NULL PRIMARY KEY, -- 1=Silks, 2=EP
code VARCHAR(12) NOT NULL UNIQUE
);
INSERT INTO dbo.currency_lu(currency_id, code) VALUES (1,'SILKS'),(2,'EP');
IF OBJECT_ID('dbo.offer_status_lu') IS NOT NULL DROP TABLE dbo.offer_status_lu;
CREATE TABLE dbo.offer_status_lu (
status_id TINYINT PRIMARY KEY, -- 1=OPEN,2=LOCKED,3=CANCELED,4=CONSUMED
name VARCHAR(20) UNIQUE
);
INSERT INTO dbo.offer_status_lu VALUES (1,'OPEN'),(2,'LOCKED'),(3,'CANCELED'),(4,'CONSUMED');
IF OBJECT_ID('dbo.join_status_lu') IS NOT NULL DROP TABLE dbo.join_status_lu;
CREATE TABLE dbo.join_status_lu (
status_id TINYINT PRIMARY KEY, -- 1=PENDING,2=ACCEPTED,3=DECLINED,4=EXPIRED,5=CANCELED
name VARCHAR(20) UNIQUE
);
INSERT INTO dbo.join_status_lu VALUES
(1,'PENDING'),(2,'ACCEPTED'),(3,'DECLINED'),(4,'EXPIRED'),(5,'CANCELED');
IF OBJECT_ID('dbo.match_state_lu') IS NOT NULL DROP TABLE dbo.match_state_lu;
CREATE TABLE dbo.match_state_lu (
state_id TINYINT PRIMARY KEY, -- 1=PENDING_CONFIRM,2=QUEUED,3=ALLOCATING,4=WARMUP,5=LIVE,6=OVERTIME,7=ENDED,8=CANCELED,9=ERROR
name VARCHAR(24) UNIQUE
);
INSERT INTO dbo.match_state_lu VALUES
(1,'PENDING_CONFIRM'),(2,'QUEUED'),(3,'ALLOCATING'),(4,'WARMUP'),
(5,'LIVE'),(6,'OVERTIME'),(7,'ENDED'),(8,'CANCELED'),(9,'ERROR');
IF OBJECT_ID('dbo.match_result_reason_lu') IS NOT NULL DROP TABLE dbo.match_result_reason_lu;
CREATE TABLE dbo.match_result_reason_lu (
reason_id TINYINT PRIMARY KEY, -- 1=TIME,2=TARGET_REACHED,3=SURRENDER,4=DC_FORFEIT,5=ERROR,6=CANCEL
name VARCHAR(24) UNIQUE
);
INSERT INTO dbo.match_result_reason_lu VALUES
(1,'TIME'),(2,'TARGET_REACHED'),(3,'SURRENDER'),(4,'DC_FORFEIT'),(5,'ERROR'),(6,'CANCEL');
IF OBJECT_ID('dbo.participant_role_lu') IS NOT NULL DROP TABLE dbo.participant_role_lu;
CREATE TABLE dbo.participant_role_lu (
role_id TINYINT PRIMARY KEY, -- 1=FIGHTER_A,2=FIGHTER_B
name VARCHAR(20) UNIQUE
);
INSERT INTO dbo.participant_role_lu VALUES (1,'FIGHTER_A'),(2,'FIGHTER_B');
-- Optional rules bitmask names
IF OBJECT_ID('dbo.rules_flag_lu') IS NOT NULL DROP TABLE dbo.rules_flag_lu;
CREATE TABLE dbo.rules_flag_lu (
bit_index TINYINT PRIMARY KEY, -- 0..31
name VARCHAR(40) UNIQUE,
description VARCHAR(200) NULL
);
-- Seed a few common toggles
INSERT INTO dbo.rules_flag_lu(bit_index,name,description) VALUES
(0,'ALLOW_ZERK','Allow berserk'),
(1,'NO_PETS','Disallow pets & summons'),
(2,'NO_JOB_CAPES','Disallow job capes'),
(3,'NO_POTS','Disallow potions'),
(4,'NORMALIZE_STATS','Normalize damage/defense'),
(5,'DISABLE_REZ','Disable resurrect/recall');
/* ==============================
2) CORE TABLES
============================== */
IF OBJECT_ID('dbo.pvp_offers') IS NOT NULL DROP TABLE dbo.pvp_offers;
CREATE TABLE dbo.pvp_offers (
challenge_id BIGINT IDENTITY(1,1) PRIMARY KEY,
creator_char_id BIGINT NOT NULL,
min_level SMALLINT NULL,
currency_id TINYINT NOT NULL REFERENCES dbo.currency_lu(currency_id),
stake_amount INT NOT NULL CHECK (stake_amount > 0),
mode TINYINT NOT NULL, -- 1=MOST_KILLS, 2=FIRST_TO_N
target_kills TINYINT NULL, -- used if mode=2
duration_seconds INT NOT NULL CHECK (duration_seconds BETWEEN 60 AND 3600),
rules_flags INT NOT NULL DEFAULT(0),
status_id TINYINT NOT NULL REFERENCES dbo.offer_status_lu(status_id) DEFAULT 1, -- OPEN
created_at DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME(),
rowver ROWVERSION
);
CREATE INDEX ix_offers_open ON dbo.pvp_offers(status_id, currency_id, created_at DESC);
CREATE INDEX ix_offers_creator ON dbo.pvp_offers(creator_char_id, created_at DESC);
IF OBJECT_ID('dbo.pvp_join_requests') IS NOT NULL DROP TABLE dbo.pvp_join_requests;
CREATE TABLE dbo.pvp_join_requests (
request_id BIGINT IDENTITY(1,1) PRIMARY KEY,
challenge_id BIGINT NOT NULL REFERENCES dbo.pvp_offers(challenge_id),
requester_char_id BIGINT NOT NULL,
requester_level SMALLINT NULL,
status_id TINYINT NOT NULL REFERENCES dbo.join_status_lu(status_id) DEFAULT 1, -- PENDING
message_note NVARCHAR(120) NULL,
created_at DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME(),
decided_at DATETIME2(0) NULL,
rowver ROWVERSION
);
CREATE INDEX ix_join_pending ON dbo.pvp_join_requests(challenge_id, status_id, created_at);
CREATE INDEX ix_join_requester ON dbo.pvp_join_requests(requester_char_id, created_at DESC);
IF OBJECT_ID('dbo.pvp_matches') IS NOT NULL DROP TABLE dbo.pvp_matches;
CREATE TABLE dbo.pvp_matches (
match_id BIGINT IDENTITY(1,1) PRIMARY KEY,
challenge_id BIGINT NOT NULL REFERENCES dbo.pvp_offers(challenge_id),
currency_id TINYINT NOT NULL REFERENCES dbo.currency_lu(currency_id),
stake_a INT NOT NULL,
stake_b INT NOT NULL,
tax_percent TINYINT NOT NULL CHECK (tax_percent BETWEEN 0 AND 100),
pot_amount INT NOT NULL, -- stake_a + stake_b
mode TINYINT NOT NULL, -- 1/2
target_kills TINYINT NULL,
duration_seconds INT NOT NULL,
rules_flags INT NOT NULL,
state_id TINYINT NOT NULL REFERENCES dbo.match_state_lu(state_id) DEFAULT 1,
world_id INT NULL,
started_at DATETIME2(0) NULL,
ends_at DATETIME2(0) NULL,
ended_at DATETIME2(0) NULL,
result_reason_id TINYINT NULL REFERENCES dbo.match_result_reason_lu(reason_id),
winner_char_id BIGINT NULL,
a_kills SMALLINT NOT NULL DEFAULT 0,
b_kills SMALLINT NOT NULL DEFAULT 0,
sudden_death BIT NOT NULL DEFAULT 0,
overtime_started_at DATETIME2(0) NULL,
tiebreak_used TINYINT NULL, -- 1=DAMAGE, 2=FIRST_BLOOD, 3=SEED
tiebreak_winner_role TINYINT NULL, -- 1=A,2=B
created_at DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME(),
rowver ROWVERSION
);
CREATE INDEX ix_matches_state ON dbo.pvp_matches(state_id, created_at);
CREATE INDEX ix_matches_world ON dbo.pvp_matches(world_id);
CREATE INDEX ix_matches_winner ON dbo.pvp_matches(winner_char_id);
IF OBJECT_ID('dbo.pvp_match_participants') IS NOT NULL DROP TABLE dbo.pvp_match_participants;
CREATE TABLE dbo.pvp_match_participants (
match_id BIGINT NOT NULL REFERENCES dbo.pvp_matches(match_id) ON DELETE CASCADE,
char_id BIGINT NOT NULL,
role_id TINYINT NOT NULL REFERENCES dbo.participant_role_lu(role_id), -- A or B
original_map_id INT NULL,
original_x INT NULL,
original_y INT NULL,
original_z INT NULL,
joined_at DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME(),
PRIMARY KEY(match_id, role_id)
);
CREATE INDEX ix_participant_char ON dbo.pvp_match_participants(char_id, match_id);
IF OBJECT_ID('dbo.pvp_match_spectators') IS NOT NULL DROP TABLE dbo.pvp_match_spectators;
CREATE TABLE dbo.pvp_match_spectators (
match_id BIGINT NOT NULL REFERENCES dbo.pvp_matches(match_id) ON DELETE CASCADE,
char_id BIGINT NOT NULL,
joined_at DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME(),
left_at DATETIME2(0) NULL,
PRIMARY KEY(match_id, char_id)
);
IF OBJECT_ID('dbo.pvp_kill_events') IS NOT NULL DROP TABLE dbo.pvp_kill_events;
CREATE TABLE dbo.pvp_kill_events (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
match_id BIGINT NOT NULL REFERENCES dbo.pvp_matches(match_id) ON DELETE CASCADE,
killer_char_id BIGINT NOT NULL,
victim_char_id BIGINT NOT NULL,
occurred_at DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME()
);
CREATE INDEX ix_kills_match_time ON dbo.pvp_kill_events(match_id, occurred_at);
IF OBJECT_ID('dbo.pvp_match_participant_stats') IS NOT NULL DROP TABLE dbo.pvp_match_participant_stats;
CREATE TABLE dbo.pvp_match_participant_stats (
match_id BIGINT NOT NULL REFERENCES dbo.pvp_matches(match_id) ON DELETE CASCADE,
role_id TINYINT NOT NULL REFERENCES dbo.participant_role_lu(role_id),
char_id BIGINT NOT NULL,
kills SMALLINT NOT NULL DEFAULT(0),
deaths SMALLINT NOT NULL DEFAULT(0),
highest_streak TINYINT NOT NULL DEFAULT(0),
current_streak TINYINT NOT NULL DEFAULT(0),
first_blood BIT NOT NULL DEFAULT(0),
last_kill BIT NOT NULL DEFAULT(0),
damage_dealt INT NOT NULL DEFAULT(0),
damage_taken INT NOT NULL DEFAULT(0),
suicides TINYINT NOT NULL DEFAULT(0),
updated_at DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME(),
PRIMARY KEY (match_id, role_id)
);
CREATE INDEX ix_part_stats_char ON dbo.pvp_match_participant_stats(char_id, match_id);
IF OBJECT_ID('dbo.pvp_escrows') IS NOT NULL DROP TABLE dbo.pvp_escrows;
CREATE TABLE dbo.pvp_escrows (
escrow_id BIGINT IDENTITY(1,1) PRIMARY KEY,
match_id BIGINT NOT NULL REFERENCES dbo.pvp_matches(match_id) ON DELETE CASCADE,
char_id BIGINT NOT NULL,
currency_id TINYINT NOT NULL REFERENCES dbo.currency_lu(currency_id),
amount INT NOT NULL CHECK (amount > 0),
status TINYINT NOT NULL DEFAULT 1, -- 1=LOCKED, 2=RELEASED, 3=REFUNDED
ts DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME()
);
CREATE INDEX ix_escrow_match ON dbo.pvp_escrows(match_id, status);
IF OBJECT_ID('dbo.pvp_worlds') IS NOT NULL DROP TABLE dbo.pvp_worlds;
CREATE TABLE dbo.pvp_worlds (
world_id INT PRIMARY KEY, -- 1..5
status TINYINT NOT NULL, -- 1=FREE,2=ALLOCATED,3=LIVE,4=CLEANING
current_match_id BIGINT NULL REFERENCES dbo.pvp_matches(match_id),
updated_at DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME()
);
-- Seed 5 worlds
MERGE dbo.pvp_worlds AS tgt
USING (VALUES (1),(2),(3),(4),(5)) AS src(world_id)
ON tgt.world_id=src.world_id
WHEN NOT MATCHED THEN INSERT(world_id,status,updated_at) VALUES(src.world_id,1,SYSUTCDATETIME());
IF OBJECT_ID('dbo.pvp_audit') IS NOT NULL DROP TABLE dbo.pvp_audit;
CREATE TABLE dbo.pvp_audit (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
match_id BIGINT NULL REFERENCES dbo.pvp_matches(match_id),
challenge_id BIGINT NULL REFERENCES dbo.pvp_offers(challenge_id),
char_id BIGINT NULL,
action VARCHAR(64) NOT NULL, -- OFFER_CREATE, JOIN_REQUEST, MATCH_START, PAYOUT, etc.
payload_json NVARCHAR(MAX) NULL,
ts DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME()
);
CREATE INDEX ix_audit_match ON dbo.pvp_audit(match_id, ts);
CREATE INDEX ix_audit_char ON dbo.pvp_audit(char_id, ts);
-- Optional: track DCs and penalties
IF OBJECT_ID('dbo.pvp_disconnects') IS NOT NULL DROP TABLE dbo.pvp_disconnects;
CREATE TABLE dbo.pvp_disconnects (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
match_id BIGINT NOT NULL REFERENCES dbo.pvp_matches(match_id) ON DELETE CASCADE,
char_id BIGINT NOT NULL,
started_at DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME(),
ended_at DATETIME2(0) NULL
);
IF OBJECT_ID('dbo.pvp_penalties') IS NOT NULL DROP TABLE dbo.pvp_penalties;
CREATE TABLE dbo.pvp_penalties (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
match_id BIGINT NOT NULL REFERENCES dbo.pvp_matches(match_id) ON DELETE CASCADE,
char_id BIGINT NOT NULL,
code VARCHAR(32) NOT NULL, -- "ILLEGAL_BUFF","LEAVE_AREA",...
details NVARCHAR(200) NULL,
ts DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME()
);
/* ==============================
3) VIEWS (for GUIs)
============================== */
CREATE VIEW dbo.v_pvp_browse AS
SELECT
o.challenge_id,
o.creator_char_id,
o.min_level,
c.code AS currency,
o.stake_amount,
o.mode,
o.target_kills,
o.duration_seconds,
o.rules_flags,
'OFFER' AS row_type,
NULL AS match_id,
NULL AS a_char_id,
NULL AS b_char_id,
NULL AS score_a,
NULL AS score_b,
o.created_at AS created_or_started_at
FROM dbo.pvp_offers o
JOIN dbo.currency_lu c ON c.currency_id=o.currency_id
WHERE o.status_id = 1 -- OPEN
UNION ALL
SELECT
NULL AS challenge_id,
NULL AS creator_char_id,
NULL AS min_level,
c.code AS currency,
(m.stake_a + m.stake_b) AS stake_amount,
m.mode,
m.target_kills,
m.duration_seconds,
m.rules_flags,
'LIVE' AS row_type,
m.match_id,
pa.char_id AS a_char_id,
pb.char_id AS b_char_id,
m.a_kills AS score_a,
m.b_kills AS score_b,
COALESCE(m.started_at, m.created_at) AS created_or_started_at
FROM dbo.pvp_matches m
JOIN dbo.currency_lu c ON c.currency_id=m.currency_id
JOIN dbo.pvp_match_participants pa ON pa.match_id=m.match_id AND pa.role_id=1
JOIN dbo.pvp_match_participants pb ON pb.match_id=m.match_id AND pb.role_id=2
WHERE m.state_id IN (2,3,4,5,6); -- QUEUED..OVERTIME
GO
CREATE VIEW dbo.v_pvp_pending_requests AS
SELECT r.request_id, r.challenge_id, r.requester_char_id, r.requester_level,
r.status_id, r.created_at
FROM dbo.pvp_join_requests r
JOIN dbo.pvp_offers o ON o.challenge_id = r.challenge_id
WHERE r.status_id = 1 AND o.status_id IN (1,2); -- PENDING; offer OPEN/LOCKED
GO
CREATE VIEW dbo.v_pvp_live_score AS
SELECT
m.match_id, m.world_id, m.state_id, m.started_at, m.duration_seconds,
pa.char_id AS a_char_id, pb.char_id AS b_char_id,
m.a_kills, m.b_kills
FROM dbo.pvp_matches m
JOIN dbo.pvp_match_participants pa ON pa.match_id=m.match_id AND pa.role_id=1
JOIN dbo.pvp_match_participants pb ON pb.match_id=m.match_id AND pb.role_id=2
WHERE m.state_id IN (3,4,5,6); -- ALLOCATING/WARMUP/LIVE/OVERTIME
GO
/* ==============================
4) STORED PROCEDURES
============================== */
-- 4.1 Create offer
CREATE PROCEDURE dbo.usp_pvp_create_offer
@creator_char_id BIGINT,
@currency_id TINYINT,
@stake_amount INT,
@min_level SMALLINT = NULL,
@mode TINYINT, -- 1 or 2
@target_kills TINYINT = NULL, -- required if mode=2
@duration_seconds INT,
@rules_flags INT = 0,
@challenge_id BIGINT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
IF @mode=2 AND (@target_kills IS NULL OR @target_kills<=0)
THROW 52001, 'FIRST_TO_N requires target_kills > 0', 1;
INSERT INTO dbo.pvp_offers
(creator_char_id,min_level,currency_id,stake_amount,mode,target_kills,duration_seconds,rules_flags,status_id)
VALUES
(@creator_char_id,@min_level,@currency_id,@stake_amount,@mode,@target_kills,@duration_seconds,@rules_flags,1);
SET @challenge_id = SCOPE_IDENTITY();
INSERT INTO dbo.pvp_audit(challenge_id,char_id,action,payload_json)
VALUES(@challenge_id,@creator_char_id,'OFFER_CREATE',NULL);
END
GO
-- 4.2 Cancel offer
CREATE PROCEDURE dbo.usp_pvp_cancel_offer
@creator_char_id BIGINT,
@challenge_id BIGINT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRAN;
BEGIN TRY
UPDATE dbo.pvp_offers WITH(UPDLOCK, HOLDLOCK)
SET status_id=3 -- CANCELED
WHERE challenge_id=@challenge_id AND creator_char_id=@creator_char_id AND status_id=1; -- OPEN
IF @@ROWCOUNT=0
THROW 52002, 'Offer not cancellable', 1;
-- Expire pending join requests
UPDATE dbo.pvp_join_requests
SET status_id=4, decided_at=SYSUTCDATETIME()
WHERE challenge_id=@challenge_id AND status_id=1;
INSERT INTO dbo.pvp_audit(challenge_id,char_id,action,payload_json)
VALUES(@challenge_id,@creator_char_id,'OFFER_CANCEL',NULL);
COMMIT;
END TRY
BEGIN CATCH
IF @@TRANCOUNT>0 ROLLBACK;
THROW;
END CATCH
END
GO
-- 4.3 Join request (user clicks Accept on list; this is the "request to fight")
CREATE PROCEDURE dbo.usp_pvp_join_request
@requester_char_id BIGINT,
@challenge_id BIGINT,
@requester_level SMALLINT = NULL,
@request_id BIGINT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @min_level SMALLINT, @status_id TINYINT;
SELECT @min_level=min_level, @status_id=status_id
FROM dbo.pvp_offers WHERE challenge_id=@challenge_id;
IF @status_id<>1 -- not OPEN
THROW 52003, 'Offer not open', 1;
IF @min_level IS NOT NULL AND @requester_level IS NOT NULL AND @requester_level < @min_level
THROW 52004, 'Level too low', 1;
INSERT INTO dbo.pvp_join_requests(challenge_id, requester_char_id, requester_level)
VALUES(@challenge_id, @requester_char_id, @requester_level);
SET @request_id = SCOPE_IDENTITY();
INSERT INTO dbo.pvp_audit(challenge_id,char_id,action,payload_json)
VALUES(@challenge_id,@requester_char_id,'JOIN_REQUEST',NULL);
END
GO
-- 4.4 Creator approves one request -> creates match, locks offer & escrow
CREATE PROCEDURE dbo.usp_pvp_approve_request
@creator_char_id BIGINT,
@request_id BIGINT,
@tax_percent TINYINT,
@original_map_a INT = NULL, @orig_x_a INT=NULL, @orig_y_a INT=NULL, @orig_z_a INT=NULL,
@original_map_b INT = NULL, @orig_x_b INT=NULL, @orig_y_b INT=NULL, @orig_z_b INT=NULL,
@match_id BIGINT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRAN;
BEGIN TRY
DECLARE @challenge_id BIGINT, @requester BIGINT, @offer_status TINYINT,
@currency_id TINYINT, @stake INT, @mode TINYINT, @tk TINYINT,
@duration INT, @rules INT;
SELECT @challenge_id = r.challenge_id, @requester = r.requester_char_id
FROM dbo.pvp_join_requests r WITH(UPDLOCK, HOLDLOCK)
WHERE r.request_id=@request_id AND r.status_id=1; -- PENDING
IF @challenge_id IS NULL THROW 52005, 'Request not pending', 1;
SELECT @offer_status = o.status_id, @currency_id=o.currency_id, @stake=o.stake_amount,
@mode=o.mode, @tk=o.target_kills, @duration=o.duration_seconds, @rules=o.rules_flags
FROM dbo.pvp_offers o WITH(UPDLOCK, HOLDLOCK)
WHERE o.challenge_id=@challenge_id AND o.creator_char_id=@creator_char_id;
IF @offer_status<>1 THROW 52006, 'Offer not open', 1;
-- Lock the offer
UPDATE dbo.pvp_offers SET status_id=2 -- LOCKED
WHERE challenge_id=@challenge_id AND status_id=1;
IF @@ROWCOUNT=0 THROW 52007, 'Offer lock race lost', 1;
-- Accept this request and decline others
UPDATE dbo.pvp_join_requests SET status_id=2, decided_at=SYSUTCDATETIME()
WHERE request_id=@request_id AND status_id=1;
UPDATE dbo.pvp_join_requests SET status_id=3, decided_at=SYSUTCDATETIME()
WHERE challenge_id=@challenge_id AND status_id=1; -- decline remaining
-- Create match
INSERT INTO dbo.pvp_matches
(challenge_id,currency_id,stake_a,stake_b,tax_percent,pot_amount,
mode,target_kills,duration_seconds,rules_flags,state_id)
VALUES
(@challenge_id,@currency_id,@stake,@stake,@tax_percent,@stake*2,
@mode,@tk,@duration,@rules,1); -- PENDING_CONFIRM
SET @match_id = SCOPE_IDENTITY();
-- Insert participants (A = creator, B = requester)
INSERT INTO dbo.pvp_match_participants(match_id,char_id,role_id,original_map_id,original_x,original_y,original_z)
VALUES(@match_id,@creator_char_id,1,@original_map_a,@orig_x_a,@orig_y_a,@orig_z_a),
(@match_id,@requester,2,@original_map_b,@orig_x_b,@orig_y_b,@orig_z_b);
-- Seed participant stats
INSERT INTO dbo.pvp_match_participant_stats(match_id,role_id,char_id)
VALUES(@match_id,1,@creator_char_id),(@match_id,2,@requester);
-- Create escrow rows (wallet debits happen in app layer, but log here)
INSERT INTO dbo.pvp_escrows(match_id,char_id,currency_id,amount,status)
VALUES(@match_id,@creator_char_id,@currency_id,@stake,1), -- LOCKED
(@match_id,@requester,@currency_id,@stake,1);
INSERT INTO dbo.pvp_audit(match_id,challenge_id,char_id,action,payload_json)
VALUES(@match_id,@challenge_id,@creator_char_id,'MATCH_CREATED',NULL);
COMMIT;
END TRY
BEGIN CATCH
IF @@TRANCOUNT>0 ROLLBACK;
THROW;
END CATCH
END
GO
-- 4.5 Allocate a FREE world to the next match (call from scheduler)
CREATE PROCEDURE dbo.usp_pvp_allocate_world
@match_id BIGINT,
@world_id INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRAN;
BEGIN TRY
DECLARE @state TINYINT;
SELECT @state=state_id FROM dbo.pvp_matches WITH(UPDLOCK) WHERE match_id=@match_id;
IF @state NOT IN (1,2) -- PENDING_CONFIRM or QUEUED
THROW 52008, 'Match not allocatable', 1;
SELECT TOP(1) @world_id=world_id
FROM dbo.pvp_worlds WITH(UPDLOCK, HOLDLOCK)
WHERE status=1 -- FREE
ORDER BY world_id;
IF @world_id IS NULL THROW 52009, 'No free world', 1;
UPDATE dbo.pvp_worlds
SET status=2, current_match_id=@match_id, updated_at=SYSUTCDATETIME()
WHERE world_id=@world_id;
UPDATE dbo.pvp_matches
SET state_id=3, world_id=@world_id -- ALLOCATING
WHERE match_id=@match_id;
INSERT INTO dbo.pvp_audit(match_id,action,payload_json)
VALUES(@match_id,'WORLD_ALLOCATED',NULL);
COMMIT;
END TRY
BEGIN CATCH
IF @@TRANCOUNT>0 ROLLBACK;
THROW;
END CATCH
END
GO
-- 4.6 Start match (after teleports and warm-up)
CREATE PROCEDURE dbo.usp_pvp_start_match
@match_id BIGINT,
@duration_seconds INT
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.pvp_matches
SET state_id=5, -- LIVE
started_at=SYSUTCDATETIME(),
ends_at=DATEADD(SECOND,@duration_seconds,SYSUTCDATETIME()),
duration_seconds=@duration_seconds
WHERE match_id=@match_id AND state_id IN (3,4,2,1); -- ALLOCATING/WARMUP/QUEUED/PENDING_CONFIRM
UPDATE dbo.pvp_worlds
SET status=3, updated_at=SYSUTCDATETIME()
WHERE current_match_id=@match_id;
INSERT INTO dbo.pvp_audit(match_id,action,payload_json)
VALUES(@match_id,'MATCH_START',NULL);
END
GO
-- 4.7 Record kill (authoritative server calls this)
CREATE PROCEDURE dbo.usp_pvp_record_kill
@match_id BIGINT,
@killer_char_id BIGINT,
@victim_char_id BIGINT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @state_id TINYINT, @mode TINYINT, @target TINYINT,
@role_killer TINYINT, @role_victim TINYINT,
@ak SMALLINT, @bk SMALLINT;
SELECT @state_id=state_id, @mode=mode, @target=target_kills
FROM dbo.pvp_matches WITH(UPDLOCK)
WHERE match_id=@match_id;
IF @state_id NOT IN (5,6) RETURN; -- only LIVE/OVERTIME
SELECT @role_killer = p.role_id
FROM dbo.pvp_match_participants p
WHERE p.match_id=@match_id AND p.char_id=@killer_char_id;
SELECT @role_victim = p.role_id
FROM dbo.pvp_match_participants p
WHERE p.match_id=@match_id AND p.char_id=@victim_char_id;
IF @role_killer IS NULL OR @role_victim IS NULL OR @role_killer=@role_victim RETURN;
INSERT INTO dbo.pvp_kill_events(match_id,killer_char_id,victim_char_id)
VALUES(@match_id,@killer_char_id,@victim_char_id);
IF @role_killer=1
UPDATE dbo.pvp_matches SET a_kills=a_kills+1 WHERE match_id=@match_id;
ELSE
UPDATE dbo.pvp_matches SET b_kills=b_kills+1 WHERE match_id=@match_id;
-- update participant stats
UPDATE s SET
kills = CASE WHEN @role_killer = s.role_id THEN kills + 1 ELSE kills END,
deaths = CASE WHEN @role_victim = s.role_id THEN deaths + 1 ELSE deaths END,
current_streak = CASE
WHEN @role_killer = s.role_id THEN current_streak + 1
WHEN @role_victim = s.role_id THEN 0
ELSE current_streak
END,
highest_streak = CASE
WHEN @role_killer = s.role_id AND current_streak + 1 > highest_streak
THEN current_streak + 1
ELSE highest_streak
END,
updated_at = SYSUTCDATETIME()
FROM dbo.pvp_match_participant_stats s
WHERE s.match_id=@match_id AND s.role_id IN (1,2);
-- First blood
IF NOT EXISTS (SELECT 1 FROM dbo.pvp_match_participant_stats WHERE match_id=@match_id AND first_blood=1)
UPDATE dbo.pvp_match_participant_stats SET first_blood=1 WHERE match_id=@match_id AND role_id=@role_killer;
-- Last kill flag
UPDATE dbo.pvp_match_participant_stats
SET last_kill = CASE WHEN role_id=@role_killer THEN 1 ELSE 0 END
WHERE match_id=@match_id AND role_id IN (1,2);
-- If FIRST_TO_N, check for finish
SELECT @ak=a_kills, @bk=b_kills FROM dbo.pvp_matches WHERE match_id=@match_id;
IF @mode=2 AND @target IS NOT NULL
BEGIN
IF @ak >= @target OR @bk >= @target
BEGIN
DECLARE @winner BIGINT;
IF @ak > @bk
SELECT @winner = char_id FROM dbo.pvp_match_participants WHERE match_id=@match_id AND role_id=1;
ELSE
SELECT @winner = char_id FROM dbo.pvp_match_participants WHERE match_id=@match_id AND role_id=2;
EXEC dbo.usp_pvp_finish_match @match_id=@match_id, @reason_id=2, @winner_char_id=@winner;
END
END
END
GO
-- 4.8 Finish match & payout (server calls on timer end or surrender/DC)
CREATE PROCEDURE dbo.usp_pvp_finish_match
@match_id BIGINT,
@reason_id TINYINT, -- per match_result_reason_lu
@winner_char_id BIGINT = NULL -- NULL allowed only if tie policy uses refund or later SD
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRAN;
BEGIN TRY
DECLARE @pot INT, @tax_pct TINYINT, @tax_amt INT, @win_amt INT,
@currency TINYINT, @a BIGINT, @b BIGINT, @ak SMALLINT, @bk SMALLINT;
SELECT @pot=pot_amount, @tax_pct=tax_percent, @currency=currency_id, @ak=a_kills, @bk=b_kills
FROM dbo.pvp_matches WITH(UPDLOCK) WHERE match_id=@match_id;
SELECT @a=char_id FROM dbo.pvp_match_participants WHERE match_id=@match_id AND role_id=1;
SELECT @b=char_id FROM dbo.pvp_match_participants WHERE match_id=@match_id AND role_id=2;
IF @winner_char_id IS NULL
BEGIN
-- Tie policy here (example: use score, else sudden death should have been used)
IF @ak > @bk SET @winner_char_id=@a;
ELSE IF @bk > @ak SET @winner_char_id=@b;
ELSE
BEGIN
-- If truly tie and no SD: pick first blood owner as tiebreak
DECLARE @fb_role TINYINT;
SELECT TOP(1) @fb_role = CASE WHEN role_id=1 THEN 1 WHEN role_id=2 THEN 2 END
FROM dbo.pvp_match_participant_stats WHERE match_id=@match_id AND first_blood=1;
IF @fb_role=1 SET @winner_char_id=@a; ELSE IF @fb_role=2 SET @winner_char_id=@b;
-- As a last resort (shouldn't happen often), pick lower char_id:
IF @winner_char_id IS NULL SET @winner_char_id = CASE WHEN @a<@b THEN @a ELSE @b END;
UPDATE dbo.pvp_matches SET tiebreak_used=2, tiebreak_winner_role=@fb_role WHERE match_id=@match_id;
END
END
SET @tax_amt = (@pot * @tax_pct) / 100;
SET @win_amt = @pot - @tax_amt;
UPDATE dbo.pvp_matches
SET state_id=7, ended_at=SYSUTCDATETIME(), result_reason_id=@reason_id, winner_char_id=@winner_char_id
WHERE match_id=@match_id;
-- Release escrows rows (status -> RELEASED), app layer should credit winner wallet with @win_amt
UPDATE dbo.pvp_escrows SET status=2, ts=SYSUTCDATETIME() WHERE match_id=@match_id AND status=1;
-- World -> CLEANING
UPDATE dbo.pvp_worlds SET status=4, updated_at=SYSUTCDATETIME()
WHERE current_match_id=@match_id;
-- Audit payout
INSERT INTO dbo.pvp_audit(match_id,action,payload_json)
VALUES(@match_id,'PAYOUT', CONCAT(N'{"winner":',@winner_char_id,',"currency":',@currency,',"pot":',@pot,',"tax":',@tax_amt,',"net":',@win_amt,'}'));
COMMIT;
-- NOTE: Credit winner wallet & route tax to sink/house in APP layer right after COMMIT.
END TRY
BEGIN CATCH
IF @@TRANCOUNT>0 ROLLBACK;
THROW;
END CATCH
END
GO
-- 4.9 Free world after cleanup (call after you’ve teleported participants back)
CREATE PROCEDURE dbo.usp_pvp_world_free
@world_id INT
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.pvp_worlds
SET status=1, current_match_id=NULL, updated_at=SYSUTCDATETIME()
WHERE world_id=@world_id;
END
GO
-- 4.10 Spectators add/remove
CREATE PROCEDURE dbo.usp_pvp_add_spectator
@match_id BIGINT,
@char_id BIGINT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @state TINYINT;
SELECT @state=state_id FROM dbo.pvp_matches WHERE match_id=@match_id;
IF @state NOT IN (3,4,5,6) THROW 52010, 'Match not spectatable', 1;
IF NOT EXISTS (SELECT 1 FROM dbo.pvp_match_spectators WHERE match_id=@match_id AND char_id=@char_id)
INSERT INTO dbo.pvp_match_spectators(match_id,char_id) VALUES(@match_id,@char_id);
END
GO
CREATE PROCEDURE dbo.usp_pvp_remove_spectator
@match_id BIGINT,
@char_id BIGINT
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.pvp_match_spectators
SET left_at=SYSUTCDATETIME()
WHERE match_id=@match_id AND char_id=@char_id AND left_at IS NULL;
END
GO
/* ==============================
5) INDICES & TUNING
============================== */
-- Filtered index for live/active matches list
IF NOT EXISTS (
SELECT 1 FROM sys.indexes WHERE name='ix_live_matches' AND object_id=OBJECT_ID('dbo.pvp_matches')
)
BEGIN
CREATE INDEX ix_live_matches ON dbo.pvp_matches(state_id, started_at DESC)
WHERE state_id IN (3,4,5,6);
END
GO
/* ==============================
DONE
============================== */
Editor is loading...
Leave a Comment