mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 21:00:26 +02:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
827eb0fc1d | ||
|
|
a482a5f5bb | ||
|
|
81f321f959 | ||
|
|
f97ff9219f | ||
|
|
85e79a067a | ||
|
|
effeda1b7c | ||
|
|
6299503191 | ||
|
|
c5d86c8d8b | ||
|
|
1c8e651bb1 |
No files matched your search
+333
-22
@@ -32,6 +32,12 @@ In the objective games a bot plays for the objective, fighting on the way:
|
||||
- king of the hill: to the hill, and around in it;
|
||||
- oddball: to the ball (taken as a weapon is), hunting whoever carries it;
|
||||
with it, away from the enemy, striking those who come near.
|
||||
|
||||
In a game of teams a bot plays with its team: it holds its fire while a
|
||||
teammate is in the line of it, takes on first the enemies near its
|
||||
teammates (people above all), escorts a teammate who carries the enemy's
|
||||
flag or the ball, and, with nothing to do, every other bot keeps near the
|
||||
nearest person on its team.
|
||||
*/
|
||||
|
||||
#include "cseries.h"
|
||||
@@ -111,6 +117,12 @@ struct bot
|
||||
real sidestep;
|
||||
/* whether it walks to a point this tick (not only weaving in a fight) */
|
||||
boolean walking;
|
||||
/* the paths to its goal not found in a row, and goals given up for a
|
||||
while on that account: following a person, a thing to pick up */
|
||||
short path_failures;
|
||||
long follow_again_time;
|
||||
long ignored_item_index;
|
||||
long ignored_item_time;
|
||||
real wander_yaw;
|
||||
short weave_phase;
|
||||
/* where it is going, and the way there */
|
||||
@@ -127,6 +139,8 @@ struct bot
|
||||
/* the objective, as it played it this tick: where it goes, whom it
|
||||
hunts first, and whether it carries the flag or ball (no firing) */
|
||||
boolean has_objective;
|
||||
/* the objective is a place to hold (the hill, its own flag's stand) */
|
||||
boolean holding;
|
||||
real_point3d objective;
|
||||
long objective_unit_index;
|
||||
boolean carrying;
|
||||
@@ -232,6 +246,124 @@ static boolean bot_can_see(
|
||||
from, &vector, unit_index, &collision);
|
||||
}
|
||||
|
||||
/* ---------- the team */
|
||||
|
||||
/* whether a player is a person (on a machine of its own, not a bot's) */
|
||||
static boolean bot_player_is_person(
|
||||
struct player_datum const *player)
|
||||
{
|
||||
return !network_game_server_is_bot_machine(player->network_player_data.machine_index);
|
||||
}
|
||||
|
||||
/* how much nearer an enemy at this point counts for being near the bot's
|
||||
teammates: up to 10 world units for one by a person, 6 by a bot */
|
||||
static real bot_threat_to_team(
|
||||
struct player_datum const *player,
|
||||
real_point3d const *enemy)
|
||||
{
|
||||
real threat = 0.0f;
|
||||
short index;
|
||||
|
||||
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
|
||||
{
|
||||
struct player_datum *mate = distributed_player(index);
|
||||
long unit_index;
|
||||
real_point3d head;
|
||||
real distance;
|
||||
real most;
|
||||
|
||||
if (!mate || mate == player || mate->team_index != player->team_index)
|
||||
continue;
|
||||
unit_index = distributed_living_unit(mate);
|
||||
if (unit_index == NONE)
|
||||
continue;
|
||||
unit_get_camera_position(unit_index, &head);
|
||||
distance = distance3d(enemy, &head);
|
||||
most = bot_player_is_person(mate) ? 10.0f : 6.0f;
|
||||
if (distance < 12.0f)
|
||||
threat = MAX(threat, most * (1.0f - distance / 12.0f));
|
||||
}
|
||||
return threat;
|
||||
}
|
||||
|
||||
/* whether a teammate stands in the line of fire to a point (nearer than
|
||||
it, within 0.8 world units of the line) */
|
||||
static boolean bot_teammate_in_line(
|
||||
struct player_datum const *player,
|
||||
real_point3d const *eye,
|
||||
real_point3d const *target)
|
||||
{
|
||||
real_vector3d line;
|
||||
real length;
|
||||
short index;
|
||||
|
||||
if (!game_engine_has_teams())
|
||||
return FALSE;
|
||||
vector_from_points3d(eye, target, &line);
|
||||
length = magnitude3d(&line);
|
||||
if (length < 0.01f)
|
||||
return FALSE;
|
||||
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
|
||||
{
|
||||
struct player_datum *mate = distributed_player(index);
|
||||
long unit_index;
|
||||
real_point3d center;
|
||||
real_vector3d offset;
|
||||
real along;
|
||||
real_point3d nearest;
|
||||
|
||||
if (!mate || mate == player || mate->team_index != player->team_index)
|
||||
continue;
|
||||
unit_index = distributed_living_unit(mate);
|
||||
if (unit_index == NONE)
|
||||
continue;
|
||||
unit_get_camera_position(unit_index, ¢er);
|
||||
center.z -= 0.25f;
|
||||
vector_from_points3d(eye, ¢er, &offset);
|
||||
along = dot_product3d(&offset, &line) / length;
|
||||
if (along <= 0.0f || along >= length)
|
||||
continue;
|
||||
nearest.x = eye->x + line.i * along / length;
|
||||
nearest.y = eye->y + line.j * along / length;
|
||||
nearest.z = eye->z + line.k * along / length;
|
||||
if (distance3d(&nearest, ¢er) < 0.8f)
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* the nearest person on the bot's team, alive (NONE: none) */
|
||||
static long bot_nearest_person(
|
||||
struct player_datum const *player,
|
||||
real_point3d const *position)
|
||||
{
|
||||
long best = NONE;
|
||||
real best_distance = 40.0f;
|
||||
short index;
|
||||
|
||||
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
|
||||
{
|
||||
struct player_datum *mate = distributed_player(index);
|
||||
long unit_index;
|
||||
real_point3d origin;
|
||||
real distance;
|
||||
|
||||
if (!mate || mate == player || mate->team_index != player->team_index || !bot_player_is_person(mate))
|
||||
continue;
|
||||
unit_index = distributed_living_unit(mate);
|
||||
if (unit_index == NONE)
|
||||
continue;
|
||||
object_get_origin(unit_index, &origin);
|
||||
distance = distance3d(position, &origin);
|
||||
if (distance < best_distance)
|
||||
{
|
||||
best_distance = distance;
|
||||
best = unit_index;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/* the nearest enemy unit the bot can see, or NONE */
|
||||
static long bot_find_target(
|
||||
struct player_datum const *player,
|
||||
@@ -256,6 +388,11 @@ static long bot_find_target(
|
||||
continue;
|
||||
unit_get_camera_position(unit_index, &head);
|
||||
distance = distance3d(eye, &head);
|
||||
if (distance >= bots.sight_range)
|
||||
continue;
|
||||
/* (one near a teammate counts as nearer: helped first) */
|
||||
if (teams)
|
||||
distance -= bot_threat_to_team(player, &head);
|
||||
if (distance < best_distance && bot_can_see(player->unit_index, eye, &head))
|
||||
{
|
||||
best = unit_index;
|
||||
@@ -299,6 +436,7 @@ static boolean bot_find_path(
|
||||
if (goal_surface == NONE)
|
||||
{
|
||||
bots.fail_goal++;
|
||||
bot->path_failures++;
|
||||
return FALSE;
|
||||
}
|
||||
/* (a little wider than a biped: paths that keep off walls and corners) */
|
||||
@@ -333,9 +471,11 @@ static boolean bot_find_path(
|
||||
bot->path.valid = FALSE;
|
||||
bots.paths_failed++;
|
||||
bots.fail_search++;
|
||||
bot->path_failures++;
|
||||
return FALSE;
|
||||
}
|
||||
bots.paths_found += bot->path.valid != FALSE;
|
||||
bot->path_failures = 0;
|
||||
return bot->path.valid;
|
||||
}
|
||||
|
||||
@@ -776,6 +916,88 @@ static boolean bots_ctf_base(
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* near a teammate who carries something, a few world units off on the bot's
|
||||
side of it (an escort, not on the carrier's heels) */
|
||||
static void bot_escort_point(
|
||||
real_point3d const *position,
|
||||
real_point3d const *carried,
|
||||
real_point3d *point)
|
||||
{
|
||||
real dx = position->x - carried->x;
|
||||
real dy = position->y - carried->y;
|
||||
real length = (real)sqrt(dx * dx + dy * dy);
|
||||
|
||||
*point = *carried;
|
||||
if (length > 0.01f)
|
||||
{
|
||||
point->x += dx / length * 2.5f;
|
||||
point->y += dy / length * 2.5f;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bots with one place to go spread out: each has a direction of its own
|
||||
(the golden angle on from the last bot's), a place of its own about a
|
||||
point to hold, and comes to a point from far off by a way of its own,
|
||||
wide of the others', going straight to it only for the last stretch. */
|
||||
|
||||
static real bot_spread_angle(
|
||||
long player_index)
|
||||
{
|
||||
return (real)DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) * 2.39996f;
|
||||
}
|
||||
|
||||
/* the bot's place about a point (radius: world units) */
|
||||
static void bot_spot_around(
|
||||
real_point3d const *center,
|
||||
long player_index,
|
||||
real radius,
|
||||
real_point3d *spot)
|
||||
{
|
||||
real angle = bot_spread_angle(player_index);
|
||||
|
||||
*spot = *center;
|
||||
spot->x += (real)cos(angle) * radius;
|
||||
spot->y += (real)sin(angle) * radius;
|
||||
}
|
||||
|
||||
/* its place about a point, or the point itself when there is no way to the
|
||||
place (in a wall, past a ledge) */
|
||||
static void bot_hold_spot(
|
||||
struct bot const *bot,
|
||||
real_point3d const *center,
|
||||
real radius,
|
||||
real_point3d *spot)
|
||||
{
|
||||
if (bot->path_failures >= 3)
|
||||
*spot = *center;
|
||||
else
|
||||
bot_spot_around(center, bot->player_index, radius, spot);
|
||||
}
|
||||
|
||||
/* where it heads for a point from where it is: from far off, out to a side
|
||||
of the straight way (its own side and width), in from 10 world units */
|
||||
static void bot_approach(
|
||||
struct bot const *bot,
|
||||
real_point3d const *position,
|
||||
real_point3d const *goal,
|
||||
real_point3d *point)
|
||||
{
|
||||
long index = DATUM_INDEX_TO_ABSOLUTE_INDEX(bot->player_index);
|
||||
real dx = goal->x - position->x;
|
||||
real dy = goal->y - position->y;
|
||||
real distance = (real)sqrt(dx * dx + dy * dy);
|
||||
real side = (index & 1 ? 1.0f : -1.0f) * (3.0f + (real)((index >> 1) % 3) * 1.5f);
|
||||
real share;
|
||||
|
||||
*point = *goal;
|
||||
/* (no way to the side: straight) */
|
||||
if (distance < 10.0f || bot->path_failures >= 2)
|
||||
return;
|
||||
share = MIN((distance - 10.0f) / 10.0f, 1.0f);
|
||||
point->x += -dy / distance * side * share;
|
||||
point->y += dx / distance * side * share;
|
||||
}
|
||||
|
||||
/* what the objective asks of the bot now (bot->has_objective and the rest) */
|
||||
static void bot_play_objective_now(
|
||||
struct bot *bot,
|
||||
@@ -809,6 +1031,7 @@ static void bot_play_objective_now(
|
||||
short index;
|
||||
|
||||
bot->has_objective = FALSE;
|
||||
bot->holding = FALSE;
|
||||
bot->objective_unit_index = NONE;
|
||||
bot->carrying = FALSE;
|
||||
if (type == game_engine_king)
|
||||
@@ -823,13 +1046,12 @@ static void bot_play_objective_now(
|
||||
dx = center.x - position->x;
|
||||
dy = center.y - position->y;
|
||||
bot->has_objective = TRUE;
|
||||
bot->objective = center;
|
||||
/* (on the hill: about in it, not on its middle) */
|
||||
if (dx * dx + dy * dy < 1.5f * 1.5f)
|
||||
{
|
||||
bot->objective.x += (real)cos(bot->weave_phase * 0.05f) * 1.0f;
|
||||
bot->objective.y += (real)sin(bot->weave_phase * 0.05f) * 1.0f;
|
||||
}
|
||||
bot->holding = TRUE;
|
||||
/* (near: a place of its own on the hill; far: a way of its own) */
|
||||
if (dx * dx + dy * dy < 6.0f * 6.0f)
|
||||
bot_hold_spot(bot, ¢er, 1.2f, &bot->objective);
|
||||
else
|
||||
bot_approach(bot, position, ¢er, &bot->objective);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -876,23 +1098,42 @@ static void bot_play_objective_now(
|
||||
bot->objective = own->position;
|
||||
return;
|
||||
}
|
||||
/* (every third guards its own) */
|
||||
/* (every third guards its own: about the stand, or, every other one
|
||||
of them, ahead of it towards the enemy's) */
|
||||
if ((DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) % 3) == 2 && bots_ctf_base(player->team_index, &base))
|
||||
{
|
||||
real_point3d enemy_base;
|
||||
|
||||
bot->has_objective = TRUE;
|
||||
bot->objective = base;
|
||||
if (distance3d(position, &base) < 4.0f)
|
||||
bot->holding = TRUE;
|
||||
if (((DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) / 3) & 1) &&
|
||||
bots_ctf_base((player->team_index + 1) % 2, &enemy_base))
|
||||
{
|
||||
bot->objective.x += (real)cos(bot->weave_phase * 0.03f) * 3.0f;
|
||||
bot->objective.y += (real)sin(bot->weave_phase * 0.03f) * 3.0f;
|
||||
real dx = enemy_base.x - base.x;
|
||||
real dy = enemy_base.y - base.y;
|
||||
real length = (real)sqrt(dx * dx + dy * dy);
|
||||
|
||||
bot->objective = base;
|
||||
if (length > 0.01f)
|
||||
{
|
||||
bot->objective.x += dx / length * 7.0f;
|
||||
bot->objective.y += dy / length * 7.0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bot_hold_spot(bot, &base, 3.0f, &bot->objective);
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* theirs: to take it, or to cover its carrier */
|
||||
/* theirs: to take it, or to escort the teammate carrying it */
|
||||
if (enemy)
|
||||
{
|
||||
bot->has_objective = TRUE;
|
||||
bot->objective = enemy->position;
|
||||
if (enemy->carrier_player_index != NONE)
|
||||
bot_hold_spot(bot, &enemy->position, 3.0f, &bot->objective);
|
||||
else
|
||||
bot_approach(bot, position, &enemy->position, &bot->objective);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -911,13 +1152,20 @@ static void bot_play_objective_now(
|
||||
bot->carrying = TRUE;
|
||||
return;
|
||||
}
|
||||
/* (one a teammate carries: not to be chased) */
|
||||
/* (one a teammate carries: escorted, not chased) */
|
||||
if (item->carrier_player_index != NONE && game_engine_has_teams())
|
||||
{
|
||||
struct player_datum *carrier = player_try_and_get(item->carrier_player_index);
|
||||
|
||||
if (carrier && carrier->team_index == player->team_index)
|
||||
{
|
||||
if (!nearest)
|
||||
{
|
||||
bot->has_objective = TRUE;
|
||||
bot_hold_spot(bot, &item->position, 3.0f, &bot->objective);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
distance = distance3d(position, &item->position);
|
||||
if (distance < nearest_distance)
|
||||
@@ -929,7 +1177,7 @@ static void bot_play_objective_now(
|
||||
if (nearest)
|
||||
{
|
||||
bot->has_objective = TRUE;
|
||||
bot->objective = nearest->position;
|
||||
bot_approach(bot, position, &nearest->position, &bot->objective);
|
||||
if (nearest->carrier_player_index != NONE)
|
||||
{
|
||||
struct player_datum *carrier = player_try_and_get(nearest->carrier_player_index);
|
||||
@@ -976,6 +1224,7 @@ static void bot_think(
|
||||
real wanted_yaw;
|
||||
real wanted_pitch = 0.0f;
|
||||
boolean firing = FALSE;
|
||||
boolean following = FALSE;
|
||||
real moved;
|
||||
|
||||
csmemset(action, 0, sizeof(*action));
|
||||
@@ -1097,13 +1346,13 @@ static void bot_think(
|
||||
}
|
||||
bot->weave_phase++;
|
||||
/* (a good weapon or powerup close by: taken in the fight too) */
|
||||
if (!bot->carrying && (game_time_get() - bot->item_look_time >= ITEM_LOOK_TICKS / 2 ||
|
||||
if (!bot->carrying && !bot->has_objective && (game_time_get() - bot->item_look_time >= ITEM_LOOK_TICKS / 2 ||
|
||||
game_time_get() < bot->item_look_time))
|
||||
{
|
||||
bot->item_look_time = game_time_get();
|
||||
bot->item_index = bot_find_item(unit, &position, 10.0f, 4.0f);
|
||||
}
|
||||
if (bot->item_index != NONE && !bot->carrying && object_try_and_get(bot->item_index) &&
|
||||
if (bot->item_index != NONE && !bot->carrying && !bot->has_objective && object_try_and_get(bot->item_index) &&
|
||||
bot_item_worth(unit, bot->item_index) > 0.0f)
|
||||
{
|
||||
real_point3d item_position;
|
||||
@@ -1153,6 +1402,13 @@ static void bot_think(
|
||||
firing = !bot->carrying && distance < BOT_FIRE_RANGE &&
|
||||
fabsf(bot_angle_difference(wanted_yaw, bot->yaw)) < BOT_FIRE_CONE &&
|
||||
fabsf(wanted_pitch - bot->pitch) < BOT_FIRE_CONE;
|
||||
/* (a teammate in the line of fire: held, and a step aside for a
|
||||
clear shot) */
|
||||
if (firing && bot_teammate_in_line(player, &eye, &head))
|
||||
{
|
||||
firing = FALSE;
|
||||
action->throttle.j = (bot->weave_phase / 30) & 1 ? 1.0f : -1.0f;
|
||||
}
|
||||
/* (a melee blow when it is upon it) */
|
||||
if (distance < 1.0f)
|
||||
SET_FLAG(action->control_flags, _unit_control_use_equipment_bit, TRUE);
|
||||
@@ -1201,6 +1457,8 @@ static void bot_think(
|
||||
long item = bot_find_item(unit, &position, BOT_ITEM_RANGE, 0.0f);
|
||||
|
||||
bot->item_look_time = game_time_get();
|
||||
if (item == bot->ignored_item_index && game_time_get() - bot->ignored_item_time < 600)
|
||||
item = NONE;
|
||||
if (item != bot->item_index)
|
||||
{
|
||||
bot->item_index = item;
|
||||
@@ -1215,8 +1473,16 @@ static void bot_think(
|
||||
}
|
||||
if (bot->item_index != NONE && !bot->has_objective)
|
||||
{
|
||||
/* (taken or gone: on) */
|
||||
if (!object_try_and_get(bot->item_index) || bot_item_worth(unit, bot->item_index) <= 0.0f)
|
||||
/* (taken or gone, or no way to it: on) */
|
||||
if (bot->path_failures >= 3)
|
||||
{
|
||||
bot->ignored_item_index = bot->item_index;
|
||||
bot->ignored_item_time = game_time_get();
|
||||
bot->path_failures = 0;
|
||||
bot->item_index = NONE;
|
||||
bot->has_goal = FALSE;
|
||||
}
|
||||
else if (!object_try_and_get(bot->item_index) || bot_item_worth(unit, bot->item_index) <= 0.0f)
|
||||
{
|
||||
bot->item_index = NONE;
|
||||
bot->has_goal = FALSE;
|
||||
@@ -1229,12 +1495,56 @@ static void bot_think(
|
||||
bot_take_item(bot, player, unit, action);
|
||||
}
|
||||
}
|
||||
/* (in a team, every other bot with nothing to do keeps near the
|
||||
nearest person on its team) */
|
||||
following = FALSE;
|
||||
if (bot->item_index == NONE && !bot->has_objective && game_engine_has_teams() &&
|
||||
(DATUM_INDEX_TO_ABSOLUTE_INDEX(bot->player_index) & 1) == 0 &&
|
||||
game_time_get() >= bot->follow_again_time)
|
||||
{
|
||||
long person = bot_nearest_person(player, &position);
|
||||
|
||||
if (person != NONE)
|
||||
{
|
||||
real_point3d origin;
|
||||
|
||||
following = TRUE;
|
||||
object_get_origin(person, &origin);
|
||||
/* (near: where it is, not on top of them) */
|
||||
if (distance3d(&position, &origin) > 6.0f)
|
||||
bot_escort_point(&position, &origin, &bot->goal);
|
||||
else
|
||||
bot->goal = position;
|
||||
bot->has_goal = TRUE;
|
||||
bot->goal_time = game_time_get();
|
||||
/* (no way to them, as up where only a teleporter goes: not for
|
||||
ten seconds) */
|
||||
if (bot->path_failures >= 3)
|
||||
{
|
||||
bot->follow_again_time = game_time_get() + 300;
|
||||
bot->path_failures = 0;
|
||||
following = FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* hunts, or roams: along its way, looking where it goes */
|
||||
if (bot->item_index == NONE && !bot->has_objective &&
|
||||
(!bot->has_goal || game_time_get() - bot->goal_time > 90 ||
|
||||
if (bot->item_index == NONE && !bot->has_objective && !following &&
|
||||
(!bot->has_goal || game_time_get() - bot->goal_time > 90 || bot->path_failures >= 3 ||
|
||||
distance3d(&position, &bot->goal) < BOT_GOAL_RADIUS))
|
||||
{
|
||||
boolean unreachable = bot->path_failures >= 3;
|
||||
|
||||
bot_choose_goal(bot, player, &position);
|
||||
/* (no way to the last: somewhere near at random instead) */
|
||||
if (unreachable)
|
||||
{
|
||||
real angle = bot_random() * 2.0f * _pi;
|
||||
|
||||
bot->goal = position;
|
||||
bot->goal.x += (real)cos(angle) * 6.0f;
|
||||
bot->goal.y += (real)sin(angle) * 6.0f;
|
||||
}
|
||||
bot->path_failures = 0;
|
||||
}
|
||||
wanted_yaw = bot->wander_yaw;
|
||||
if (bot_next_point(bot, player->unit_index, &position, &point))
|
||||
@@ -1363,6 +1673,7 @@ static struct bot *bot_get(
|
||||
free_bot->player_index = player_index;
|
||||
free_bot->target_unit_index = NONE;
|
||||
free_bot->item_index = NONE;
|
||||
free_bot->ignored_item_index = NONE;
|
||||
}
|
||||
return free_bot;
|
||||
}
|
||||
|
||||
@@ -140,6 +140,12 @@ out (halo_vr_scope_pass). */
|
||||
int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3],
|
||||
float *vertical_field_of_view);
|
||||
int halo_vr_scope_pass(void);
|
||||
/* The menus' pointer (d3d8_gl.c's halo_ui_pointer_update): where a
|
||||
controller points on the menus' screen, in their 640x480, whether that
|
||||
moved since asked, and the trigger's clicks since (nonzero while it points
|
||||
at the screen); and where it points this frame, to draw it (render.c) */
|
||||
int halo_vr_menu_pointer(int *x, int *y, int *click_x, int *click_y, int *moved, int *clicks);
|
||||
int halo_vr_menu_pointer_at(int *x, int *y);
|
||||
/* Players' names over their heads (hud.c, render.c): whether they are drawn
|
||||
apart this frame; the slot a name is drawn into (left, top, right, bottom
|
||||
of the game's 640x480); a name's place (world units, over the head),
|
||||
|
||||
@@ -1365,11 +1365,21 @@ HRESULT WINAPI Direct3D_CreateDevice(UINT adapter, D3DDEVTYPE device_type, void
|
||||
/* ---------- the menus' pointer */
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* the controller pointing at the menus' screen in VR (halo_vr_menu_pointer) */
|
||||
int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer)
|
||||
{
|
||||
(void)menus_active;
|
||||
(void)pointer;
|
||||
return 0;
|
||||
int x, y, click_x, click_y, moved, clicks;
|
||||
|
||||
if (!menus_active || !halo_vr_menu_pointer(&x, &y, &click_x, &click_y, &moved, &clicks))
|
||||
return 0;
|
||||
memset(pointer, 0, sizeof(*pointer));
|
||||
pointer->x = (short)(x - (halo_screen_width() - 640) / 2);
|
||||
pointer->y = (short)y;
|
||||
pointer->click_x = (short)(click_x - (halo_screen_width() - 640) / 2);
|
||||
pointer->click_y = (short)click_y;
|
||||
pointer->moved = (unsigned char)(moved != 0);
|
||||
pointer->left_clicks = (unsigned char)(clicks < 255 ? clicks : 255);
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
/* a point in the window, as SDL reports it, in the menus' coordinates: the
|
||||
|
||||
@@ -1969,6 +1969,133 @@ static int crosshair_pose(struct halo_xr_pose *pose, float *size)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---------- the menus' pointer
|
||||
|
||||
In the menus (the screen fixed in the room), a controller points at the
|
||||
screen and its trigger clicks, as the desktop builds' mouse does
|
||||
(halo_ui_pointer.h): where its aim's line meets the screen, in the menus'
|
||||
640x480. The hand whose trigger was pressed last points; a click gives a
|
||||
short tick of the hand's rumble. */
|
||||
|
||||
static struct
|
||||
{
|
||||
int valid;
|
||||
float x, y;
|
||||
float reported_x, reported_y;
|
||||
int hand;
|
||||
int trigger_down[HALO_XR_HAND_COUNT];
|
||||
int clicks;
|
||||
/* a click is taken where it pointed as the trigger began to move
|
||||
(pulling it through tips the controller) */
|
||||
int pulling[HALO_XR_HAND_COUNT];
|
||||
float pull_x[HALO_XR_HAND_COUNT], pull_y[HALO_XR_HAND_COUNT];
|
||||
float click_x, click_y;
|
||||
} menu_pointer;
|
||||
|
||||
static void menu_pointer_update(void)
|
||||
{
|
||||
static const float forward[3] = { 0.0f, 0.0f, -1.0f };
|
||||
const struct halo_xr_hand *hand;
|
||||
float direction[3], t, hit_x, hit_y, width, height;
|
||||
int index;
|
||||
|
||||
menu_pointer.valid = 0;
|
||||
if (!vr.active || vr.last_overlay_mode != HALO_XR_OVERLAY_SCREEN || !vr.frame.focused)
|
||||
{
|
||||
menu_pointer.trigger_down[0] = menu_pointer.trigger_down[1] = 0;
|
||||
menu_pointer.pulling[0] = menu_pointer.pulling[1] = 0;
|
||||
return;
|
||||
}
|
||||
/* (the hand that clicked last points; a click counts as it goes down) */
|
||||
for (index = 0; index < HALO_XR_HAND_COUNT; index++)
|
||||
{
|
||||
int down = vr.frame.hands[index].active && (vr.frame.hands[index].buttons & HALO_XR_BUTTON_TRIGGER);
|
||||
|
||||
if (down && !menu_pointer.trigger_down[index])
|
||||
{
|
||||
menu_pointer.hand = index;
|
||||
menu_pointer.clicks++;
|
||||
}
|
||||
menu_pointer.trigger_down[index] = down;
|
||||
}
|
||||
hand = &vr.frame.hands[menu_pointer.hand];
|
||||
if (!hand->active || !(hand->flags & HALO_XR_POSE_VALID))
|
||||
{
|
||||
hand = &vr.frame.hands[1 - menu_pointer.hand];
|
||||
if (!hand->active || !(hand->flags & HALO_XR_POSE_VALID))
|
||||
return;
|
||||
}
|
||||
quaternion_rotate(hand->aim.orientation, forward, direction);
|
||||
if (direction[2] > -1e-3f)
|
||||
return;
|
||||
t = (-vr.screen_distance - hand->aim.position[2]) / direction[2];
|
||||
if (t <= 0.0f)
|
||||
return;
|
||||
hit_x = hand->aim.position[0] + direction[0] * t;
|
||||
hit_y = hand->aim.position[1] + direction[1] * t;
|
||||
width = vr.screen_width;
|
||||
height = vr.screen_width * 0.75f;
|
||||
menu_pointer.x = (hit_x / width + 0.5f) * 640.0f;
|
||||
menu_pointer.y = (0.5f - hit_y / height) * 480.0f;
|
||||
menu_pointer.valid = menu_pointer.x >= 0.0f && menu_pointer.x < 640.0f && menu_pointer.y >= 0.0f &&
|
||||
menu_pointer.y < 480.0f;
|
||||
/* (the click where the trigger began to move, if it pointed then) */
|
||||
{
|
||||
int pulling = vr.frame.hands[menu_pointer.hand].trigger > 0.1f;
|
||||
|
||||
if (pulling && !menu_pointer.pulling[menu_pointer.hand] && menu_pointer.valid)
|
||||
{
|
||||
menu_pointer.pull_x[menu_pointer.hand] = menu_pointer.x;
|
||||
menu_pointer.pull_y[menu_pointer.hand] = menu_pointer.y;
|
||||
}
|
||||
if (!pulling || !menu_pointer.pulling[menu_pointer.hand])
|
||||
{
|
||||
menu_pointer.click_x = menu_pointer.x;
|
||||
menu_pointer.click_y = menu_pointer.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
menu_pointer.click_x = menu_pointer.pull_x[menu_pointer.hand];
|
||||
menu_pointer.click_y = menu_pointer.pull_y[menu_pointer.hand];
|
||||
}
|
||||
if (!pulling || menu_pointer.valid)
|
||||
menu_pointer.pulling[menu_pointer.hand] = pulling;
|
||||
}
|
||||
}
|
||||
|
||||
int halo_vr_menu_pointer(int *x, int *y, int *click_x, int *click_y, int *moved, int *clicks)
|
||||
{
|
||||
menu_pointer_update();
|
||||
*click_x = (int)menu_pointer.click_x;
|
||||
*click_y = (int)menu_pointer.click_y;
|
||||
*clicks = menu_pointer.clicks;
|
||||
if (menu_pointer.clicks)
|
||||
host_xr_haptic(menu_pointer.hand, 0.3f, 0.02f);
|
||||
menu_pointer.clicks = 0;
|
||||
if (!menu_pointer.valid)
|
||||
return 0;
|
||||
*x = (int)menu_pointer.x;
|
||||
*y = (int)menu_pointer.y;
|
||||
/* (moved past a hand's tremor: a few pixels) */
|
||||
*moved = fabsf(menu_pointer.x - menu_pointer.reported_x) >= 3.0f ||
|
||||
fabsf(menu_pointer.y - menu_pointer.reported_y) >= 3.0f;
|
||||
if (*moved)
|
||||
{
|
||||
menu_pointer.reported_x = menu_pointer.x;
|
||||
menu_pointer.reported_y = menu_pointer.y;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int halo_vr_menu_pointer_at(int *x, int *y)
|
||||
{
|
||||
if (!menu_pointer.valid)
|
||||
return 0;
|
||||
*x = (int)menu_pointer.x;
|
||||
*y = (int)menu_pointer.y;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---------- players' names
|
||||
|
||||
Names over the heads (hud.c) are drawn into slots of a region of the back
|
||||
@@ -2273,6 +2400,12 @@ int halo_vr_scope(short local_player_index, float magnification, float position[
|
||||
return 0;
|
||||
}
|
||||
int halo_vr_scope_pass(void) { return 0; }
|
||||
int halo_vr_menu_pointer(int *x, int *y, int *click_x, int *click_y, int *moved, int *clicks)
|
||||
{
|
||||
(void)x; (void)y; (void)click_x; (void)click_y; (void)moved; *clicks = 0;
|
||||
return 0;
|
||||
}
|
||||
int halo_vr_menu_pointer_at(int *x, int *y) { (void)x; (void)y; return 0; }
|
||||
int halo_vr_names_apart(void) { return 0; }
|
||||
void halo_vr_name_slot(int slot, short bounds[4]) { (void)slot; (void)bounds; }
|
||||
int halo_vr_add_name(const float point[3]) { (void)point; return -1; }
|
||||
|
||||
@@ -573,9 +573,10 @@ static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
|
||||
if (left->buttons & HALO_XR_BUTTON_MENU) pad->wButtons |= XINPUT_GAMEPAD_START;
|
||||
if (left->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_LEFT_THUMB;
|
||||
if (right->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_RIGHT_THUMB;
|
||||
if ((BYTE)(right->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER])
|
||||
/* (in the menus the triggers click the pointer: halo_vr_menu_pointer) */
|
||||
if (!menus && (BYTE)(right->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER])
|
||||
pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = (BYTE)(right->trigger * 255.0f);
|
||||
if ((BYTE)(left->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER])
|
||||
if (!menus && (BYTE)(left->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER])
|
||||
pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER] = (BYTE)(left->trigger * 255.0f);
|
||||
if (menus)
|
||||
{
|
||||
|
||||
@@ -677,6 +677,9 @@ struct widget_instance;
|
||||
#include "text/text_group.h"
|
||||
#include "text/unicode.h"
|
||||
#include "ui_widget.h"
|
||||
#ifdef HALO_ANDROID
|
||||
#include "halo_vr.h"
|
||||
#endif
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
@@ -5269,6 +5272,8 @@ struct ui_mouse_target
|
||||
rectangle2d bounds;
|
||||
short kind;
|
||||
short button_index;
|
||||
/* (a button's: where its icon begins) */
|
||||
short icon_x0;
|
||||
};
|
||||
|
||||
static struct ui_mouse_target ui_mouse_targets[UI_MOUSE_MAXIMUM_TARGETS];
|
||||
@@ -5403,6 +5408,7 @@ static void ui_mouse_note_target(
|
||||
rectangle2d bounds = definition->bounds;
|
||||
short button_index;
|
||||
short kind;
|
||||
short icon_x0;
|
||||
|
||||
if (!ui_mouse_noting_targets ||
|
||||
ui_mouse_target_count >= UI_MOUSE_MAXIMUM_TARGETS ||
|
||||
@@ -5415,13 +5421,22 @@ static void ui_mouse_note_target(
|
||||
bounds.y0 += offset.y;
|
||||
bounds.y1 += offset.y;
|
||||
button_index = ui_mouse_key_button(widget);
|
||||
icon_x0 = bounds.x0;
|
||||
if (button_index != NONE)
|
||||
{
|
||||
/* the icon and its label: the text beginning just right of it */
|
||||
struct widget_instance *sibling;
|
||||
rectangle2d label_bounds;
|
||||
short best_distance = 17;
|
||||
#ifdef HALO_ANDROID
|
||||
/* (VR: a controller points less steadily than a mouse: the label a
|
||||
little further off is the button's too) */
|
||||
short most_distance = halo_vr_active() ? 25 : 17;
|
||||
#else
|
||||
short most_distance = 17;
|
||||
#endif
|
||||
|
||||
best_distance = most_distance;
|
||||
kind = _ui_mouse_target_button;
|
||||
for (sibling = parent ? parent->child : NULL; sibling; sibling = sibling->next)
|
||||
{
|
||||
@@ -5445,12 +5460,23 @@ static void ui_mouse_note_target(
|
||||
label_bounds = sibling_bounds;
|
||||
}
|
||||
}
|
||||
if (best_distance < 17)
|
||||
if (best_distance < most_distance)
|
||||
{
|
||||
bounds.x1 = MAX(bounds.x1, label_bounds.x1);
|
||||
bounds.y0 = MIN(bounds.y0, label_bounds.y0);
|
||||
bounds.y1 = MAX(bounds.y1, label_bounds.y1);
|
||||
}
|
||||
#ifdef HALO_ANDROID
|
||||
/* (and a margin about it: above and below, little to the sides,
|
||||
where the next key is) */
|
||||
if (halo_vr_active())
|
||||
{
|
||||
bounds.x0 -= 2;
|
||||
bounds.x1 += 2;
|
||||
bounds.y0 -= 4;
|
||||
bounds.y1 += 4;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else if (parent && ui_mouse_list_shows_several(parent))
|
||||
{
|
||||
@@ -5482,6 +5508,7 @@ static void ui_mouse_note_target(
|
||||
target->bounds = bounds;
|
||||
target->kind = kind;
|
||||
target->button_index = button_index;
|
||||
target->icon_x0 = icon_x0;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -5492,16 +5519,33 @@ static struct ui_mouse_target *ui_mouse_target_at(
|
||||
short y)
|
||||
{
|
||||
long index;
|
||||
struct ui_mouse_target *button = NULL;
|
||||
|
||||
for (index = ui_mouse_target_count - 1; index >= 0; index--)
|
||||
{
|
||||
rectangle2d const *bounds = &ui_mouse_targets[index].bounds;
|
||||
struct ui_mouse_target *target = &ui_mouse_targets[index];
|
||||
rectangle2d const *bounds = &target->bounds;
|
||||
|
||||
if (x >= bounds->x0 && x < bounds->x1 && y >= bounds->y0 && y < bounds->y1)
|
||||
return &ui_mouse_targets[index];
|
||||
if (!(x >= bounds->x0 && x < bounds->x1 && y >= bounds->y0 && y < bounds->y1))
|
||||
continue;
|
||||
/* port: a key's label's text box can be far wider than its text, over
|
||||
the next key: of keys under the point, the one whose icon is nearest
|
||||
on its left (each key from its icon to the next one's) */
|
||||
if (target->kind == _ui_mouse_target_button)
|
||||
{
|
||||
if (!button ||
|
||||
(target->icon_x0 <= x + 2 && (button->icon_x0 > x + 2 || target->icon_x0 > button->icon_x0)))
|
||||
{
|
||||
button = target;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!button)
|
||||
return target;
|
||||
break;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
return button;
|
||||
}
|
||||
|
||||
static boolean ui_mouse_widget_has_focus(
|
||||
@@ -5692,6 +5736,8 @@ enum
|
||||
MAXIMUM_SHOWN_PUBLIC_GAMES = 32,
|
||||
/* the online games panel's rows: "enter a code" and the games */
|
||||
ONLINE_PANEL_ROWS = 11,
|
||||
/* (the rows before the games: "enter a code") */
|
||||
ONLINE_PANEL_FIXED_ROWS = 1,
|
||||
};
|
||||
|
||||
static struct
|
||||
@@ -5932,6 +5978,48 @@ static boolean ui_join_code_take_event(
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* the pointer over the VR port's panels: the row under it chosen, and a
|
||||
click on it as A (TRUE: a panel is open and took it) */
|
||||
static boolean ui_join_code_take_pointer(
|
||||
struct halo_ui_pointer const *pointer)
|
||||
{
|
||||
struct event_record press;
|
||||
short row;
|
||||
/* (a click where it was clicked, which may be a moment before) */
|
||||
short x = pointer->left_clicks ? pointer->click_x : pointer->x;
|
||||
short y = pointer->left_clicks ? pointer->click_y : pointer->y;
|
||||
|
||||
csmemset(&press, 0, sizeof(press));
|
||||
press.type = _event_type_button;
|
||||
press.data.button.index = _gamepad_analog_button_a;
|
||||
press.data.button.value = 1;
|
||||
if (ui_join_code_globals.panel_open)
|
||||
{
|
||||
if (x >= 48 && x < 592 && y >= 128 && y < 128 + ONLINE_PANEL_ROWS * 22)
|
||||
{
|
||||
row = (short)(ui_join_code_globals.scroll + (y - 128) / 22);
|
||||
if (row < ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
|
||||
{
|
||||
ui_join_code_globals.selection = row;
|
||||
if (pointer->left_clicks)
|
||||
ui_join_code_panel_event(&press);
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
if (ui_join_code_globals.host_panel_open)
|
||||
{
|
||||
if (x >= 148 && x < 492 && y >= 187 && y < 187 + HOST_PANEL_ROWS * 30)
|
||||
{
|
||||
ui_join_code_globals.host_selection = (short)((y - 187) / 30);
|
||||
if (pointer->left_clicks)
|
||||
ui_join_code_host_panel_event(&press);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* the keyboard closed: the code typed, looked up */
|
||||
static void ui_join_code_update(
|
||||
void)
|
||||
@@ -6186,8 +6274,9 @@ static void ui_widgets_process_mouse(
|
||||
struct ui_mouse_target *target;
|
||||
short controller_index = 0;
|
||||
|
||||
if (!halo_ui_pointer_update(ui_mouse_menus_active(), &pointer) ||
|
||||
virtual_keyboard_active())
|
||||
/* (the keyboard takes the pointer itself: virtual_keyboard.c) */
|
||||
if (virtual_keyboard_active() ||
|
||||
!halo_ui_pointer_update(ui_mouse_menus_active(), &pointer))
|
||||
{
|
||||
ui_mouse_press_count = 0;
|
||||
ui_mouse_hover_pending = FALSE;
|
||||
@@ -6195,6 +6284,11 @@ static void ui_widgets_process_mouse(
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
/* (the VR port's panels over the menus take the pointer themselves) */
|
||||
if (ui_join_code_take_pointer(&pointer))
|
||||
return;
|
||||
#endif
|
||||
if (pointer.moved)
|
||||
{
|
||||
ui_mouse_hover_pending = TRUE;
|
||||
|
||||
@@ -1166,6 +1166,25 @@ static boolean virtual_keyboard_select(
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* the key drawn at a point of the menus' 640x480, or NONE */
|
||||
static short virtual_keyboard_key_at(
|
||||
short x,
|
||||
short y)
|
||||
{
|
||||
short key;
|
||||
|
||||
for (key = 0; key < NUMBER_OF_VIRTUAL_KEYS; key++)
|
||||
{
|
||||
rectangle2d const *bounds = &keyboard_rect[key];
|
||||
|
||||
if (x >= bounds->x0 && x < bounds->x1 && y >= bounds->y0 && y < bounds->y1)
|
||||
return key;
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void virtual_keyboard_process_internal(
|
||||
void)
|
||||
{
|
||||
@@ -1304,6 +1323,56 @@ static void virtual_keyboard_process_internal(
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* the VR port's: the controller's pointer (halo_ui_pointer.h) picks the
|
||||
key under it, and its click types it */
|
||||
{
|
||||
struct halo_ui_pointer pointer;
|
||||
|
||||
if (halo_ui_pointer_update(TRUE, &pointer))
|
||||
{
|
||||
short x = pointer.left_clicks ? pointer.click_x : pointer.x;
|
||||
short y = pointer.left_clicks ? pointer.click_y : pointer.y;
|
||||
short key = virtual_keyboard_key_at(x, y);
|
||||
|
||||
if (key != NONE && (pointer.moved || pointer.left_clicks))
|
||||
{
|
||||
short row, column;
|
||||
boolean found = FALSE;
|
||||
|
||||
for (row = 0; row < VIRTUAL_KEYBOARD_ROW_COUNT && !found; row++)
|
||||
{
|
||||
for (column = 0; column < VIRTUAL_KEYBOARD_COLUMN_COUNT && !found; column++)
|
||||
{
|
||||
if (virtual_keyboard_layout_table[row][column] == key)
|
||||
{
|
||||
virtual_keyboard_globals.row = row;
|
||||
virtual_keyboard_globals.column = column;
|
||||
found = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pointer.left_clicks && key != NONE)
|
||||
action = _event_key_select;
|
||||
/* (the keys' hints under the keyboard, in its picture: B = BACK
|
||||
cancels, A = ENTER finishes, as Done does) */
|
||||
if (pointer.left_clicks && key == NONE && y >= 412 && y < 448)
|
||||
{
|
||||
if (x >= 380 && x < 480)
|
||||
{
|
||||
action = _event_cancel;
|
||||
}
|
||||
else if (x >= 488 && x < 600)
|
||||
{
|
||||
virtual_keyboard_globals.row = 0;
|
||||
virtual_keyboard_globals.column = 0;
|
||||
action = _event_key_select;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (action != NONE)
|
||||
{
|
||||
virtual_keyboard_globals.last_key = virtual_keyboard_layout_table[
|
||||
|
||||
@@ -874,6 +874,28 @@ static void render_vr_overlay(
|
||||
interface_draw_screen();
|
||||
halo_screen_ui_offset(TRUE);
|
||||
render_ui_widgets(window->local_player_index, &window->rasterizer_camera.viewport_bounds);
|
||||
if (menus_active)
|
||||
{
|
||||
/* the controller's pointer on the menus' screen: a dot, ringed */
|
||||
int pointer_x, pointer_y;
|
||||
|
||||
if (halo_vr_menu_pointer_at(&pointer_x, &pointer_y))
|
||||
{
|
||||
rectangle2d dot;
|
||||
|
||||
pointer_x -= (int)(halo_screen_width() - 640) / 2;
|
||||
dot.x0 = (short)(pointer_x - 5);
|
||||
dot.x1 = (short)(pointer_x + 5);
|
||||
dot.y0 = (short)(pointer_y - 5);
|
||||
dot.y1 = (short)(pointer_y + 5);
|
||||
draw_quad(&dot, 0xE0102030);
|
||||
dot.x0 += 2;
|
||||
dot.x1 -= 2;
|
||||
dot.y0 += 2;
|
||||
dot.y1 -= 2;
|
||||
draw_quad(&dot, 0xFF8FD3FF);
|
||||
}
|
||||
}
|
||||
halo_screen_ui_offset(FALSE);
|
||||
|
||||
rasterizer_window_end();
|
||||
|
||||
Reference in new issue
Block a user