Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions config.lua.dist
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ toggleMaintainMode = false
maintainModeMessage = ""

-- Combat settings
-- NOTE: valid values for worldType are: "pvp", "no-pvp" and "pvp-enforced"
-- NOTE: valid values for worldType are: "expert-pvp", "retro-pvp", "no-pvp" and "pvp-enforced"
-- NOTE: "expert-pvp" = full 2014 Open PvP (pass-through, situation boxes, dove/white/yellow/red hand modes, first-in-stack)
-- NOTE: "retro-pvp" (aliases "pvp"/"open") = plain Open PvP without the 2014 Expert extras
-- NOTE: removeBeginningWeaponAmmunition: spears, arrows, bolt have endless ammo (allows training for paladins)
-- NOTE: refundManaOnBeginningWeapons: wand of vortex and snakebite refund mana used (allows training for mages)
worldType = "pvp"
worldType = "expert-pvp"
hotkeyAimbotEnabled = true
protectionLevel = 7
pzLocked = 60 * 1000
Expand Down
5 changes: 5 additions & 0 deletions data-global/scripts/lib/register_actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,11 @@ function onUseMachete(player, item, fromPosition, target, toPosition, isHotkey)
end

if table.contains(wildGrowth, target.itemid) then
-- Open PvP (2014 rules): a rune-cast wild growth can only be cut by its caster
if target:hasOwner() and not target:isOwner(player) then
player:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE)
return true
end
toPosition:sendMagicEffect(CONST_ME_POFF)
target:remove()
return true
Expand Down
7 changes: 7 additions & 0 deletions data/migrations/64.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function onUpdateDatabase()
logger.info("Updating database to version 64 (open pvp: frag share weight columns)")

if not db.query("ALTER TABLE `player_kills` ADD COLUMN `weight` smallint(5) UNSIGNED NOT NULL DEFAULT 100") then
logger.warn("Column weight may already exist on player_kills.")
end
end
9 changes: 9 additions & 0 deletions data/scripts/runes/magic_wall.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ function onCreateMagicWall(creature, position)
if item then
item:setDuration(16, 24)
item:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, string.format("Casted by: %s", creature:getName()))
-- Open PvP (2014 rules): ownership decides who the wall blocks and who walks through.
-- Normalize summon casts to the master so the owner is always a stable player GUID.
local fieldOwner = creature:getMaster() or creature
item:setOwner(fieldOwner)
-- Cast outside a PvP situation -> PvE wall: blocks only monsters, every player
-- (including the caster) walks through it
if fieldOwner:isPlayer() and not fieldOwner:hasActivePvpSituation() then
item:setCustomAttribute("pveWall", true)
end
end
end

Expand Down
9 changes: 9 additions & 0 deletions data/scripts/runes/wild_growth.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ function onCreateWildGrowth(creature, position)
if item then
item:setDuration(30)
item:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, string.format("Casted by: %s", creature:getName()))
-- Open PvP (2014 rules): ownership decides who the growth blocks, who passes and who may cut it.
-- Normalize summon casts to the master so the owner is always a stable player GUID.
local fieldOwner = creature:getMaster() or creature
item:setOwner(fieldOwner)
-- Cast outside a PvP situation -> PvE growth: blocks only monsters, every player
-- (including the caster) walks through it
if fieldOwner:isPlayer() and not fieldOwner:hasActivePvpSituation() then
item:setCustomAttribute("pveWall", true)
end
end
end

Expand Down
31 changes: 31 additions & 0 deletions data/scripts/talkactions/god/pvp_mark_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-- PvP-box protocol RE helper (opcode 0x93 CreatureMark, client 15.25).
-- /pvpmark <markType>[,<weaponType>]
-- weaponType omitted / 255 -> legacy border mark [0x93][id][0x01][markType] (markType = color byte)
-- weaponType set -> new-style mark [0x93][id][markType][weaponType]
-- Sent about your current target (or yourself if no target). Watch which value renders the
-- yellow / orange / brown PvP situation box, then wire it into the real PvP-situation code.
local pvpMark = TalkAction("/pvpmark")

function pvpMark.onSay(player, words, param)
if param == "" then
player:sendCancelMessage("Usage: /pvpmark <markType>[,<weaponType>] — e.g. /pvpmark 2 or /pvpmark 1,3")
return true
end

local split = param:split(",")
local markType = tonumber(split[1])
local weaponType = tonumber(split[2]) or 255
if not markType then
player:sendCancelMessage("markType must be a number (0-255).")
return true
end

