mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-06-24 15:43:13 -04:00
Merge remote-tracking branch 'origin/randomizer' into rando-archi
This commit is contained in:
@@ -12,6 +12,11 @@
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include <cstring>
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/randomizer/game/stages.h"
|
||||
#include "dusk/randomizer/game/tools.h"
|
||||
#endif
|
||||
|
||||
static int daObj_Gb_Draw(obj_gb_class* i_this) {
|
||||
g_env_light.settingTevStruct(0x10, &i_this->current.pos, &i_this->tevStr);
|
||||
g_env_light.setLightTevColorType_MAJI(i_this->mModel, &i_this->tevStr);
|
||||
@@ -169,6 +174,13 @@ static int useHeapInit(fopAc_ac_c* actor) {
|
||||
static int daObj_Gb_Create(fopAc_ac_c* actor) {
|
||||
fopAcM_ct(actor, obj_gb_class);
|
||||
obj_gb_class* i_this = (obj_gb_class*)actor;
|
||||
#if TARGET_PC
|
||||
// Only spawn the added wall in randomizer if it should exist
|
||||
if (randomizer_IsActive() && getStageID() == StageIDs::Mirror_Chamber &&
|
||||
!randomizer_mirrorChamberWallShouldExist()) {
|
||||
return cPhs_ERROR_e;
|
||||
}
|
||||
#endif
|
||||
int rv = dComIfG_resLoad(&i_this->mPhase, "Obj_gb");
|
||||
|
||||
if (rv == cPhs_COMPLEATE_e) {
|
||||
|
||||
@@ -2013,6 +2013,13 @@ stage_arrow_class* dComIfGp_getRoomArrow(int i_roomNo) {
|
||||
void dComIfGp_setNextStage(char const* i_stage, s16 i_point, s8 i_roomNo, s8 i_layer,
|
||||
f32 i_lastSpeed, u32 i_lastMode, int i_setPoint, s8 i_wipe,
|
||||
s16 i_lastAngle, int param_9, int i_wipeSpeedT) {
|
||||
#if TARGET_PC
|
||||
// In rando, override this entrance if applicable
|
||||
if (randomizer_IsActive()) {
|
||||
randomizer_checkAndOverrideEntranceData(i_stage, i_roomNo, i_point, i_layer);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (i_layer >= 15) {
|
||||
i_layer = -1;
|
||||
}
|
||||
|
||||
@@ -125,6 +125,10 @@ std::optional<std::string> RandomizerContext::WriteToFile(const fspath& path) {
|
||||
textData << YAML::EndMap;
|
||||
textData << YAML::EndMap;
|
||||
|
||||
for (const auto& [key, override] : mEntranceOverrides) {
|
||||
out["mEntranceOverrides"][key] = std::bit_cast<int>(override);
|
||||
}
|
||||
|
||||
seedData << YAML::Dump(out);
|
||||
seedData << '\n' << textData.c_str();
|
||||
seedData.close();
|
||||
@@ -286,6 +290,13 @@ std::optional<std::string> RandomizerContext::LoadFromPath(const fspath& path) {
|
||||
}
|
||||
}
|
||||
|
||||
// Entrance Overrides
|
||||
for (const auto& entranceNode : in["mEntranceOverrides"]) {
|
||||
auto key = entranceNode.first.as<int>();
|
||||
auto override = std::bit_cast<EntranceOverride>(entranceNode.second.as<int>());
|
||||
this->mEntranceOverrides[key] = override;
|
||||
}
|
||||
|
||||
dusk::ui::push_toast(dusk::ui::Toast{
|
||||
.title = "Randomizer",
|
||||
.content = fmt::format("Loaded Randomizer Seed {}", this->mHash),
|
||||
@@ -299,7 +310,7 @@ std::filesystem::path RandomizerContext::GetSeedDataPath() const {
|
||||
}
|
||||
|
||||
int RandomizerContext::SettingToEnum(const std::string& settingName) {
|
||||
static const std::unordered_map<std::string, int> nameToEnum = {
|
||||
static const std::map<std::string, int> nameToEnum = {
|
||||
{"Hyrule Barrier Dungeons", HYRULE_BARRIER_DUNGEONS},
|
||||
{"Hyrule Barrier Requirements", HYRULE_BARRIER_REQUIREMENTS},
|
||||
{"Hyrule Barrier Fused Shadows", HYRULE_BARRIER_FUSED_SHADOWS},
|
||||
@@ -317,6 +328,7 @@ int RandomizerContext::SettingToEnum(const std::string& settingName) {
|
||||
{"Skip Minor Cutscenes", SKIP_MINOR_CUTSCENES},
|
||||
{"Skip Major Cutscenes", SKIP_MAJOR_CUTSCENES},
|
||||
{"Skip Bridge Donation", SKIP_BRIDGE_DONATION},
|
||||
{"Mirror Chamber Access", MIRROR_CHAMBER_ACCESS},
|
||||
};
|
||||
|
||||
if (nameToEnum.contains(settingName)) {
|
||||
@@ -327,7 +339,7 @@ int RandomizerContext::SettingToEnum(const std::string& settingName) {
|
||||
}
|
||||
|
||||
int RandomizerContext::OptionToEnum(const std::string& optionName) {
|
||||
static const std::unordered_map<std::string, int> nameToEnum = {
|
||||
static const std::map<std::string, int> nameToEnum = {
|
||||
{"On", ON},
|
||||
{"Off", OFF},
|
||||
{"None", NONE},
|
||||
@@ -342,6 +354,8 @@ int RandomizerContext::OptionToEnum(const std::string& optionName) {
|
||||
{"Ordon Sword", ORDON_SWORD},
|
||||
{"Master Sword", MASTER_SWORD},
|
||||
{"Light Sword", LIGHT_SWORD},
|
||||
{"Closed", CLOSED},
|
||||
{"Barrier", BARRIER},
|
||||
};
|
||||
|
||||
if (nameToEnum.contains(optionName)) {
|
||||
@@ -539,7 +553,7 @@ static void updateGoalFlags() {
|
||||
}
|
||||
}
|
||||
|
||||
// Palace of Twlight Access
|
||||
// Palace of Twilight Access
|
||||
if (!dComIfGs_isEventBit(FIXED_THE_MIRROR_OF_TWILIGHT)) {
|
||||
bool openPalace = false;
|
||||
switch (settings[RandomizerContext::PALACE_OF_TWILIGHT_REQUIREMENTS]) {
|
||||
@@ -808,6 +822,21 @@ int randomizer_getItemAtLocation(const std::string& locationName) {
|
||||
return randomizer_GetContext().mItemLocations[locationName].itemId;
|
||||
}
|
||||
|
||||
void randomizer_checkAndOverrideEntranceData(const char*& stageName, s8& roomNo, s16& pointNo, s8& mapLayer) {
|
||||
RandomizerContext::EntranceOverride override = {
|
||||
static_cast<u8>(getStageID(stageName)), roomNo, static_cast<s8>(pointNo), mapLayer
|
||||
};
|
||||
|
||||
int key = std::bit_cast<int>(override);
|
||||
if (randomizer_GetContext().mEntranceOverrides.contains(key)) {
|
||||
auto& newOverride = randomizer_GetContext().mEntranceOverrides[key];
|
||||
stageName = allStages[newOverride.stageId];
|
||||
pointNo = newOverride.pointNo;
|
||||
roomNo = newOverride.roomNo;
|
||||
mapLayer = newOverride.mapLayer;
|
||||
}
|
||||
}
|
||||
|
||||
static void randomizer_setTempFlag(RandomizerContext::itemLocationData data) {
|
||||
// If stage is 0xFF, then this is an event flag
|
||||
if (data.stage == 0xFF) {
|
||||
@@ -871,6 +900,12 @@ bool randomizer_checkTempleOfTimeRequirement() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool randomizer_mirrorChamberWallShouldExist() {
|
||||
auto mirrorChamberAccess = randomizer_GetContext().mSettings[RandomizerContext::MIRROR_CHAMBER_ACCESS];
|
||||
return mirrorChamberAccess == RandomizerContext::CLOSED ||
|
||||
(mirrorChamberAccess == RandomizerContext::BARRIER && !dComIfGs_isStageBossEnemy(0x13));
|
||||
}
|
||||
|
||||
u8 randomizer_getRandomFoolishItemModelID() {
|
||||
static constexpr auto foolishItemModels = std::to_array<u8>({
|
||||
dItemNo_Randomizer_ARMOR_e,
|
||||
@@ -1219,7 +1254,7 @@ RandomizerContext WriteSeedData(randomizer::logic::world::World* world) {
|
||||
const auto& stageName = stageNode.first.as<std::string>();
|
||||
for (const auto& roomNode : stageNode.second) {
|
||||
u8 roomNo{};
|
||||
// Special value for
|
||||
// Special value for actors always on the stage and not just one specific room
|
||||
if (roomNode.first.as<std::string>() == "Stage") {
|
||||
roomNo = RandomizerContext::ROOM_STAGE;
|
||||
} else {
|
||||
@@ -1331,6 +1366,27 @@ RandomizerContext WriteSeedData(randomizer::logic::world::World* world) {
|
||||
}
|
||||
}
|
||||
|
||||
// Entrance Overrides
|
||||
if (world->Setting("Mirror Chamber Access") == "Closed") {
|
||||
// Set exiting the Arbiter's Grounds Boss Room to spawn at the Arbiter's Grounds entrance
|
||||
// if mirror chamber access is closed
|
||||
RandomizerContext::EntranceOverride original = {
|
||||
StageIDs::Mirror_Chamber,
|
||||
4,
|
||||
0,
|
||||
-1
|
||||
};
|
||||
|
||||
RandomizerContext::EntranceOverride override = {
|
||||
StageIDs::Bulblin_Camp,
|
||||
3,
|
||||
3,
|
||||
-1
|
||||
};
|
||||
|
||||
randoData.mEntranceOverrides[std::bit_cast<int>(original)] = override;
|
||||
}
|
||||
|
||||
return std::move(randoData);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,14 +70,15 @@ public:
|
||||
// Map of language -> map of key -> string
|
||||
std::unordered_map<int, std::unordered_map<u32, std::string>> mTextOverrides{};
|
||||
|
||||
// TODO: hook this up to generator data
|
||||
struct {
|
||||
// for now use hardcoded values for this
|
||||
std::string mapName = "F_SP103"; // (Ordon) Outside Link's House
|
||||
int pointNo = 1;
|
||||
int roomNo = 1;
|
||||
int mapLayer = -1;
|
||||
} mStartLocation;
|
||||
struct EntranceOverride {
|
||||
u8 stageId = 0xFF;
|
||||
s8 roomNo = -1;
|
||||
s8 pointNo = -1;
|
||||
s8 mapLayer = -1;
|
||||
};
|
||||
|
||||
// keyed by stageId << 24 | pointNo << 16 | roomNo << 8 | mapLayer
|
||||
std::unordered_map<int, EntranceOverride> mEntranceOverrides{};
|
||||
|
||||
std::optional<std::string> WriteToFile();
|
||||
std::optional<std::string> WriteToFile(const fspath& path);;
|
||||
@@ -103,6 +104,7 @@ public:
|
||||
SKIP_MINOR_CUTSCENES,
|
||||
SKIP_MAJOR_CUTSCENES,
|
||||
SKIP_BRIDGE_DONATION,
|
||||
MIRROR_CHAMBER_ACCESS,
|
||||
};
|
||||
|
||||
enum Options {
|
||||
@@ -120,6 +122,8 @@ public:
|
||||
ORDON_SWORD,
|
||||
MASTER_SWORD,
|
||||
LIGHT_SWORD,
|
||||
BARRIER,
|
||||
CLOSED,
|
||||
};
|
||||
|
||||
static int SettingToEnum(const std::string& settingName);
|
||||
@@ -208,6 +212,10 @@ bool randomizer_IsActive();
|
||||
|
||||
int randomizer_getItemAtLocation(const std::string& locationName);
|
||||
|
||||
/*
|
||||
* @brief Overrides the given entrance paramaters if an override exists for them
|
||||
*/
|
||||
void randomizer_checkAndOverrideEntranceData(const char*& i_Name, s8& i_RoomNo, s16& i_Point, s8& i_Layer);
|
||||
/*
|
||||
* @brief Puts the associated flag into the randomizer state's temporary flag
|
||||
* variable. This allows the tracker/Archipelago to know a location has been checked
|
||||
@@ -219,6 +227,8 @@ void randomizer_setTempFlagForFLWOverride(u32 key);
|
||||
|
||||
bool randomizer_checkTempleOfTimeRequirement();
|
||||
|
||||
bool randomizer_mirrorChamberWallShouldExist();
|
||||
|
||||
u8 randomizer_getRandomFoolishItemModelID();
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
- Type: Spawn
|
||||
Forward:
|
||||
Connection: Links Spawn -> Outside Links House
|
||||
Stage: -1
|
||||
Room: -1
|
||||
Spawn: ""
|
||||
Stage: 43
|
||||
Room: 1
|
||||
Spawn: "01"
|
||||
Spawn Type: ""
|
||||
Parameters: ""
|
||||
State: "FF"
|
||||
@@ -449,11 +449,11 @@
|
||||
Return:
|
||||
Connection: Arbiters Grounds Boss Room -> Mirror Chamber Lower
|
||||
Alias: Arbiters Grounds Boss Room -> Mirror Chamber
|
||||
Stage: -1
|
||||
Room: -1
|
||||
Spawn: ""
|
||||
Spawn Type: ""
|
||||
Parameters: ""
|
||||
Stage: 60
|
||||
Room: 4
|
||||
Spawn: "00"
|
||||
Spawn Type: "10"
|
||||
Parameters: "501F"
|
||||
State: "FF"
|
||||
|
||||
- Type: Boss
|
||||
|
||||
@@ -57,7 +57,16 @@
|
||||
event: 42
|
||||
parameters: 0x00000000
|
||||
next node index: 0x10C
|
||||
#5: # zel_05.bmg
|
||||
|
||||
5: # zel_05.bmg
|
||||
# Patch Gor Liggs to not set the flag for the third key shard
|
||||
# Change event index 17 to event index 42 which does nothing
|
||||
- index: 0x258
|
||||
type: event
|
||||
event: 42
|
||||
parameters: 0x00000000
|
||||
next node index: 0x1AC
|
||||
|
||||
#6: # zel_06.bmg
|
||||
#7: # zel_07.bmg
|
||||
#8: # zel_08.bmg
|
||||
|
||||
@@ -3470,6 +3470,28 @@ F_SP124:
|
||||
layers:
|
||||
- 0
|
||||
|
||||
# Mirror Chamber
|
||||
F_SP125:
|
||||
# Room 4 - Main Chamber
|
||||
4:
|
||||
# Add barrier to prevent players from going back to the Arbiters Boss Room
|
||||
# depending on settings
|
||||
- action: add
|
||||
name: Obj_gb
|
||||
parameters: 0x800F0601
|
||||
position:
|
||||
x: 1794.0
|
||||
y: 2523.0
|
||||
z: -17400.0
|
||||
angle:
|
||||
x: 0xFF7F
|
||||
y: 0x0000
|
||||
z: 0x0000
|
||||
set id: 0xFFFF
|
||||
layers:
|
||||
- 0
|
||||
- 1
|
||||
|
||||
# Upper Zora's River
|
||||
F_SP126:
|
||||
# Room 0 - Main area
|
||||
|
||||
@@ -79,6 +79,15 @@
|
||||
- Closed: "Midna will block the player from leaving Faron Woods until Forest Temple is completed."
|
||||
- Open: "Midna will not prevent the player from leaving Faron Woods."
|
||||
|
||||
- Name: Mirror Chamber Access
|
||||
Need In Game: True
|
||||
Tracker Important: True
|
||||
Default Option: Open
|
||||
Options:
|
||||
- Open: "The entrance is open and operates like normal. If you start with the Mirror Chamber Portal, you can access the Stallord boss fight without going through Arbiter's Grounds."
|
||||
- Barrier: "A barrier is placed in front of the Mirror Chamber entrance and goes away once Stallord is defeated."
|
||||
- Closed: "The Mirror Chamber is isolated from the world and cannot be reached from the Stallord boss room. To access it, players will either need the portal or access from Palace of Twilight."
|
||||
|
||||
######################
|
||||
## Item Pool ##
|
||||
######################
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
Can Warp: True
|
||||
Exits:
|
||||
Mirror Chamber Upper: Nothing
|
||||
Arbiters Grounds Boss Room: Nothing
|
||||
Arbiters Grounds Boss Room: Mirror_Chamber_Access == Open or (Mirror_Chamber_Access == Barrier and 'Can_Complete_Arbiters_Grounds')
|
||||
|
||||
- Name: Mirror Chamber Upper
|
||||
Map Sector: Desert Province
|
||||
|
||||
@@ -102,12 +102,14 @@
|
||||
Snowpeak Summit Lower: Nothing
|
||||
|
||||
- Name: Snowpeak Ruins East Door Interior
|
||||
Region: Snowpeak Ruins
|
||||
Can Transform: Never
|
||||
Exits:
|
||||
Snowpeak Ruins East Door Exterior: Can_Open_Doors
|
||||
Snowpeak Ruins Entrance: Nothing
|
||||
|
||||
- Name: Snowpeak Ruins West Door Interior
|
||||
Region: Snowpeak Ruins
|
||||
Can Transform: Never
|
||||
Exits:
|
||||
Snowpeak Ruins West Door Exterior: Can_Open_Doors
|
||||
|
||||
@@ -250,7 +250,7 @@ namespace randomizer::logic::area
|
||||
this->AddHintRegion(region);
|
||||
LOG_TO_DEBUG("Assigned \"" + region + "\" as hint region to \"" + this->GetName() + "\"");
|
||||
|
||||
// Also assign any loactions in this area to the dungeon if there are any dungeon regions
|
||||
// Also assign any locations in this area to the dungeon if there are any dungeon regions
|
||||
if (dungeons.contains(region))
|
||||
{
|
||||
auto locAccs = this->GetLocations();
|
||||
|
||||
@@ -423,6 +423,15 @@ namespace randomizer::logic::item_pool
|
||||
startingItems["Castle Town Portal"] = 1;
|
||||
}
|
||||
|
||||
// Automatically give players the Mirror Chamber Portal if Mirror Chamber Access is closed
|
||||
// and they aren't both randomizing and decoupling dungeon entrances. Otherwise, there's no
|
||||
// way to access the chamber
|
||||
if (world->Setting("Mirror Chamber Access") == "Closed" &&
|
||||
!(world->Setting("Randomize Dungeon Entrances") == "On" && world->Setting("Decouple Entrances") == "On"))
|
||||
{
|
||||
startingItems["Mirror Chamber Portal"] = 1;
|
||||
}
|
||||
|
||||
// Add each item to the world's _startingItemPool and erase it from the regular _itemPool
|
||||
for (const auto& [itemName, count] : startingItems)
|
||||
{
|
||||
|
||||
@@ -65,19 +65,19 @@ namespace randomizer::logic::spoiler_log
|
||||
LogBasicInfo(spoilerLog, randomizer);
|
||||
|
||||
// Gather worlds with starting inventories
|
||||
std::list<world::World*> worldswithStartingInventories = {};
|
||||
std::list<world::World*> worldsWithStartingInventories = {};
|
||||
for (const auto& world : worlds)
|
||||
{
|
||||
if (!world->GetStartingItemPool().empty())
|
||||
{
|
||||
worldswithStartingInventories.push_back(world.get());
|
||||
worldsWithStartingInventories.push_back(world.get());
|
||||
}
|
||||
}
|
||||
// Print starting inventories if there are any
|
||||
if (!worldswithStartingInventories.empty())
|
||||
if (!worldsWithStartingInventories.empty())
|
||||
{
|
||||
spoilerLog << std::endl << "All Starting Items:" << std::endl;
|
||||
for (const auto& world : worldswithStartingInventories)
|
||||
for (const auto& world : worldsWithStartingInventories)
|
||||
{
|
||||
spoilerLog << " World " << world->GetID() << ":" << std::endl;
|
||||
for (const auto& item : world->GetStartingItemPool())
|
||||
@@ -87,7 +87,31 @@ namespace randomizer::logic::spoiler_log
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Print required dungeons
|
||||
// Gather worlds with required dungeons
|
||||
std::list<world::World*> worldsWithRequiredDungeons = {};
|
||||
for (const auto& world : worlds)
|
||||
{
|
||||
for (const auto& [dungeonName, dungeon] : world->GetDungeonTable()) {
|
||||
if (dungeon->IsRequired()) {
|
||||
worldsWithRequiredDungeons.push_back(world.get());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Print required dungeons if there are any
|
||||
if (!worldsWithRequiredDungeons.empty())
|
||||
{
|
||||
spoilerLog << std::endl << "Required Dungeons:" << std::endl;
|
||||
for (const auto& world : worldsWithRequiredDungeons)
|
||||
{
|
||||
spoilerLog << " World " << world->GetID() << ":" << std::endl;
|
||||
for (const auto& [dungeonName, dungeon] : world->GetDungeonTable()) {
|
||||
if (dungeon->IsRequired()) {
|
||||
spoilerLog << " - " << dungeonName << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get name lengths for pretty formatting
|
||||
size_t longestNameLength = 0;
|
||||
|
||||
@@ -728,22 +728,22 @@ namespace randomizer::logic::world
|
||||
for (auto& [areaName, area] : this->_areaTable)
|
||||
{
|
||||
area->AssignHintRegionsAndDungeonLocations();
|
||||
}
|
||||
|
||||
for (auto& [areaName, area] : this->_areaTable)
|
||||
{
|
||||
// Also assign dungeons their starting entrance
|
||||
for (const auto& exit : area->GetExits())
|
||||
{
|
||||
auto parentRegions = exit->GetParentArea()->GetHintRegions();
|
||||
auto connectedRegions = exit->GetConnectedArea()->GetHintRegions();
|
||||
if (!parentRegions.contains("None"))
|
||||
for (auto& [dungeonName, dungeon] : this->_dungeons)
|
||||
{
|
||||
for (auto& [dungeonName, dungeon] : this->_dungeons)
|
||||
// If this exit leads into a dungeon and its parent area is not part of the dungeon
|
||||
// then this is the entrance that leads into the dungeon
|
||||
if (connectedRegions.contains(dungeonName) && !parentRegions.contains(dungeonName))
|
||||
{
|
||||
// If this exit leads into a dungeon and its parent area is not part of the dungeon
|
||||
// then this is the entrance that leads into the dungeon
|
||||
if (connectedRegions.contains(dungeonName) && !parentRegions.contains(dungeonName))
|
||||
{
|
||||
dungeon->AddStartingEntrance(exit);
|
||||
}
|
||||
dungeon->AddStartingEntrance(exit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -926,6 +926,7 @@ RandomizerWindow::RandomizerWindow(dFile_select_c* fileSelect /*= nullptr*/) : m
|
||||
});
|
||||
rando_config_group(leftPane, rightPane, "Palace of Twilight Requirements");
|
||||
rando_config_group(leftPane, rightPane, "Faron Woods Logic");
|
||||
rando_config_group(leftPane, rightPane, "Mirror Chamber Access");
|
||||
|
||||
// leftPane.add_section("World (TODO)");
|
||||
|
||||
@@ -1220,8 +1221,7 @@ RandomizerWindow::RandomizerWindow(dFile_select_c* fileSelect /*= nullptr*/) : m
|
||||
|
||||
leftPane.register_control(leftPane.add_button("Warp to Start").on_pressed([] {
|
||||
mDoAud_seStartMenu(kSoundClick);
|
||||
auto& locData = randomizer_GetContext().mStartLocation;
|
||||
dComIfGp_setNextStage(locData.mapName.c_str(), locData.pointNo, locData.roomNo, locData.mapLayer);
|
||||
dComIfGp_setNextStage("F_SP103", 1, 1, -1);
|
||||
}), rightPane, [](Pane& pane) {
|
||||
pane.clear();
|
||||
pane.add_rml("Respawns the player at their appropriate starting location.");
|
||||
|
||||
Reference in New Issue
Block a user