summaryrefslogtreecommitdiffstatshomepage
path: root/src/mess/machine/sms_lphaser.c
blob: 25d5f4273e4e2c2f234cec66d10b7f065fcab2f5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
/**********************************************************************

    Sega Master System "Light Phaser" (light gun) emulation

    Copyright MESS Team.
    Visit http://mamedev.org for licensing and usage restrictions.

**********************************************************************/

#include "sms_lphaser.h"



//**************************************************************************
//  DEVICE DEFINITIONS
//**************************************************************************

const device_type SMS_LIGHT_PHASER = &device_creator<sms_light_phaser_device>;


#define LGUN_RADIUS           6
#define LGUN_X_INTERVAL       4


INPUT_CHANGED_MEMBER( sms_light_phaser_device::th_pin_w )
{
	if (newval != oldval)
		m_port->th_pin_w(newval);
}


INPUT_CHANGED_MEMBER( sms_light_phaser_device::position_changed )
{
	if (newval != oldval)
		sensor_check();
}


static INPUT_PORTS_START( sms_light_phaser )
	PORT_START("CTRL_PORT")
	PORT_BIT( 0x20, IP_ACTIVE_LOW, IPT_BUTTON1 ) // TL (trigger)
	PORT_BIT( 0x40, IP_ACTIVE_LOW, IPT_SPECIAL ) PORT_CHANGED_MEMBER(DEVICE_SELF, sms_light_phaser_device, th_pin_w, 0)
	PORT_BIT( 0x9f, IP_ACTIVE_LOW, IPT_UNUSED )

	PORT_START("LPHASER_X")
	PORT_BIT( 0xff, 0x00, IPT_LIGHTGUN_X) PORT_CROSSHAIR(X, 1.0, 0.0, 0) PORT_SENSITIVITY(50) PORT_KEYDELTA(15)  PORT_CHANGED_MEMBER(DEVICE_SELF, sms_light_phaser_device, position_changed, 0)

	PORT_START("LPHASER_Y")
	PORT_BIT( 0xff, 0x00, IPT_LIGHTGUN_Y) PORT_CROSSHAIR(Y, 1.0, 0.0, 0) PORT_SENSITIVITY(50) PORT_KEYDELTA(15) PORT_CHANGED_MEMBER(DEVICE_SELF, sms_light_phaser_device, position_changed, 0)
INPUT_PORTS_END


//-------------------------------------------------
//  input_ports - device-specific input ports
//-------------------------------------------------

ioport_constructor sms_light_phaser_device::device_input_ports() const
{
	return INPUT_PORTS_NAME( sms_light_phaser );
}



//**************************************************************************
//  LIVE DEVICE
//**************************************************************************

//-------------------------------------------------
//  sms_light_phaser_device - constructor
//-------------------------------------------------

sms_light_phaser_device::sms_light_phaser_device(const machine_config &mconfig, const char *tag, device_t *owner, UINT32 clock) :
	device_t(mconfig, SMS_LIGHT_PHASER, "Light Phaser", tag, owner, clock, "sms_light_phaser", __FILE__),
	device_video_interface(mconfig, *this),
	device_sms_control_port_interface(mconfig, *this),
	m_lphaser_pins(*this, "CTRL_PORT"),
	m_lphaser_x(*this, "LPHASER_X"),
	m_lphaser_y(*this, "LPHASER_Y")
{
}


//-------------------------------------------------
//  device_start - device-specific startup
//-------------------------------------------------

void sms_light_phaser_device::device_start()
{
	m_lphaser_timer = timer_alloc(TIMER_LPHASER);
	m_last_state = 1;
}


//-------------------------------------------------
//  sms_peripheral_r - light phaser read
//-------------------------------------------------

UINT8 sms_light_phaser_device::peripheral_r()
{
	return m_lphaser_pins->read();
}