local target = player:getTarget() or player
player:sendCreatureSquare(target, markType, weaponType)
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, string.format("[pvpmark] target=%s markType=%d weaponType=%d (%s)", target:getName(), markType, weaponType, weaponType == 255 and "legacy border" or "new-style mark"))
return true
end

pvpMark:separator(" ")
pvpMark:groupType("god")
pvpMark:register()
3 changes: 2 additions & 1 deletion schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS `server_config` (
CONSTRAINT `server_config_pk` PRIMARY KEY (`config`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '63'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0');
INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '64'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0');

-- Table structure `accounts`
CREATE TABLE IF NOT EXISTS `accounts` (
Expand Down Expand Up @@ -703,6 +703,7 @@ CREATE TABLE IF NOT EXISTS `player_kills` (
`time` bigint(20) UNSIGNED NOT NULL DEFAULT '0',
`target` int(11) NOT NULL,
`unavenged` tinyint(1) NOT NULL DEFAULT '0',
`weight` smallint(5) UNSIGNED NOT NULL DEFAULT '100',
CONSTRAINT `player_kills_players_fk`
FOREIGN KEY (`player_id`) REFERENCES `players` (`id`)
ON DELETE CASCADE
Expand Down
127 changes: 125 additions & 2 deletions src/creatures/combat/combat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,13 @@ ReturnValue Combat::canTargetCreature(const std::shared_ptr<Player> &player, con
return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER;
}

if (player->hasSecureMode() && !Combat::isInPvpZone(player, target) && player->getSkullClient(target->getPlayer()) == SKULL_NONE) {
if (g_game().isExpertPvp()) {
// Open PvP: the expert PvP mode governs who may be targeted (supersedes secure mode)
const ReturnValue modeRet = checkExpertPvpMode(player, target->getPlayer());
if (modeRet != RETURNVALUE_NOERROR) {
return modeRet;
}
} else if (player->hasSecureMode() && !Combat::isInPvpZone(player, target) && player->getSkullClient(target->getPlayer()) == SKULL_NONE) {
return RETURNVALUE_TURNSECUREMODETOATTACKUNMARKEDPLAYERS;
}
}
Expand Down Expand Up @@ -467,6 +473,90 @@ bool Combat::isProtected(const std::shared_ptr<Player> &attacker, const std::sha
return false;
}

bool pvpModeAllows(PvpMode_t mode, const PvpFacts &facts) {
if (mode == PVP_MODE_RED_FIST) {
// red fist: attack anyone EXCEPT party/guild members
return !facts.partner && !facts.guildMate;
}

// every remaining mode allows self-defense: the target attacked us or we are already in a mutual PvP situation
if (facts.targetAttackedMe || facts.inSituation) {
return true;
}

// white/yellow hand: defend party/guild by allowing attacks on someone who hit one of our members
if ((mode == PVP_MODE_WHITE_HAND || mode == PVP_MODE_YELLOW_HAND) && facts.attackedAllyOfMe) {
return true;
}

// yellow hand: skulled players are fair game
if (mode == PVP_MODE_YELLOW_HAND && facts.targetSkulled) {
return true;
}

return false;
}

ReturnValue Combat::checkExpertPvpMode(const std::shared_ptr<Player> &attacker, const std::shared_ptr<Player> &target) {
if (!attacker || !target || attacker == target || attacker->isAccessPlayer()) {
return RETURNVALUE_NOERROR;
}

if (!g_game().isExpertPvp() || Combat::isInPvpZone(attacker, target) || attacker->isInWar(target)) {
return RETURNVALUE_NOERROR;
}

const PvpMode_t mode = attacker->getPvpMode();

// gather the decision facts from the live players, then defer to the pure decision matrix. All of
// these accessors are side-effect-free reads, so evaluating them eagerly does not change behavior.
PvpFacts facts;
facts.targetAttackedMe = target->hasAttacked(attacker);
facts.inSituation = attacker->isInPvpSituationWith(target);
facts.targetSkulled = attacker->getSkullClient(target) != SKULL_NONE;
facts.attackedAllyOfMe = target->hasAttackedAllyOf(attacker);
facts.partner = attacker->isPartner(target);
facts.guildMate = attacker->isGuildMate(target);

return pvpModeAllows(mode, facts) ? RETURNVALUE_NOERROR : RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER;
}

std::shared_ptr<Player> Combat::resolveFieldOwnerPlayer(uint32_t ownerId) {
// A field's ownerId is a stable player GUID (setOwner normalises players/summons to their master GUID).
// Resolve it to the owning player, falling back to a live summon's master for safety.
if (ownerId == 0) {
return nullptr;
}
if (const auto &ownerPlayer = g_game().getPlayerByGUID(ownerId)) {
return ownerPlayer;
}
if (const auto &ownerCreature = g_game().getCreatureByID(ownerId)) {
if (ownerCreature->isSummon() && ownerCreature->getMaster()) {
return ownerCreature->getMaster()->getPlayer();
}
}
return nullptr;
}

bool Combat::isOwnedFieldBystander(const std::shared_ptr<Player> &player, const std::shared_ptr<Item> &field) {
if (!player || !field || !g_game().isExpertPvp()) {
return false;
}

const auto ownerId = field->getOwnerId();
if (ownerId == 0) {
return false; // map/GM/monster fields concern everyone
}

// owner and everyone in a PvP situation with him are involved; the rest are bystanders
const auto &ownerPlayer = resolveFieldOwnerPlayer(ownerId);
return ownerPlayer && ownerPlayer != player && !ownerPlayer->isInPvpSituationWith(player);
}

bool Combat::isPveWall(const std::shared_ptr<Item> &item) {
return item && item->getCustomAttribute("pveWall") != nullptr;
}

ReturnValue Combat::canDoCombat(const std::shared_ptr<Creature> &attacker, const std::shared_ptr<Creature> &target, bool aggressive) {
if (!aggressive) {
return RETURNVALUE_NOERROR;
Expand Down Expand Up @@ -499,6 +589,15 @@ ReturnValue Combat::canDoCombat(const std::shared_ptr<Creature> &attacker, const
}

const auto &targetPlayerTile = targetPlayer->getTile();
// Open PvP (2014 rules): a summon's aggression is attributed to its master, so the owner's
// expert PvP mode (dove/white hand) governs whether the summon may hit this player. Without
// this a dove/white-hand player could freely damage neutrals through their summon.
if (!attackerPlayer && masterAttackerPlayer) {
const ReturnValue summonExpertRet = checkExpertPvpMode(masterAttackerPlayer, targetPlayer);
if (summonExpertRet != RETURNVALUE_NOERROR) {
return summonExpertRet;
}
}
if (attackerPlayer) {
if (attackerPlayer->hasFlag(PlayerFlags_t::CannotAttackPlayer)) {
return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER;
Expand All @@ -508,6 +607,20 @@ ReturnValue Combat::canDoCombat(const std::shared_ptr<Creature> &attacker, const
return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER;
}

// Open PvP (2014 rules): a player hidden under another player in the tile stack
// cannot INITIATE PvP — defending inside an existing situation stays allowed.
if (g_game().isExpertPvp() && !attackerPlayer->isFirstInStack()
&& !attackerPlayer->isInPvpSituationWith(targetPlayer) && !targetPlayer->hasAttacked(attackerPlayer)) {
return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER;
}

// Open PvP expert mode gate — enforced here too so dove/white hand players never
// damage innocents, not even with area-spell splash.
const ReturnValue expertRet = checkExpertPvpMode(attackerPlayer, targetPlayer);
if (expertRet != RETURNVALUE_NOERROR) {
return expertRet;
}

// nopvp-zone
const auto &attackerTile = attackerPlayer->getTile();
if (targetPlayerTile && targetPlayerTile->hasFlag(TILESTATE_NOPVPZONE)) {
Expand Down Expand Up @@ -2645,6 +2758,14 @@ void MagicField::onStepInField(const std::shared_ptr<Creature> &creature) {
const auto &conditionCopy = it.conditionDamage->clone();
auto ownerId = getOwnerId();
if (ownerId) {
// Open PvP (2014 rules): a player-made field harms only its caster and players in a
// PvP situation with him — bystanders walk through unharmed.
if (const auto &steppingPlayer = creature->getPlayer()) {
if (Combat::isOwnedFieldBystander(steppingPlayer, static_self_cast<Item>())) {
return;
}
}

bool harmfulField = true;
const auto &itemTile = getTile();
if (g_game().getWorldType() == WORLDTYPE_OPTIONAL || (itemTile && itemTile->hasFlag(TILESTATE_NOPVPZONE))) {
Expand All @@ -2662,7 +2783,9 @@ void MagicField::onStepInField(const std::shared_ptr<Creature> &creature) {

const auto &targetPlayer = creature->getPlayer();
if (targetPlayer) {
const auto &attackerPlayer = g_game().getPlayerByID(ownerId);
// ownerId is a player GUID, not a runtime creature id — resolve it correctly so the
// isProtected() gate is not silently skipped for player-owned fields.
const auto &attackerPlayer = Combat::resolveFieldOwnerPlayer(ownerId);
if (attackerPlayer) {
if (Combat::isProtected(attackerPlayer, targetPlayer)) {
harmfulField = false;
Expand Down
24 changes: 24 additions & 0 deletions src/creatures/combat/combat.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,21 @@ class AreaCombat {
bool hasExtArea = false;
};

// Open PvP expert modes: the pure allow/deny decision extracted from Combat::checkExpertPvpMode so it
// can be unit-tested in isolation. Inputs are pre-gathered facts about the attacker/target pair; the
// function reads no globals and has no side effects.
struct PvpFacts {
bool targetAttackedMe = false; // the target has attacked us (self-defense)
bool inSituation = false; // we are already in a mutual PvP situation with the target
bool targetSkulled = false; // the target carries a skull visible to us (yellow-hand fair game)
bool attackedAllyOfMe = false; // the target has attacked one of our party/guild members
bool partner = false; // the target is in our party
bool guildMate = false; // the target is in our guild
};

// Returns true if `mode` permits attacking a target described by `facts`. Pure decision matrix.
bool pvpModeAllows(PvpMode_t mode, const PvpFacts &facts);

class Combat {
public:
Combat() = default;
Expand All @@ -211,6 +226,15 @@ class Combat {

static bool isInPvpZone(const std::shared_ptr<Creature> &attacker, const std::shared_ptr<Creature> &target);
static bool isProtected(const std::shared_ptr<Player> &attacker, const std::shared_ptr<Player> &target);
// Open PvP expert modes (dove / white hand / yellow hand / red fist): who may attack whom.
static ReturnValue checkExpertPvpMode(const std::shared_ptr<Player> &attacker, const std::shared_ptr<Player> &target);
static bool isOwnedFieldBystander(const std::shared_ptr<Player> &player, const std::shared_ptr<Item> &field);
// Open PvP (2014 rules): resolve a field's ownerId (a player GUID) to the owning player, falling
// back to a live summon's master. Single source of truth for field-owner lookup.
static std::shared_ptr<Player> resolveFieldOwnerPlayer(uint32_t ownerId);
// Open PvP (2014 rules): wall cast OUTSIDE a PvP situation = PvE wall — blocks only monsters,
// every player (including the caster) walks through it. Flagged at cast time by the rune script.
static bool isPveWall(const std::shared_ptr<Item> &item);
static bool isPlayerCombat(const std::shared_ptr<Creature> &target);
static CombatType_t ConditionToDamageType(ConditionType_t type);
static ConditionType_t DamageToConditionType(CombatType_t type);
Expand Down
8 changes: 8 additions & 0 deletions src/creatures/combat/spells.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,14 @@ bool Spell::playerSpellCheck(const std::shared_ptr<Player> &player) const {
return false;
}

// Open PvP (2014 rules): a player hidden under another player in the tile stack
// cannot use aggressive area spells/runes — only the top ("first in stack") player can.
if (aggressive && range < 1 && g_game().isExpertPvp() && !player->isFirstInStack()) {
player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE);
g_game().addMagicEffect(player->getPosition(), CONST_ME_POFF);
return false;
}

if (aggressive && player->hasCondition(CONDITION_PACIFIED)) {
player->sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED);
g_game().addMagicEffect(player->getPosition(), CONST_ME_POFF);
Expand Down
2 changes: 1 addition & 1 deletion src/creatures/creature.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,7 @@ class Creature : virtual public Thing, public SharedObject {
const auto stepSpeed = getStepSpeed();
walk.calculatedStepSpeed = 1;
if (stepSpeed > -Creature::speedB) {
const auto formula = std::floor((Creature::speedA * log(stepSpeed + Creature::speedB) + Creature::speedC) + .5);
const auto formula = std::floor((Creature::speedA * std::log(stepSpeed + Creature::speedB) + Creature::speedC) + .5);
walk.calculatedStepSpeed = static_cast<uint16_t>(std::max(formula, 1.));
}

Expand Down
6 changes: 4 additions & 2 deletions src/creatures/creatures_definitions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1615,9 +1615,11 @@ struct Kill {
uint32_t target;
time_t time;
bool unavenged;
// Open PvP "unfair fight" frag share: 1.0 = full frag, <1.0 with 6+ participants.
double weight;

Kill(uint32_t _target, time_t _time, bool _unavenged) :
target(_target), time(_time), unavenged(_unavenged) { }
Kill(uint32_t _target, time_t _time, bool _unavenged, double _weight = 1.0) :
target(_target), time(_time), unavenged(_unavenged), weight(_weight) { }
};

struct IntervalInfo {
Expand Down
Loading
Loading