mirror of
https://gitlab.com/hashborgir/d2tools.git
synced 2024-11-30 04:26:03 +00:00
ajaxify d2s editor
This commit is contained in:
parent
ce83c42701
commit
81f1199a96
@ -43,7 +43,7 @@ foreach ($ISCData as $k => $v) {
|
|||||||
//$filePath = "D:\Diablo II\MODS\MedianXL2012\save\Test.d2s";
|
//$filePath = "D:\Diablo II\MODS\MedianXL2012\save\Test.d2s";
|
||||||
|
|
||||||
|
|
||||||
$filePath = "Test.d2s";
|
$filePath = "Pest.d2s";
|
||||||
$char = new D2Char($filePath);
|
$char = new D2Char($filePath);
|
||||||
|
|
||||||
//$char->setChar("CharacterStatus", "Died", 0);
|
//$char->setChar("CharacterStatus", "Died", 0);
|
||||||
@ -51,23 +51,23 @@ $char = new D2Char($filePath);
|
|||||||
//$char->setChar("CharacterStatus", "Expansion", 1);
|
//$char->setChar("CharacterStatus", "Expansion", 1);
|
||||||
//$char->setChar("LeftmousebuttonskillID", 223);
|
//$char->setChar("LeftmousebuttonskillID", 223);
|
||||||
|
|
||||||
$char->setAllSkills(1);
|
//$char->setAllSkills(1);
|
||||||
//$char->setSkill(1, 99);
|
$char->setSkill(4, 99);
|
||||||
//$char->setChar("CharacterClass", "Necromancer"); // 127
|
//$char->setChar("CharacterClass", "Necromancer"); // 127
|
||||||
//$char->setChar("CharacterProgression", 1); // 0 in normal, 1 finished normal, 2 finished nm, 3 finished hell
|
//$char->setChar("CharacterProgression", 1); // 0 in normal, 1 finished normal, 2 finished nm, 3 finished hell
|
||||||
|
|
||||||
//$char->setChar("CharacterStatus", "Died", 1);
|
//$char->setChar("CharacterStatus", "Died", 1);
|
||||||
$char->setChar("CharacterLevel", 99);
|
//$char->setChar("CharacterLevel", 99);
|
||||||
$char->setStat("strength", 270);
|
//$char->setStat("strength", 270);
|
||||||
$char->setStat("energy", 270);
|
//$char->setStat("energy", 270);
|
||||||
$char->setStat("dexterity", 270);
|
//$char->setStat("dexterity", 270);
|
||||||
$char->setStat("vitality", 270);
|
//$char->setStat("vitality", 270);
|
||||||
$char->setStat("hitpoints", 100);
|
//$char->setStat("hitpoints", 100);
|
||||||
$char->setStat("maxhp", 150);
|
//$char->setStat("maxhp", 150);
|
||||||
$char->setStat("stamina", 280);
|
//$char->setStat("stamina", 280);
|
||||||
$char->setStat("maxstamina", 290);
|
//$char->setStat("maxstamina", 290);
|
||||||
$char->setStat("mana", 70);
|
//$char->setStat("mana", 70);
|
||||||
$char->setStat("maxmana", 200);
|
//$char->setStat("maxmana", 200);
|
||||||
|
|
||||||
|
|
||||||
//$char->setStat("soulcounter", 80);
|
//$char->setStat("soulcounter", 80);
|
||||||
@ -88,5 +88,5 @@ unset($char); // destroy $char so we can read it again after writing to it to ge
|
|||||||
|
|
||||||
$char = new D2Char($filePath);
|
$char = new D2Char($filePath);
|
||||||
|
|
||||||
var_dump($char->cData['CharacterStatus']);
|
var_dump($char->cData);
|
||||||
var_dump($char->cData['stats']);
|
//var_dump($char->cData['stats']);
|
||||||
|
36
res/app.js
36
res/app.js
@ -376,5 +376,41 @@ $(document).ready(function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get the value of the input named "filePath"
|
||||||
|
function constructURL(cmd, name, value, filePath) {
|
||||||
|
return "/saveCharacter.php?cmd=" + cmd + "&name=" + name + "&value=" + value + "&filePath=" + filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(".charform input[name='Difficulty'], .charform .skill, #CharacterName, #CharacterClass, #CharacterLevel, #CharacterClass").change(function () {
|
||||||
|
var name = $(this).attr("name");
|
||||||
|
var newValue = $(this).val();
|
||||||
|
|
||||||
|
var cmd = $(this).attr("cmd") || $(this).attr("id") || name;
|
||||||
|
var parentFormId = $(this).closest("form").attr("id");
|
||||||
|
var parentFormClass = $(this).closest("form").attr("class");
|
||||||
|
var filePath = $(`#${parentFormId} input[name='filePath']`).val();
|
||||||
|
|
||||||
|
var url = constructURL(cmd, name, newValue, filePath);
|
||||||
|
|
||||||
|
console.log(url);
|
||||||
|
console.log(newValue)
|
||||||
|
|
||||||
|
|
||||||
|
$.get(url, function (response) {
|
||||||
|
if (cmd == 'CharacterName') {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
console.log(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});// end document.ready
|
});// end document.ready
|
@ -36,6 +36,11 @@ require_once './src/D2ItemDesc.php';
|
|||||||
require_once './src/D2Char.php';
|
require_once './src/D2Char.php';
|
||||||
require_once './src/D2CharStructureData.php';
|
require_once './src/D2CharStructureData.php';
|
||||||
require_once './src/D2ByteReader.php';
|
require_once './src/D2ByteReader.php';
|
||||||
|
require_once './src/D2BitReader.php';
|
||||||
|
require_once './src/D2Item.php';
|
||||||
|
require_once './src/D2Char.php';
|
||||||
|
|
||||||
|
PDO_Connect("sqlite:" . DB_FILE);
|
||||||
|
|
||||||
// Create an instance of D2CharStructureData class
|
// Create an instance of D2CharStructureData class
|
||||||
$csData = new D2CharStructureData();
|
$csData = new D2CharStructureData();
|
||||||
@ -45,8 +50,7 @@ $cmd = $g['cmd'];
|
|||||||
|
|
||||||
// Get the file path from the POST data and replace backslashes /
|
// Get the file path from the POST data and replace backslashes /
|
||||||
$filePath = $g['filePath'];
|
$filePath = $g['filePath'];
|
||||||
$filePath = str_replace("\\", "\\\\", $filePath);
|
//$filePath = str_replace("\\", "\\\\", $filePath);
|
||||||
|
|
||||||
// Handle the WP check/uncheck
|
// Handle the WP check/uncheck
|
||||||
if ($cmd == "wp") {
|
if ($cmd == "wp") {
|
||||||
$diff = $g['diff'];
|
$diff = $g['diff'];
|
||||||
@ -174,3 +178,68 @@ if ($cmd == "q") {
|
|||||||
$checksum = (shell_exec("bin\d2scs.exe \"$filePath\""));
|
$checksum = (shell_exec("bin\d2scs.exe \"$filePath\""));
|
||||||
fclose($fp);
|
fclose($fp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$cmd = $_GET['cmd'];
|
||||||
|
$name = $_GET['name'];
|
||||||
|
$value = $_GET['value'];
|
||||||
|
|
||||||
|
$char = new D2Char(basename($filePath));
|
||||||
|
|
||||||
|
switch ($cmd) {
|
||||||
|
case "CharacterName":
|
||||||
|
// Handle CharacterName command
|
||||||
|
// Validate the character name length
|
||||||
|
if (strlen($value) < 2 || strlen($value) > 15) {
|
||||||
|
echo "Character name must be between 2 and 15 characters long.";
|
||||||
|
// You can handle the error case accordingly (e.g., return an error response)
|
||||||
|
// Additional validation checks can be added here as needed
|
||||||
|
} else {
|
||||||
|
// Validate the character name format
|
||||||
|
$pattern = '/^[A-Za-z0-9_-]+$/'; // Regular expression pattern for allowed characters
|
||||||
|
if (!preg_match($pattern, $value)) {
|
||||||
|
echo "\nCharacter name contains invalid characters. Only alphabets, numbers, underscore, and dash are allowed.";
|
||||||
|
// Handle the error case accordingly
|
||||||
|
} else {
|
||||||
|
// The character name is valid, proceed with further processing
|
||||||
|
$char->setChar($cmd, $value);
|
||||||
|
echo "\nCharacter Name and filename Changed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "\nHandling CharacterName command - Name: $value, File Path: $filePath";
|
||||||
|
break;
|
||||||
|
case "CharacterClass":
|
||||||
|
// Handle CharacterClass command
|
||||||
|
$char->setChar($cmd, $value);
|
||||||
|
echo "Handling CharacterClass command - Class: $value, File Path: $filePath";
|
||||||
|
break;
|
||||||
|
case "CharacterLevel":
|
||||||
|
// Handle CharacterLevel command
|
||||||
|
$char->setChar($cmd, $value);
|
||||||
|
echo "Handling CharacterLevel command - Level: $value, File Path: $filePath";
|
||||||
|
break;
|
||||||
|
|
||||||
|
// handle difficulty wierdly
|
||||||
|
case "Normal":
|
||||||
|
$char->setChar("Difficulty", $value);
|
||||||
|
echo "Handling Difficulty command - Difficulty: $value, File Path: $filePath";
|
||||||
|
break;
|
||||||
|
case "NM":
|
||||||
|
$char->setChar("Difficulty", $value);
|
||||||
|
echo "Handling Difficulty command - Difficulty: $value, File Path: $filePath";
|
||||||
|
break;
|
||||||
|
case "Hell":
|
||||||
|
$char->setChar("Difficulty", $value);
|
||||||
|
echo "Handling Difficulty command - Difficulty: $value, File Path: $filePath";
|
||||||
|
break;
|
||||||
|
|
||||||
|
// skills
|
||||||
|
case "skills":
|
||||||
|
//var_dump($char->cData);
|
||||||
|
$char->setSkill($name, $value);
|
||||||
|
echo "Handling skills command - Skill: $name, Value: $value, File Path: $filePath";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Handle unknown command
|
||||||
|
echo "Unknown command: $cmd";
|
||||||
|
break;
|
||||||
|
}
|
@ -254,6 +254,7 @@ WHERE sk.charclass = '$class'";
|
|||||||
foreach ($skills as $k => $v) {
|
foreach ($skills as $k => $v) {
|
||||||
if ($this->skillData[$k]['String']) {
|
if ($this->skillData[$k]['String']) {
|
||||||
$cData['skills'][$k] = [
|
$cData['skills'][$k] = [
|
||||||
|
'id' => $this->skillData[$k]['Id'],
|
||||||
'skill' => $this->skillData[$k]['String'],
|
'skill' => $this->skillData[$k]['String'],
|
||||||
'points' => $v,
|
'points' => $v,
|
||||||
'page' => $this->skillData[$k]['SkillPage'],
|
'page' => $this->skillData[$k]['SkillPage'],
|
||||||
@ -373,12 +374,23 @@ WHERE sk.charclass = '$class'";
|
|||||||
public function setChar(string $stat, mixed $val, mixed $val2 = null) {
|
public function setChar(string $stat, mixed $val, mixed $val2 = null) {
|
||||||
switch ($stat) {
|
switch ($stat) {
|
||||||
case 'CharacterName':
|
case 'CharacterName':
|
||||||
if (strlen($val) < 1 || strlen($val) > 15) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$pack = $this->ByteReader->bitsToHexString($this->ByteReader->toBits(pack('Z16', $val)));
|
$pack = $this->ByteReader->bitsToHexString($this->ByteReader->toBits(pack('Z16', $val)));
|
||||||
$this->ByteReader->writeBytes(20, $pack);
|
$this->ByteReader->writeBytes(20, $pack);
|
||||||
rename($this->filePath, $_SESSION['savepath'] . "$val.d2s");
|
$this->data = $this->ByteReader->getData();
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
// file is not being saved, and copied before being saved, hence the bug, fixed
|
||||||
|
|
||||||
|
$fileName = pathinfo($this->filePath, PATHINFO_FILENAME);
|
||||||
|
$originalFileNames = glob($_SESSION['savepath'] . $fileName . '*');
|
||||||
|
foreach ($originalFileNames as $originalFileName) {
|
||||||
|
$newFileName = $_SESSION['savepath'] . $val . '.' . pathinfo($originalFileName, PATHINFO_EXTENSION);
|
||||||
|
if ($originalFileName !== $newFileName) {
|
||||||
|
copy($originalFileName, $newFileName);
|
||||||
|
unlink($originalFileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "CharacterClass":
|
case "CharacterClass":
|
||||||
@ -398,6 +410,7 @@ WHERE sk.charclass = '$class'";
|
|||||||
if ($val > 99) {
|
if ($val > 99) {
|
||||||
$val = 99;
|
$val = 99;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->ByteReader->writeByte(43, $val);
|
$this->ByteReader->writeByte(43, $val);
|
||||||
$this->setStat('level', $val);
|
$this->setStat('level', $val);
|
||||||
|
|
||||||
@ -443,6 +456,21 @@ WHERE sk.charclass = '$class'";
|
|||||||
$this->save();
|
$this->save();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'Difficulty':
|
||||||
|
switch ($val) {
|
||||||
|
case "Normal":
|
||||||
|
$this->data[168] = pack('C', 255); // 1000 0000 MSB = Difficulty
|
||||||
|
break;
|
||||||
|
case "NM":
|
||||||
|
$this->data[169] = pack('C', 255);
|
||||||
|
break;
|
||||||
|
case "Hell":
|
||||||
|
$this->data[170] = pack('C', 255);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
case 'LeftmousebuttonskillID':
|
case 'LeftmousebuttonskillID':
|
||||||
$this->ByteReader->writeBytes(120, dechex($val));
|
$this->ByteReader->writeBytes(120, dechex($val));
|
||||||
break;
|
break;
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
$tabs .= <<<EOT
|
$tabs .= <<<EOT
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link btn btn-outline-danger" style="background: #ddd; border: 1px solid #888;" id="{$c->cData['CharacterName']}-tab" data-toggle="tab" href="#{$c->cData['CharacterName']}" role="tab" aria-controls="{$c->cData['CharacterName']}" aria-selected="true"><img height="64" width="" src="img/chars/{$c->cData['CharacterClass']}.gif">{$c->cData['CharacterName']}</a>
|
<a class="nav-link btn btn-outline-danger" style="background: #ddd; border: 1px solid #888;" id="{$c->cData['CharacterName']}-tab" data-toggle="tab" href="#{$c->cData['CharacterName']}" role="tab" aria-controls="{$c->cData['CharacterName']}" aria-selected="true"><img height="64" width="" src="img/chars/{$c->cData['CharacterClass']}.gif"><span class="chartab">{$c->cData['CharacterName']}</span></a>
|
||||||
</li>
|
</li>
|
||||||
EOT;
|
EOT;
|
||||||
echo $tabs;
|
echo $tabs;
|
||||||
@ -39,12 +39,10 @@ EOT;
|
|||||||
}
|
}
|
||||||
|
|
||||||
$wps = '';
|
$wps = '';
|
||||||
$wps .= "<input type='radio' value='1' name='wp_all' id='wp_all'>";
|
//$wps .= "<input type='radio' value='1' name='wp_all' id='wp_all'>";
|
||||||
$wps .= "<label style='font-size: 1.3em;color:green;' for='wp_all'>Enable All Waypoints</label><br>";
|
//$wps .= "<label style='font-size: 1.3em;color:green;' for='wp_all'>Enable All Waypoints</label><br>";
|
||||||
$wps .= "<input type='radio' value='0' name='wp_all' id='wp_all_off'>";
|
//$wps .= "<input type='radio' value='0' name='wp_all' id='wp_all_off'>";
|
||||||
$wps .= "<label style='font-size: 1.3em; color: red;' for='wp_all_off'>Disable All Waypoints</label><hr>";
|
//$wps .= "<label style='font-size: 1.3em; color: red;' for='wp_all_off'>Disable All Waypoints</label><hr>";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
foreach ($c->cData['Waypoints'] as $diff => $waypoints) { // diff is difficulty
|
foreach ($c->cData['Waypoints'] as $diff => $waypoints) { // diff is difficulty
|
||||||
$wps .= "<h2>$diff</h2>";
|
$wps .= "<h2>$diff</h2>";
|
||||||
@ -60,7 +58,6 @@ EOT;
|
|||||||
$wps .= "<label for='$k'>$kD</label><br>";
|
$wps .= "<label for='$k'>$kD</label><br>";
|
||||||
$wp_count++;
|
$wp_count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$classOptions = [
|
$classOptions = [
|
||||||
@ -74,54 +71,77 @@ EOT;
|
|||||||
}
|
}
|
||||||
|
|
||||||
$difficulties = [
|
$difficulties = [
|
||||||
'Normal' => 'DifficultyNormal',
|
'Normal' => '1',
|
||||||
'NM' => 'DifficultyNM',
|
'NM' => '2',
|
||||||
'Hell' => 'DifficultyHell'
|
'Hell' => '3'
|
||||||
];
|
];
|
||||||
|
|
||||||
$radio = '';
|
$radio = '';
|
||||||
|
|
||||||
foreach ($difficulties as $difficulty => $id) {
|
foreach ($difficulties as $difficulty => $id) {
|
||||||
$checked = ($c->cData['Difficulty'][$difficulty] == 1) ? 'checked' : '';
|
$checked = ($c->cData['Difficulty'][$difficulty] == 1) ? 'checked' : '';
|
||||||
$radio .= "<input $checked type='radio' id='$id' name='Difficulty' value='$difficulty'>$difficulty<br>";
|
$radio .= "<input $checked type='radio' id='$difficulty' name='Difficulty' value='$difficulty'>";
|
||||||
|
$radio .= "<label for='$difficulty'>$difficulty</label><br>";
|
||||||
}
|
}
|
||||||
|
|
||||||
$skills = '';
|
$skills = '';
|
||||||
|
/*
|
||||||
|
'skills' =>
|
||||||
|
array (size=1)
|
||||||
|
'skills' =>
|
||||||
|
array (size=52)
|
||||||
|
0 =>
|
||||||
|
array (size=7)
|
||||||
|
'id' => int 36
|
||||||
|
'skill' => string 'Fire Bolt' (length=9)
|
||||||
|
'points' => int 0
|
||||||
|
'page' => int 3
|
||||||
|
'row' => int 1
|
||||||
|
'col' => int 2
|
||||||
|
'icon' => int 0
|
||||||
|
*/
|
||||||
|
$skillcounter = 1;
|
||||||
foreach ($c->cData['skills']['skills'] as $k => $skill) {
|
foreach ($c->cData['skills']['skills'] as $k => $skill) {
|
||||||
$skills .= "<input style='width: 64px;' class='skill-$k skill' name='skills[$k]' type='number' min='0' max='99' value='{$skill['points']}'>: {$skill['skill']}<hr>";
|
$skills .= "<input cmd='skills' style='width: 64px;' class='skill-$k skill' name='$skillcounter' type='number' min='0' max='255' value='{$skill['points']}'>: {$skill['skill']}<hr>";
|
||||||
|
$skillcounter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// dump($c['Waypoints']);
|
// dump($c['Waypoints']);
|
||||||
$tabContent .= <<<EOT
|
$tabContent .= <<<EOT
|
||||||
<div style="background: white;" class="tab-pane fade" id="{$c->cData['CharacterName']}" role="tabpanel" aria-labelledby="{$c->cData['CharacterName']}-tab">
|
<div style="background: white; margin-top: 10px;" class="tab-pane fade" id="{$c->cData['CharacterName']}" role="tabpanel" aria-labelledby="{$c->cData['CharacterName']}-tab">
|
||||||
<form class="charform" method="POST" action="/saveCharacter.php">
|
<form id='{$c->cData['CharacterName']}' class="charform {$c->cData['CharacterName']}" method="POST" action="/saveCharacter.php">
|
||||||
<input type="hidden" name="filePath" id="filePath" value="{$c->cData['filePath']}">
|
<input type="hidden" name="filePath" id="filePath" value="{$c->cData['filePath']}">
|
||||||
<div class="container" style="font-size: 14px;">
|
<div class="container" style="font-size: 14px;">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h1 style="margin: 15px;">{$c->cData['CharacterName']}</h1>
|
<!--<input class="btn btn-danger" style="" type="submit" value="Save Character">
|
||||||
<input class="btn btn-danger" style="" type="submit" value="Save Character">
|
<img src="/img/chars/{$c->cData['CharacterClass']}.gif" style='height: 64px; width: 64px;'><br>-->
|
||||||
<img src="/img/chars/{$c->cData['CharacterClass']}.gif"><br>
|
Name: <input type="text" id="CharacterName" name="CharacterName" maxlength="16" value="{$c->cData['CharacterName']}" pattern="[A-Za-z0-9_-]+" title="Character name can only contain alphabets, numbers, underscore, and dash." data-oldvalue="{$c->cData['CharacterName']}">
|
||||||
<input type="text" id="ChracterName" name="ChracterName" maxlength="16" value="{$c->cData['CharacterName']}">
|
<br>
|
||||||
<select id='CharacterClass'>
|
<select id='CharacterClass' name='CharacterClass'>
|
||||||
$option
|
$option
|
||||||
</select>
|
</select><br>
|
||||||
Level: <input style="border: 1px solid black;width: 54px;" type="number" id="CharacterLevel" value="{$c->cData['CharacterLevel']}"><br>
|
Character Level: <input style="border: 1px solid black;width: 54px;" type="number" name="CharacterLevel" id="CharacterLevel" value="{$c->cData['CharacterLevel']}"><br>
|
||||||
$radio
|
<hr>
|
||||||
|
<!--Difficulty:<br>
|
||||||
|
$radio-->
|
||||||
|
<div>
|
||||||
|
<h3>Skills</h3>
|
||||||
$skills
|
$skills
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2>Quests</h2>
|
<h3>Quests</h3>
|
||||||
$quests
|
$quests
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2>Waypoints</h2>
|
<h3>Waypoints</h3>
|
||||||
$wps
|
$wps
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input class="btn btn-danger" style="" type="submit" value="Save Character">
|
<!--<input class="btn btn-danger" style="" type="submit" value="Save Character">-->
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
EOT;
|
EOT;
|
||||||
|
@ -179,7 +179,7 @@ $htmltop = <<<EOT
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gemtable td img {
|
.gemtable td img {
|
||||||
max-height: 128px;
|
max-height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
|
Loading…
Reference in New Issue
Block a user