/*
    Light Phaser (light gun) emulation notes:
    - The sensor is activated based on color brightness of some individual
      pixels being drawn by the beam, at circular area where the gun is aiming.
    - Currently, brightness is calculated based only on single pixels.
    - In general, after the trigger is pressed, games draw the next frame using
      a light color pattern, to make sure sensor will be activated. If emulation
      skips that frame, sensor may stay deactivated. Frameskip set to 0 (no skip)
      is recommended to avoid problems.
    - When sensor switches from on (0) to off (1), a value is latched for the
      HCount register.
    - When sensor switches from off to on, a flag is set. The emulation uses the
      flag to signal that TH line is activated when the status of the input port
      is read. After read, the flag is cleared, or else it is cleared later when
      the Pause status is read (end of a frame). This is necessary because the
      "Color & Switch Test" ROM only reads the TH state after VINT occurs.
    - The gun test of "Color & Switch Test" is an example that requires checks
      of sensor status independent of other events, like trigger press or TH bit
      reads. Another example is the title screen of "Hang-On & Safari Hunt", where
      the game only reads HCount register in a loop, expecting a latch by the gun.
    - The whole procedure is managed by a timer callback, that always reschedule
      itself to run in some intervals when the beam is at the circular area.
*/
int sms_light_phaser_device::bright_aim_area( emu_timer *timer, int lgun_x, int lgun_y )
{
	const int r_x_r = LGUN_RADIUS * LGUN_RADIUS;
	const rectangle &visarea = m_screen->visible_area();
	int beam_x = m_screen->hpos();
	int beam_y = m_screen->vpos();
	int dx, dy;
	int result = 1;
	int pos_changed = 0;
	double dx_radius;

	while (1)
	{
		/* If beam's y isn't at a line where the aim area is, change it
		   the next line it enters that area. */
		dy = abs(beam_y - lgun_y);
		if (dy > LGUN_RADIUS || beam_y < visarea.min_y || beam_y > visarea.max_y)
		{
			beam_y = lgun_y - LGUN_RADIUS;
			if (beam_y < visarea.min_y)
				beam_y = visarea.min_y;
			dy = abs(beam_y - lgun_y);
			pos_changed = 1;
		}

		/* Caculate distance in x of the radius, relative to beam's y distance.
		   First try some shortcuts. */
		switch (dy)
		{
		case LGUN_RADIUS:
			dx_radius = 0;
			break;
		case 0:
			dx_radius = LGUN_RADIUS;
			break;
		default:
			/* step 1: r^2 = dx^2 + dy^2 */
			/* step 2: dx^2 = r^2 - dy^2 */
			/* step 3: dx = sqrt(r^2 - dy^2) */
			dx_radius = ceil((float) sqrt((float) (r_x_r - (dy * dy))));
		}

		/* If beam's x isn't in the circular aim area, change it
		   to the next point it enters that area. */
		dx = abs(beam_x - lgun_x);
		if (dx > dx_radius || beam_x < visarea.min_x || beam_x > visarea.max_x)
		{
			/* If beam's x has passed the aim area, advance to
			   next line and recheck y/x coordinates. */
			if (beam_x > lgun_x)
			{
				beam_x = 0;
				beam_y++;
				continue;
			}
			beam_x = lgun_x - dx_radius;
			if (beam_x < visarea.min_x)
				beam_x = visarea.min_x;
			pos_changed = 1;
		}

		if (!pos_changed)
		{
			rgb_t color;
			UINT8 brightness;
			/* brightness of the lightgray color in the frame drawn by Light Phaser games */
			const UINT8 sensor_min_brightness = 0x7f;

			if (m_last_state == 0) /* sensor is on */
			{
				/* keep sensor on until out of the aim area */
				result = 0;
				/* Set next check for when sensor will be off */
				beam_x = lgun_x + dx_radius + 1;
				if (beam_x > visarea.max_x)
					beam_x = visarea.max_x + 1;
				break;
			}

			color = m_port->pixel_r();

			/* reference: http://www.w3.org/TR/AERT#color-contrast */
			brightness = (RGB_RED(color) * 0.299) + (RGB_GREEN(color) * 0.587) + (RGB_BLUE(color) * 0.114);
			//printf ("color brightness: %2X for x %d y %d\n", brightness, beam_x, beam_y);

			result = (brightness >= sensor_min_brightness) ? 0 : 1;

			/* next check at same line */
			beam_x += LGUN_X_INTERVAL;
			pos_changed = 1;
		}
		else
			break;
	}
	timer->adjust(m_screen->time_until_pos(beam_y, beam_x));

	return result;
}


UINT16 sms_light_phaser_device::screen_hpos_nonscaled(int scaled_hpos)
{
	const rectangle &visarea = m_screen->visible_area();
	int offset_x = (scaled_hpos * (visarea.max_x - visarea.min_x)) / 255;
	return visarea.min_x + offset_x;
}


UINT16 sms_light_phaser_device::screen_vpos_nonscaled(int scaled_vpos)
{
	const rectangle &visarea = m_screen->visible_area();
	int offset_y = (scaled_vpos * (visarea.max_y - visarea.min_y)) / 255;
	return visarea.min_y + offset_y;
}


void sms_light_phaser_device::sensor_check()
{
	int new_state;

	const int x = screen_hpos_nonscaled(m_lphaser_x->read());
	const int y = screen_vpos_nonscaled(m_lphaser_y->read());

	new_state = bright_aim_area(m_lphaser_timer, x, y);
	if (new_state != m_last_state)
	{
		m_port->th_pin_w(new_state);
		m_last_state = new_state;
	}
}


void sms_light_phaser_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr)
{
	switch (id)
	{
	case TIMER_LPHASER:
		sensor_check();
		break;
	default:
		assert_always(FALSE, "Unknown id in sms_light_phaser_device::device_timer");
	}
}