Better navmesh, Added icons, Added PSXPlayer component
This commit is contained in:
31
Editor/PSXNavMeshEditor.cs
Normal file
31
Editor/PSXNavMeshEditor.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using SplashEdit.RuntimeCode;
|
||||
using System.Linq;
|
||||
|
||||
namespace SplashEdit.EditorCode
|
||||
{
|
||||
[CustomEditor(typeof(PSXNavMesh))]
|
||||
public class PSXNavMeshEditor : Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
PSXNavMesh comp = (PSXNavMesh)target;
|
||||
if (GUILayout.Button("Create preview"))
|
||||
{
|
||||
PSXSceneExporter exporter = FindObjectsByType<PSXSceneExporter>(FindObjectsSortMode.None).FirstOrDefault();
|
||||
if(exporter != null)
|
||||
{
|
||||
comp.CreateNavmesh(exporter.GTEScaling);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("No PSXSceneExporter found in the scene. We can't pull the GTE scaling from the exporter.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/PSXNavMeshEditor.cs.meta
Normal file
2
Editor/PSXNavMeshEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d3bd83aac4c3ce9ab1698a6a2bc735d
|
||||
BIN
Icons/PSXNavmesh.png
Normal file
BIN
Icons/PSXNavmesh.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
130
Icons/PSXNavmesh.png.meta
Normal file
130
Icons/PSXNavmesh.png.meta
Normal file
@@ -0,0 +1,130 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d695ef52da250cdcea6c30ab1122c56e
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Icons/PSXPlayer.png
Normal file
BIN
Icons/PSXPlayer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
130
Icons/PSXPlayer.png.meta
Normal file
130
Icons/PSXPlayer.png.meta
Normal file
@@ -0,0 +1,130 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d7bd095e76e6f3df976224b15405059
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -3,7 +3,6 @@ using Unity.AI.Navigation;
|
||||
using UnityEngine.AI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
|
||||
namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
public struct PSXNavMeshTri
|
||||
@@ -20,23 +19,35 @@ namespace SplashEdit.RuntimeCode
|
||||
public class PSXNavMesh : MonoBehaviour
|
||||
{
|
||||
|
||||
Mesh mesh;
|
||||
|
||||
[HideInInspector]
|
||||
public List<PSXNavMeshTri> Navmesh { get; set; }
|
||||
|
||||
public void CreateNavmesh(float GTEScaling)
|
||||
{
|
||||
mesh = new Mesh();
|
||||
Navmesh = new List<PSXNavMeshTri>();
|
||||
NavMeshSurface navMeshSurface = GetComponent<NavMeshSurface>();
|
||||
navMeshSurface.overrideTileSize = true;
|
||||
navMeshSurface.tileSize = 16;
|
||||
navMeshSurface.overrideVoxelSize = true;
|
||||
navMeshSurface.voxelSize = 0.1f;
|
||||
navMeshSurface.BuildNavMesh();
|
||||
NavMeshTriangulation triangulation = NavMesh.CalculateTriangulation();
|
||||
navMeshSurface.overrideTileSize = false;
|
||||
navMeshSurface.overrideVoxelSize = false;
|
||||
|
||||
int[] triangles = triangulation.indices;
|
||||
Vector3[] vertices = triangulation.vertices;
|
||||
|
||||
mesh.vertices = vertices;
|
||||
mesh.triangles = triangles;
|
||||
|
||||
mesh.RecalculateNormals();
|
||||
|
||||
for (int i = 0; i < triangles.Length; i += 3)
|
||||
{
|
||||
|
||||
|
||||
int vid0 = triangles[i];
|
||||
int vid1 = triangles[i + 1];
|
||||
int vid2 = triangles[i + 2];
|
||||
@@ -58,5 +69,27 @@ namespace SplashEdit.RuntimeCode
|
||||
Navmesh.Add(tri);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void OnDrawGizmos()
|
||||
{
|
||||
if (mesh == null) return;
|
||||
Gizmos.DrawMesh(mesh);
|
||||
Gizmos.color = Color.green;
|
||||
|
||||
var vertices = mesh.vertices;
|
||||
var triangles = mesh.triangles;
|
||||
|
||||
for (int i = 0; i < triangles.Length; i += 3)
|
||||
{
|
||||
Vector3 v0 = vertices[triangles[i]];
|
||||
Vector3 v1 = vertices[triangles[i + 1]];
|
||||
Vector3 v2 = vertices[triangles[i + 2]];
|
||||
|
||||
Gizmos.DrawLine(v0, v1);
|
||||
Gizmos.DrawLine(v1, v2);
|
||||
Gizmos.DrawLine(v2, v0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a2f8d45e1591de1e945b3b7bdfb123b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: d695ef52da250cdcea6c30ab1122c56e, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
30
Runtime/PSXPlayer.cs
Normal file
30
Runtime/PSXPlayer.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
|
||||
|
||||
namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
public class PSXPlayer : MonoBehaviour
|
||||
{
|
||||
public float PlayerHeight;
|
||||
|
||||
[HideInInspector]
|
||||
public Vector3 camPoint;
|
||||
float maxDistance = 1000f;
|
||||
|
||||
public void FindNavmesh()
|
||||
{
|
||||
NavMeshHit hit;
|
||||
if (NavMesh.SamplePosition(transform.position, out hit, maxDistance, NavMesh.AllAreas))
|
||||
{
|
||||
camPoint = hit.position + new Vector3(0, PlayerHeight, 0);
|
||||
}
|
||||
}
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
FindNavmesh();
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawSphere(camPoint, 0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/PSXPlayer.cs.meta
Normal file
11
Runtime/PSXPlayer.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dee32f3a19300d7a3aae7424f01c9332
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 4d7bd095e76e6f3df976224b15405059, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
@@ -26,6 +27,10 @@ namespace SplashEdit.RuntimeCode
|
||||
private bool verticalLayout;
|
||||
private List<ProhibitedArea> prohibitedAreas;
|
||||
|
||||
private Vector3 _playerPos;
|
||||
private Quaternion _playerRot;
|
||||
private float _playerHeight;
|
||||
|
||||
public void Export()
|
||||
{
|
||||
_psxData = DataStorage.LoadData(out selectedResolution, out dualBuffering, out verticalLayout, out prohibitedAreas);
|
||||
@@ -44,6 +49,21 @@ namespace SplashEdit.RuntimeCode
|
||||
}
|
||||
|
||||
PackTextures();
|
||||
|
||||
PSXPlayer player = FindObjectsByType<PSXPlayer>(FindObjectsSortMode.None).FirstOrDefault();
|
||||
if (player != null)
|
||||
{
|
||||
player.FindNavmesh();
|
||||
_playerPos = player.camPoint;
|
||||
_playerHeight = player.PlayerHeight;
|
||||
_playerRot = player.transform.rotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Can't export a scene without a Player created");
|
||||
return;
|
||||
}
|
||||
|
||||
ExportFile();
|
||||
}
|
||||
|
||||
@@ -103,14 +123,24 @@ namespace SplashEdit.RuntimeCode
|
||||
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)))
|
||||
{
|
||||
// Header
|
||||
writer.Write('S'); // 1 byte
|
||||
writer.Write('P'); // 1 byte
|
||||
writer.Write((ushort)1); // 2 bytes - version
|
||||
writer.Write((ushort)_exporters.Length); // 2 bytes
|
||||
writer.Write((ushort)_navmeshes.Length);
|
||||
writer.Write((ushort)_atlases.Length); // 2 bytes
|
||||
writer.Write((ushort)clutCount); // 2 bytes
|
||||
for (int i = 0; i < 2; i++) writer.Write((ushort)0);
|
||||
writer.Write('S'); // 1 byte // 1
|
||||
writer.Write('P'); // 1 byte // 2
|
||||
writer.Write((ushort)1); // 2 bytes - version // 4
|
||||
writer.Write((ushort)_exporters.Length); // 2 bytes // 6
|
||||
writer.Write((ushort)_navmeshes.Length); // 8
|
||||
writer.Write((ushort)_atlases.Length); // 2 bytes // 10
|
||||
writer.Write((ushort)clutCount); // 2 bytes // 12
|
||||
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(_playerPos.x, GTEScaling)); // 14
|
||||
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(-_playerPos.y, GTEScaling)); // 16
|
||||
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(_playerPos.z, GTEScaling)); // 18
|
||||
|
||||
writer.Write((ushort)PSXTrig.ConvertToFixed12(_playerRot.eulerAngles.x * Mathf.Deg2Rad)); // 20
|
||||
writer.Write((ushort)PSXTrig.ConvertToFixed12(_playerRot.eulerAngles.y * Mathf.Deg2Rad)); // 22
|
||||
writer.Write((ushort)PSXTrig.ConvertToFixed12(_playerRot.eulerAngles.z * Mathf.Deg2Rad)); // 24
|
||||
|
||||
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(_playerHeight, GTEScaling)); // 26
|
||||
|
||||
writer.Write((ushort)0);
|
||||
|
||||
// GameObject section (exporters)
|
||||
foreach (PSXObjectExporter exporter in _exporters)
|
||||
@@ -253,23 +283,25 @@ namespace SplashEdit.RuntimeCode
|
||||
}
|
||||
}
|
||||
|
||||
foreach (PSXNavMesh navmesh in _navmeshes) {
|
||||
foreach (PSXNavMesh navmesh in _navmeshes)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
long navmeshDataOffset = writer.BaseStream.Position;
|
||||
navmeshDataOffsets.Add(navmeshDataOffset);
|
||||
|
||||
foreach(PSXNavMeshTri tri in navmesh.Navmesh) {
|
||||
writer.Write((ushort) tri.v0.vx);
|
||||
writer.Write((ushort) tri.v0.vy);
|
||||
writer.Write((ushort) tri.v0.vz);
|
||||
foreach (PSXNavMeshTri tri in navmesh.Navmesh)
|
||||
{
|
||||
writer.Write((int)tri.v0.vx);
|
||||
writer.Write((int)tri.v0.vy);
|
||||
writer.Write((int)tri.v0.vz);
|
||||
|
||||
writer.Write((ushort) tri.v1.vx);
|
||||
writer.Write((ushort) tri.v1.vy);
|
||||
writer.Write((ushort) tri.v1.vz);
|
||||
writer.Write((int)tri.v1.vx);
|
||||
writer.Write((int)tri.v1.vy);
|
||||
writer.Write((int)tri.v1.vz);
|
||||
|
||||
writer.Write((ushort) tri.v2.vx);
|
||||
writer.Write((ushort) tri.v2.vy);
|
||||
writer.Write((ushort) tri.v2.vz);
|
||||
writer.Write((int)tri.v2.vx);
|
||||
writer.Write((int)tri.v2.vy);
|
||||
writer.Write((int)tri.v2.vz);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using UnityEditor;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using System.Linq;
|
||||
|
||||
namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
@@ -330,7 +331,8 @@ namespace SplashEdit.RuntimeCode
|
||||
}
|
||||
|
||||
|
||||
public static byte ColorUnityToPSX(float v) => (byte)(Mathf.Clamp(v*255, 0, 255));
|
||||
public static byte ColorUnityToPSX(float v) => (byte)(Mathf.Clamp(v * 255, 0, 255));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user